forked from GitHub-Mirror/riotX-android
Import old SDK as legacy code to replace smoothly
This commit is contained in:
parent
058f1704fa
commit
3215fa47d5
Binary file not shown.
@ -44,7 +44,6 @@ class HomeActivity : RiotActivity() {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
fun newIntent(context: Context): Intent {
|
||||
|
@ -43,7 +43,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation fileTree(dir: 'libs', include: ['*.aar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
|
||||
implementation "com.android.support:appcompat-v7:$support_version"
|
||||
@ -52,11 +52,13 @@ dependencies {
|
||||
// Network
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
|
||||
implementation 'com.squareup.retrofit2:converter-moshi:2.4.0'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
|
||||
implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
|
||||
implementation 'com.squareup.okio:okio:1.15.0'
|
||||
|
||||
implementation 'com.google.code.gson:gson:2.8.5'
|
||||
implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
|
||||
|
||||
|
BIN
matrix-sdk-android/libs/olm-sdk.aar
Normal file
BIN
matrix-sdk-android/libs/olm-sdk.aar
Normal file
Binary file not shown.
BIN
matrix-sdk-android/libs/react-native-webrtc.aar
Normal file
BIN
matrix-sdk-android/libs/react-native-webrtc.aar
Normal file
Binary file not shown.
@ -1,2 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="im.vector.matrix.android" />
|
||||
package="im.vector.matrix.android" >
|
||||
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
</manifest>
|
||||
|
@ -1,3 +0,0 @@
|
||||
package im.vector.matrix.android.api.events
|
||||
|
||||
class EventContent : HashMap<Any, Any>()
|
@ -1,9 +1,13 @@
|
||||
package im.vector.matrix.android.api.failure
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
data class MatrixError(@Json(name = "errcode") val code: String,
|
||||
@Json(name = "error") val message: String) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MatrixError(
|
||||
@Json(name = "errcode") val code: String,
|
||||
@Json(name = "error") val message: String
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val FORBIDDEN = "M_FORBIDDEN"
|
||||
|
@ -9,7 +9,7 @@ import retrofit2.Retrofit
|
||||
|
||||
class SessionModule(private val connectionConfig: HomeServerConnectionConfig) : Module {
|
||||
|
||||
override fun invoke(): ModuleDefinition = module {
|
||||
override fun invoke(): ModuleDefinition = module(override = true) {
|
||||
scope(DefaultSession.SCOPE) {
|
||||
val retrofitBuilder = get() as Retrofit.Builder
|
||||
retrofitBuilder
|
||||
|
@ -8,7 +8,7 @@ import retrofit2.Retrofit
|
||||
|
||||
class SyncModule : Module {
|
||||
|
||||
override fun invoke(): ModuleDefinition = module {
|
||||
override fun invoke(): ModuleDefinition = module(override = true) {
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
val retrofit: Retrofit = get()
|
||||
|
@ -1,538 +0,0 @@
|
||||
/*
|
||||
* Copyright 2014 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*//*
|
||||
|
||||
package im.vector.matrix.android.internal.events.sync
|
||||
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import im.vector.matrix.android.api.events.Event
|
||||
import im.vector.matrix.android.api.events.EventType
|
||||
import im.vector.matrix.android.internal.events.sync.data.SyncResponse
|
||||
import java.util.*
|
||||
|
||||
class SyncResponseHandler {
|
||||
|
||||
*/
|
||||
/**
|
||||
* Manage the sync accountData field
|
||||
*
|
||||
* @param accountData the account data
|
||||
* @param isInitialSync true if it is an initial sync response
|
||||
*//*
|
||||
|
||||
private fun manageAccountData(accountData: Map<String, Any>?, isInitialSync: Boolean) {
|
||||
try {
|
||||
if (accountData!!.containsKey("events")) {
|
||||
val events = accountData["events"] as List<Map<String, Any>>
|
||||
|
||||
if (!events.isEmpty()) {
|
||||
// ignored users list
|
||||
manageIgnoredUsers(events, isInitialSync)
|
||||
// push rules
|
||||
managePushRulesUpdate(events)
|
||||
// direct messages rooms
|
||||
manageDirectChatRooms(events, isInitialSync)
|
||||
// URL preview
|
||||
manageUrlPreview(events)
|
||||
// User widgets
|
||||
manageUserWidgets(events)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
*/
|
||||
/**
|
||||
* Refresh the push rules from the account data events list
|
||||
*
|
||||
* @param events the account data events.
|
||||
*//*
|
||||
|
||||
private fun managePushRulesUpdate(events: List<Map<String, Any>>) {
|
||||
for (event in events) {
|
||||
val type = event["type"] as String
|
||||
if (TextUtils.equals(type, "m.push_rules")) {
|
||||
if (event.containsKey("content")) {
|
||||
val gson = JsonUtils.getGson(false)
|
||||
// convert the data to PushRulesResponse
|
||||
// because BingRulesManager supports only PushRulesResponse
|
||||
val element = gson.toJsonTree(event["content"])
|
||||
getBingRulesManager().buildRules(gson.fromJson(element, PushRulesResponse::class.java))
|
||||
// warn the client that the push rules have been updated
|
||||
onBingRulesUpdate()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
/**
|
||||
* Check if the ignored users list is updated
|
||||
*
|
||||
* @param events the account data events list
|
||||
*//*
|
||||
|
||||
private fun manageIgnoredUsers(events: List<Map<String, Any>>, isInitialSync: Boolean) {
|
||||
val newIgnoredUsers = ignoredUsers(events)
|
||||
|
||||
if (null != newIgnoredUsers) {
|
||||
val curIgnoredUsers = getIgnoredUserIds()
|
||||
|
||||
// the both lists are not empty
|
||||
if (0 != newIgnoredUsers.size || 0 != curIgnoredUsers.size) {
|
||||
// check if the ignored users list has been updated
|
||||
if (newIgnoredUsers.size != curIgnoredUsers.size || !newIgnoredUsers.containsAll(curIgnoredUsers)) {
|
||||
// update the store
|
||||
mStore.setIgnoredUserIdsList(newIgnoredUsers)
|
||||
mIgnoredUserIdsList = newIgnoredUsers
|
||||
|
||||
if (!isInitialSync) {
|
||||
// warn there is an update
|
||||
onIgnoredUsersListUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
/**
|
||||
* Extract the ignored users list from the account data events list..
|
||||
*
|
||||
* @param events the account data events list.
|
||||
* @return the ignored users list. null means that there is no defined user ids list.
|
||||
*//*
|
||||
|
||||
private fun ignoredUsers(events: List<Map<String, Any>>): List<String>? {
|
||||
var ignoredUsers: List<String>? = null
|
||||
|
||||
if (0 != events.size) {
|
||||
for (event in events) {
|
||||
val type = event["type"] as String
|
||||
|
||||
if (TextUtils.equals(type, AccountDataRestClient.ACCOUNT_DATA_TYPE_IGNORED_USER_LIST)) {
|
||||
if (event.containsKey("content")) {
|
||||
val contentDict = event["content"] as Map<String, Any>
|
||||
|
||||
if (contentDict.containsKey(AccountDataRestClient.ACCOUNT_DATA_KEY_IGNORED_USERS)) {
|
||||
val ignored_users = contentDict[AccountDataRestClient.ACCOUNT_DATA_KEY_IGNORED_USERS] as Map<String, Any>
|
||||
|
||||
if (null != ignored_users) {
|
||||
ignoredUsers = ArrayList(ignored_users.keys)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return ignoredUsers
|
||||
}
|
||||
|
||||
|
||||
*/
|
||||
/**
|
||||
* Extract the direct chat rooms list from the dedicated events.
|
||||
*
|
||||
* @param events the account data events list.
|
||||
*//*
|
||||
|
||||
private fun manageDirectChatRooms(events: List<Map<String, Any>>, isInitialSync: Boolean) {
|
||||
if (0 != events.size) {
|
||||
for (event in events) {
|
||||
val type = event["type"] as String
|
||||
|
||||
if (TextUtils.equals(type, AccountDataRestClient.ACCOUNT_DATA_TYPE_DIRECT_MESSAGES)) {
|
||||
if (event.containsKey("content")) {
|
||||
val contentDict = event["content"] as Map<String, List<String>>
|
||||
|
||||
Log.d(LOG_TAG, "## manageDirectChatRooms() : update direct chats map$contentDict")
|
||||
|
||||
mStore.setDirectChatRoomsDict(contentDict)
|
||||
|
||||
// reset the current list of the direct chat roomIDs
|
||||
// to update it
|
||||
mLocalDirectChatRoomIdsList = null
|
||||
|
||||
if (!isInitialSync) {
|
||||
// warn there is an update
|
||||
onDirectMessageChatRoomsListUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
/**
|
||||
* Manage the URL preview flag
|
||||
*
|
||||
* @param events the events list
|
||||
*//*
|
||||
|
||||
private fun manageUrlPreview(events: List<Map<String, Any>>) {
|
||||
if (0 != events.size) {
|
||||
for (event in events) {
|
||||
val type = event["type"] as String
|
||||
|
||||
if (TextUtils.equals(type, AccountDataRestClient.ACCOUNT_DATA_TYPE_PREVIEW_URLS)) {
|
||||
if (event.containsKey("content")) {
|
||||
val contentDict = event["content"] as Map<String, Any>
|
||||
|
||||
Log.d(LOG_TAG, "## manageUrlPreview() : $contentDict")
|
||||
var enable = true
|
||||
if (contentDict.containsKey(AccountDataRestClient.ACCOUNT_DATA_KEY_URL_PREVIEW_DISABLE)) {
|
||||
enable = !(contentDict[AccountDataRestClient.ACCOUNT_DATA_KEY_URL_PREVIEW_DISABLE] as Boolean)
|
||||
}
|
||||
|
||||
mStore.setURLPreviewEnabled(enable)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
/**
|
||||
* Manage the user widgets
|
||||
*
|
||||
* @param events the events list
|
||||
*//*
|
||||
|
||||
private fun manageUserWidgets(events: List<Map<String, Any>>) {
|
||||
if (0 != events.size) {
|
||||
for (event in events) {
|
||||
val type = event["type"] as String
|
||||
|
||||
if (TextUtils.equals(type, AccountDataRestClient.ACCOUNT_DATA_TYPE_WIDGETS)) {
|
||||
if (event.containsKey("content")) {
|
||||
val contentDict = event["content"] as Map<String, Any>
|
||||
|
||||
Log.d(LOG_TAG, "## manageUserWidgets() : $contentDict")
|
||||
|
||||
mStore.setUserWidgets(contentDict)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//================================================================================
|
||||
// Sync V2
|
||||
//================================================================================
|
||||
|
||||
*/
|
||||
/**
|
||||
* Handle a presence event.
|
||||
*
|
||||
* @param presenceEvent the presence event.
|
||||
*//*
|
||||
|
||||
private fun handlePresenceEvent(presenceEvent: Event) {
|
||||
// Presence event
|
||||
if (presenceEvent.type == EventType.PRESENCE) {
|
||||
val userPresence = presenceEvent.content<>()
|
||||
// use the sender by default
|
||||
if (!TextUtils.isEmpty(presenceEvent.sender)) {
|
||||
userPresence.user_id = presenceEvent.sender
|
||||
}
|
||||
var user = mStore.getUser(userPresence.user_id)
|
||||
|
||||
if (user == null) {
|
||||
user = userPresence
|
||||
user!!.setDataHandler(this)
|
||||
} else {
|
||||
user!!.currently_active = userPresence.currently_active
|
||||
user!!.presence = userPresence.presence
|
||||
user!!.lastActiveAgo = userPresence.lastActiveAgo
|
||||
}
|
||||
|
||||
user!!.setLatestPresenceTs(System.currentTimeMillis())
|
||||
|
||||
// check if the current user has been updated
|
||||
if (mCredentials.userId.equals(user!!.user_id)) {
|
||||
// always use the up-to-date information
|
||||
getMyUser().displayname = user!!.displayname
|
||||
getMyUser().avatar_url = user!!.getAvatarUrl()
|
||||
|
||||
mStore.setAvatarURL(user!!.getAvatarUrl(), presenceEvent.getOriginServerTs())
|
||||
mStore.setDisplayName(user!!.displayname, presenceEvent.getOriginServerTs())
|
||||
}
|
||||
|
||||
mStore.storeUser(user)
|
||||
onPresenceUpdate(presenceEvent, user)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun manageResponse(syncResponse: SyncResponse?, fromToken: String?, isCatchingUp: Boolean) {
|
||||
val isInitialSync = fromToken == null
|
||||
var isEmptyResponse = true
|
||||
if (syncResponse == null) {
|
||||
return
|
||||
}
|
||||
// Handle the to device events before the room ones
|
||||
// to ensure to decrypt them properly
|
||||
if (syncResponse.toDevice?.events?.isNotEmpty() == true) {
|
||||
for (toDeviceEvent in syncResponse.toDevice.events) {
|
||||
handleToDeviceEvent(toDeviceEvent)
|
||||
}
|
||||
}
|
||||
// Handle account data before the room events
|
||||
// to be able to update direct chats dictionary during invites handling.
|
||||
manageAccountData(syncResponse.accountData, isInitialSync)
|
||||
// joined rooms events
|
||||
if (syncResponse.rooms?.join?.isNotEmpty() == true) {
|
||||
Log.d(LOG_TAG, "Received " + syncResponse.rooms.join.size + " joined rooms")
|
||||
val roomIds = syncResponse.rooms.join.keys
|
||||
// Handle first joined rooms
|
||||
for (roomId in roomIds) {
|
||||
try {
|
||||
if (null != mLeftRoomsStore.getRoom(roomId)) {
|
||||
Log.d(LOG_TAG, "the room $roomId moves from left to the joined ones")
|
||||
mLeftRoomsStore.deleteRoom(roomId)
|
||||
}
|
||||
|
||||
getRoom(roomId).handleJoinedRoomSync(syncResponse.rooms.join[roomId], isInitialSync)
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "## manageResponse() : handleJoinedRoomSync failed " + e.message + " for room " + roomId, e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
isEmptyResponse = false
|
||||
}
|
||||
|
||||
// invited room management
|
||||
if (syncResponse.rooms?.invite?.isNotEmpty() == true) {
|
||||
Log.d(LOG_TAG, "Received " + syncResponse.rooms.invite.size + " invited rooms")
|
||||
|
||||
val roomIds = syncResponse.rooms.invite.keys
|
||||
|
||||
var updatedDirectChatRoomsDict: MutableMap<String, List<String>>? = null
|
||||
var hasChanged = false
|
||||
|
||||
for (roomId in roomIds) {
|
||||
try {
|
||||
Log.d(LOG_TAG, "## manageResponse() : the user has been invited to $roomId")
|
||||
val room = getRoom(roomId)
|
||||
val invitedRoomSync = syncResponse.rooms.invite[roomId]
|
||||
room.handleInvitedRoomSync(invitedRoomSync)
|
||||
// Handle here the invites to a direct chat.
|
||||
if (room.isDirectChatInvitation()) {
|
||||
// Retrieve the inviter user id.
|
||||
var participantUserId: String? = null
|
||||
for (event in invitedRoomSync.inviteState.events) {
|
||||
if (null != event.sender) {
|
||||
participantUserId = event.sender
|
||||
break
|
||||
}
|
||||
}
|
||||
if (null != participantUserId) {
|
||||
// Prepare the updated dictionary.
|
||||
if (null == updatedDirectChatRoomsDict) {
|
||||
if (null != getStore().getDirectChatRoomsDict()) {
|
||||
// Consider the current dictionary.
|
||||
updatedDirectChatRoomsDict = HashMap(getStore().getDirectChatRoomsDict())
|
||||
} else {
|
||||
updatedDirectChatRoomsDict = HashMap()
|
||||
}
|
||||
}
|
||||
|
||||
val roomIdsList: MutableList<String>
|
||||
if (updatedDirectChatRoomsDict!!.containsKey(participantUserId)) {
|
||||
roomIdsList = ArrayList(updatedDirectChatRoomsDict[participantUserId])
|
||||
} else {
|
||||
roomIdsList = ArrayList()
|
||||
}
|
||||
|
||||
// Check whether the room was not yet seen as direct chat
|
||||
if (roomIdsList.indexOf(roomId) < 0) {
|
||||
Log.d(LOG_TAG, "## manageResponse() : add this new invite in direct chats")
|
||||
|
||||
roomIdsList.add(roomId) // update room list with the new room
|
||||
updatedDirectChatRoomsDict[participantUserId] = roomIdsList
|
||||
hasChanged = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "## manageResponse() : handleInvitedRoomSync failed " + e.message + " for room " + roomId, e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
isEmptyResponse = false
|
||||
|
||||
if (hasChanged) {
|
||||
// Update account data to add new direct chat room(s)
|
||||
mAccountDataRestClient.setAccountData(mCredentials.userId, AccountDataRestClient.ACCOUNT_DATA_TYPE_DIRECT_MESSAGES,
|
||||
updatedDirectChatRoomsDict, object : ApiCallback<Void>() {
|
||||
fun onSuccess(info: Void) {
|
||||
Log.d(LOG_TAG, "## manageResponse() : succeeds")
|
||||
}
|
||||
|
||||
fun onNetworkError(e: Exception) {
|
||||
Log.e(LOG_TAG, "## manageResponse() : update account data failed " + e.message, e)
|
||||
// TODO: we should try again.
|
||||
}
|
||||
|
||||
fun onMatrixError(e: MatrixError) {
|
||||
Log.e(LOG_TAG, "## manageResponse() : update account data failed " + e.getMessage())
|
||||
}
|
||||
|
||||
fun onUnexpectedError(e: Exception) {
|
||||
Log.e(LOG_TAG, "## manageResponse() : update account data failed " + e.message, e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// left room management
|
||||
// it should be done at the end but it seems there is a server issue
|
||||
// when inviting after leaving a room, the room is defined in the both leave & invite rooms list.
|
||||
if (null != syncResponse.rooms.leave && syncResponse.rooms.leave.size > 0) {
|
||||
Log.d(LOG_TAG, "Received " + syncResponse.rooms.leave.size + " left rooms")
|
||||
|
||||
val roomIds = syncResponse.rooms.leave.keys
|
||||
|
||||
for (roomId in roomIds) {
|
||||
// RoomSync leftRoomSync = syncResponse.rooms.leave.get(roomId);
|
||||
|
||||
// Presently we remove the existing room from the rooms list.
|
||||
// FIXME SYNC V2 Archive/Display the left rooms!
|
||||
// For that create 'handleArchivedRoomSync' method
|
||||
|
||||
var membership = RoomMember.MEMBERSHIP_LEAVE
|
||||
val room = getRoom(roomId)
|
||||
|
||||
// Retrieve existing room
|
||||
// check if the room still exists.
|
||||
if (null != room) {
|
||||
// use 'handleJoinedRoomSync' to pass the last events to the room before leaving it.
|
||||
// The room will then be able to notify its listeners.
|
||||
room!!.handleJoinedRoomSync(syncResponse.rooms.leave[roomId], isInitialSync)
|
||||
|
||||
val member = room!!.getMember(getUserId())
|
||||
if (null != member) {
|
||||
membership = member!!.membership
|
||||
}
|
||||
|
||||
Log.d(LOG_TAG, "## manageResponse() : leave the room $roomId")
|
||||
}
|
||||
|
||||
if (!TextUtils.equals(membership, RoomMember.MEMBERSHIP_KICK) && !TextUtils.equals(membership, RoomMember.MEMBERSHIP_BAN)) {
|
||||
// ensure that the room data are properly deleted
|
||||
getStore().deleteRoom(roomId)
|
||||
onLeaveRoom(roomId)
|
||||
} else {
|
||||
onRoomKick(roomId)
|
||||
}
|
||||
|
||||
// don't add to the left rooms if the user has been kicked / banned
|
||||
if (mAreLeftRoomsSynced && TextUtils.equals(membership, RoomMember.MEMBERSHIP_LEAVE)) {
|
||||
val leftRoom = getRoom(mLeftRoomsStore, roomId, true)
|
||||
leftRoom.handleJoinedRoomSync(syncResponse.rooms.leave[roomId], isInitialSync)
|
||||
}
|
||||
}
|
||||
|
||||
isEmptyResponse = false
|
||||
}
|
||||
}
|
||||
|
||||
// groups
|
||||
if (null != syncResponse.groups) {
|
||||
// Handle invited groups
|
||||
if (null != syncResponse.groups.invite && !syncResponse.groups.invite.isEmpty()) {
|
||||
// Handle invited groups
|
||||
for (groupId in syncResponse.groups.invite.keySet()) {
|
||||
val invitedGroupSync = syncResponse.groups.invite.get(groupId)
|
||||
mGroupsManager.onNewGroupInvitation(groupId, invitedGroupSync.profile, invitedGroupSync.inviter, !isInitialSync)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle joined groups
|
||||
if (null != syncResponse.groups.join && !syncResponse.groups.join.isEmpty()) {
|
||||
for (groupId in syncResponse.groups.join.keySet()) {
|
||||
mGroupsManager.onJoinGroup(groupId, !isInitialSync)
|
||||
}
|
||||
}
|
||||
// Handle left groups
|
||||
if (null != syncResponse.groups.leave && !syncResponse.groups.leave.isEmpty()) {
|
||||
// Handle joined groups
|
||||
for (groupId in syncResponse.groups.leave.keySet()) {
|
||||
mGroupsManager.onLeaveGroup(groupId, !isInitialSync)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle presence of other users
|
||||
if (null != syncResponse.presence && null != syncResponse.presence.events) {
|
||||
Log.d(LOG_TAG, "Received " + syncResponse.presence.events.size + " presence events")
|
||||
for (presenceEvent in syncResponse.presence.events) {
|
||||
handlePresenceEvent(presenceEvent)
|
||||
}
|
||||
}
|
||||
|
||||
if (null != mCrypto) {
|
||||
mCrypto.onSyncCompleted(syncResponse, fromToken, isCatchingUp)
|
||||
}
|
||||
|
||||
val store = getStore()
|
||||
|
||||
if (!isEmptyResponse && null != store) {
|
||||
store!!.setEventStreamToken(syncResponse.nextBatch)
|
||||
store!!.commit()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
*/
|
||||
/*
|
||||
* Handle a 'toDevice' event
|
||||
* @param event the event
|
||||
*//*
|
||||
|
||||
private fun handleToDeviceEvent(event: Event) {
|
||||
// Decrypt event if necessary
|
||||
decryptEvent(event, null)
|
||||
|
||||
if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_MESSAGE)
|
||||
&& null != event.getContent()
|
||||
&& TextUtils.equals(JsonUtils.getMessageMsgType(event.getContent()), "m.bad.encrypted")) {
|
||||
Log.e(LOG_TAG, "## handleToDeviceEvent() : Warning: Unable to decrypt to-device event : " + event.getContent())
|
||||
} else {
|
||||
onToDeviceEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val LOG_TAG = MXDataHandler::class.java!!.getSimpleName()
|
||||
|
||||
private val LEFT_ROOMS_FILTER = "{\"room\":{\"timeline\":{\"limit\":1},\"include_leave\":true}}"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
*/
|
@ -4,6 +4,8 @@ import com.squareup.moshi.Moshi
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.events.sync.data.SyncResponse
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.filter.FilterBody
|
||||
import im.vector.matrix.android.internal.legacy.util.FilterUtil
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.util.CancelableCoroutine
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
@ -17,8 +19,10 @@ class Synchronizer(private val syncAPI: SyncAPI,
|
||||
fun synchronize(callback: MatrixCallback<SyncResponse>): Cancelable {
|
||||
val job = GlobalScope.launch(coroutineDispatchers.main) {
|
||||
val params = HashMap<String, String>()
|
||||
val filterBody = FilterBody()
|
||||
FilterUtil.enableLazyLoading(filterBody, true)
|
||||
params["timeout"] = "0"
|
||||
params["filter"] = "{}"
|
||||
params["filter"] = filterBody.toJSONString()
|
||||
val syncResponse = executeRequest<SyncResponse> {
|
||||
apiCall = syncAPI.sync(params)
|
||||
moshi = jsonMapper
|
||||
|
@ -0,0 +1,532 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.login.Credentials;
|
||||
import im.vector.matrix.android.internal.legacy.ssl.Fingerprint;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.CipherSuite;
|
||||
import okhttp3.TlsVersion;
|
||||
|
||||
/**
|
||||
* Represents how to connect to a specific Homeserver, may include credentials to use.
|
||||
*/
|
||||
public class HomeServerConnectionConfig {
|
||||
|
||||
// the home server URI
|
||||
private Uri mHsUri;
|
||||
// the identity server URI
|
||||
private Uri mIdentityServerUri;
|
||||
// the anti-virus server URI
|
||||
private Uri mAntiVirusServerUri;
|
||||
// allowed fingerprints
|
||||
private List<Fingerprint> mAllowedFingerprints = new ArrayList<>();
|
||||
// the credentials
|
||||
private Credentials mCredentials;
|
||||
// tell whether we should reject X509 certs that were issued by trusts CAs and only trustcerts with matching fingerprints.
|
||||
private boolean mPin;
|
||||
// the accepted TLS versions
|
||||
private List<TlsVersion> mTlsVersions;
|
||||
// the accepted TLS cipher suites
|
||||
private List<CipherSuite> mTlsCipherSuites;
|
||||
// should accept TLS extensions
|
||||
private boolean mShouldAcceptTlsExtensions = true;
|
||||
// allow Http connection
|
||||
private boolean mAllowHttpExtension;
|
||||
// Force usage of TLS versions
|
||||
private boolean mForceUsageTlsVersions;
|
||||
|
||||
/**
|
||||
* Private constructor. Please use the Builder
|
||||
*/
|
||||
private HomeServerConnectionConfig() {
|
||||
// Private constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the home server URI.
|
||||
*
|
||||
* @param uri the new HS uri
|
||||
*/
|
||||
public void setHomeserverUri(Uri uri) {
|
||||
mHsUri = uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the home server uri
|
||||
*/
|
||||
public Uri getHomeserverUri() {
|
||||
return mHsUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the identity server uri
|
||||
*/
|
||||
public Uri getIdentityServerUri() {
|
||||
if (null != mIdentityServerUri) {
|
||||
return mIdentityServerUri;
|
||||
}
|
||||
// Else consider the HS uri by default.
|
||||
return mHsUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the anti-virus server uri
|
||||
*/
|
||||
public Uri getAntiVirusServerUri() {
|
||||
if (null != mAntiVirusServerUri) {
|
||||
return mAntiVirusServerUri;
|
||||
}
|
||||
// Else consider the HS uri by default.
|
||||
return mHsUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the allowed fingerprints.
|
||||
*/
|
||||
public List<Fingerprint> getAllowedFingerprints() {
|
||||
return mAllowedFingerprints;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the credentials
|
||||
*/
|
||||
public Credentials getCredentials() {
|
||||
return mCredentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the credentials.
|
||||
*
|
||||
* @param credentials the new credentials
|
||||
*/
|
||||
public void setCredentials(Credentials credentials) {
|
||||
mCredentials = credentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether we should reject X509 certs that were issued by trusts CAs and only trust
|
||||
* certs with matching fingerprints.
|
||||
*/
|
||||
public boolean shouldPin() {
|
||||
return mPin;
|
||||
}
|
||||
|
||||
/**
|
||||
* TLS versions accepted for TLS connections with the home server.
|
||||
*/
|
||||
@Nullable
|
||||
public List<TlsVersion> getAcceptedTlsVersions() {
|
||||
return mTlsVersions;
|
||||
}
|
||||
|
||||
/**
|
||||
* TLS cipher suites accepted for TLS connections with the home server.
|
||||
*/
|
||||
@Nullable
|
||||
public List<CipherSuite> getAcceptedTlsCipherSuites() {
|
||||
return mTlsCipherSuites;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether we should accept TLS extensions.
|
||||
*/
|
||||
public boolean shouldAcceptTlsExtensions() {
|
||||
return mShouldAcceptTlsExtensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if Http connection is allowed (false by default).
|
||||
*/
|
||||
public boolean isHttpConnectionAllowed() {
|
||||
return mAllowHttpExtension;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the usage of TlsVersions has to be forced
|
||||
*/
|
||||
public boolean forceUsageOfTlsVersions() {
|
||||
return mForceUsageTlsVersions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HomeserverConnectionConfig{" +
|
||||
"mHsUri=" + mHsUri +
|
||||
", mIdentityServerUri=" + mIdentityServerUri +
|
||||
", mAntiVirusServerUri=" + mAntiVirusServerUri +
|
||||
", mAllowedFingerprints size=" + mAllowedFingerprints.size() +
|
||||
", mCredentials=" + mCredentials +
|
||||
", mPin=" + mPin +
|
||||
", mShouldAcceptTlsExtensions=" + mShouldAcceptTlsExtensions +
|
||||
", mTlsVersions=" + (null == mTlsVersions ? "" : mTlsVersions.size()) +
|
||||
", mTlsCipherSuites=" + (null == mTlsCipherSuites ? "" : mTlsCipherSuites.size()) +
|
||||
'}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the object instance into a JSon object
|
||||
*
|
||||
* @return the JSon representation
|
||||
* @throws JSONException the JSON conversion failure reason
|
||||
*/
|
||||
public JSONObject toJson() throws JSONException {
|
||||
JSONObject json = new JSONObject();
|
||||
|
||||
json.put("home_server_url", mHsUri.toString());
|
||||
json.put("identity_server_url", getIdentityServerUri().toString());
|
||||
if (mAntiVirusServerUri != null) {
|
||||
json.put("antivirus_server_url", mAntiVirusServerUri.toString());
|
||||
}
|
||||
|
||||
json.put("pin", mPin);
|
||||
|
||||
if (mCredentials != null) json.put("credentials", mCredentials.toJson());
|
||||
if (mAllowedFingerprints != null) {
|
||||
List<JSONObject> fingerprints = new ArrayList<>(mAllowedFingerprints.size());
|
||||
|
||||
for (Fingerprint fingerprint : mAllowedFingerprints) {
|
||||
fingerprints.add(fingerprint.toJson());
|
||||
}
|
||||
|
||||
json.put("fingerprints", new JSONArray(fingerprints));
|
||||
}
|
||||
|
||||
json.put("tls_extensions", mShouldAcceptTlsExtensions);
|
||||
|
||||
if (mTlsVersions != null) {
|
||||
List<String> tlsVersions = new ArrayList<>(mTlsVersions.size());
|
||||
|
||||
for (TlsVersion tlsVersion : mTlsVersions) {
|
||||
tlsVersions.add(tlsVersion.javaName());
|
||||
}
|
||||
|
||||
json.put("tls_versions", new JSONArray(tlsVersions));
|
||||
}
|
||||
|
||||
json.put("force_usage_of_tls_versions", mForceUsageTlsVersions);
|
||||
|
||||
if (mTlsCipherSuites != null) {
|
||||
List<String> tlsCipherSuites = new ArrayList<>(mTlsCipherSuites.size());
|
||||
|
||||
for (CipherSuite tlsCipherSuite : mTlsCipherSuites) {
|
||||
tlsCipherSuites.add(tlsCipherSuite.javaName());
|
||||
}
|
||||
|
||||
json.put("tls_cipher_suites", new JSONArray(tlsCipherSuites));
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an object instance from the json object.
|
||||
*
|
||||
* @param jsonObject the json object
|
||||
* @return a HomeServerConnectionConfig instance
|
||||
* @throws JSONException the conversion failure reason
|
||||
*/
|
||||
public static HomeServerConnectionConfig fromJson(JSONObject jsonObject) throws JSONException {
|
||||
JSONArray fingerprintArray = jsonObject.optJSONArray("fingerprints");
|
||||
List<Fingerprint> fingerprints = new ArrayList<>();
|
||||
if (fingerprintArray != null) {
|
||||
for (int i = 0; i < fingerprintArray.length(); i++) {
|
||||
fingerprints.add(Fingerprint.fromJson(fingerprintArray.getJSONObject(i)));
|
||||
}
|
||||
}
|
||||
|
||||
JSONObject credentialsObj = jsonObject.optJSONObject("credentials");
|
||||
Credentials creds = credentialsObj != null ? Credentials.fromJson(credentialsObj) : null;
|
||||
|
||||
Builder builder = new Builder()
|
||||
.withHomeServerUri(Uri.parse(jsonObject.getString("home_server_url")))
|
||||
.withIdentityServerUri(jsonObject.has("identity_server_url") ? Uri.parse(jsonObject.getString("identity_server_url")) : null)
|
||||
.withCredentials(creds)
|
||||
.withAllowedFingerPrints(fingerprints)
|
||||
.withPin(jsonObject.optBoolean("pin", false));
|
||||
|
||||
// Set the anti-virus server uri if any
|
||||
if (jsonObject.has("antivirus_server_url")) {
|
||||
builder.withAntiVirusServerUri(Uri.parse(jsonObject.getString("antivirus_server_url")));
|
||||
}
|
||||
|
||||
builder.withShouldAcceptTlsExtensions(jsonObject.optBoolean("tls_extensions", true));
|
||||
|
||||
// Set the TLS versions if any
|
||||
if (jsonObject.has("tls_versions")) {
|
||||
JSONArray tlsVersionsArray = jsonObject.optJSONArray("tls_versions");
|
||||
if (tlsVersionsArray != null) {
|
||||
for (int i = 0; i < tlsVersionsArray.length(); i++) {
|
||||
builder.addAcceptedTlsVersion(TlsVersion.forJavaName(tlsVersionsArray.getString(i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.forceUsageOfTlsVersions(jsonObject.optBoolean("force_usage_of_tls_versions", false));
|
||||
|
||||
// Set the TLS cipher suites if any
|
||||
if (jsonObject.has("tls_cipher_suites")) {
|
||||
JSONArray tlsCipherSuitesArray = jsonObject.optJSONArray("tls_cipher_suites");
|
||||
if (tlsCipherSuitesArray != null) {
|
||||
for (int i = 0; i < tlsCipherSuitesArray.length(); i++) {
|
||||
builder.addAcceptedTlsCipherSuite(CipherSuite.forJavaName(tlsCipherSuitesArray.getString(i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder
|
||||
*/
|
||||
public static class Builder {
|
||||
private HomeServerConnectionConfig mHomeServerConnectionConfig;
|
||||
|
||||
/**
|
||||
* Builder constructor
|
||||
*/
|
||||
public Builder() {
|
||||
mHomeServerConnectionConfig = new HomeServerConnectionConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param hsUri The URI to use to connect to the homeserver. Cannot be null
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder withHomeServerUri(final Uri hsUri) {
|
||||
if (hsUri == null || (!"http".equals(hsUri.getScheme()) && !"https".equals(hsUri.getScheme()))) {
|
||||
throw new RuntimeException("Invalid home server URI: " + hsUri);
|
||||
}
|
||||
|
||||
// remove trailing /
|
||||
if (hsUri.toString().endsWith("/")) {
|
||||
try {
|
||||
String url = hsUri.toString();
|
||||
mHomeServerConnectionConfig.mHsUri = Uri.parse(url.substring(0, url.length() - 1));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Invalid home server URI: " + hsUri);
|
||||
}
|
||||
} else {
|
||||
mHomeServerConnectionConfig.mHsUri = hsUri;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param identityServerUri The URI to use to manage identity. Can be null
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder withIdentityServerUri(@Nullable final Uri identityServerUri) {
|
||||
if ((null != identityServerUri) && (!"http".equals(identityServerUri.getScheme()) && !"https".equals(identityServerUri.getScheme()))) {
|
||||
throw new RuntimeException("Invalid identity server URI: " + identityServerUri);
|
||||
}
|
||||
|
||||
// remove trailing /
|
||||
if ((null != identityServerUri) && identityServerUri.toString().endsWith("/")) {
|
||||
try {
|
||||
String url = identityServerUri.toString();
|
||||
mHomeServerConnectionConfig.mIdentityServerUri = Uri.parse(url.substring(0, url.length() - 1));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Invalid identity server URI: " + identityServerUri);
|
||||
}
|
||||
} else {
|
||||
mHomeServerConnectionConfig.mIdentityServerUri = identityServerUri;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param credentials The credentials to use, if needed. Can be null.
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder withCredentials(@Nullable Credentials credentials) {
|
||||
mHomeServerConnectionConfig.mCredentials = credentials;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param allowedFingerprints If using SSL, allow server certs that match these fingerprints.
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder withAllowedFingerPrints(@Nullable List<Fingerprint> allowedFingerprints) {
|
||||
if (allowedFingerprints != null) {
|
||||
mHomeServerConnectionConfig.mAllowedFingerprints.addAll(allowedFingerprints);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pin If true only allow certs matching given fingerprints, otherwise fallback to
|
||||
* standard X509 checks.
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder withPin(boolean pin) {
|
||||
mHomeServerConnectionConfig.mPin = pin;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param shouldAcceptTlsExtension
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder withShouldAcceptTlsExtensions(boolean shouldAcceptTlsExtension) {
|
||||
mHomeServerConnectionConfig.mShouldAcceptTlsExtensions = shouldAcceptTlsExtension;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an accepted TLS version for TLS connections with the home server.
|
||||
*
|
||||
* @param tlsVersion the tls version to add to the set of TLS versions accepted.
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder addAcceptedTlsVersion(@NonNull TlsVersion tlsVersion) {
|
||||
if (mHomeServerConnectionConfig.mTlsVersions == null) {
|
||||
mHomeServerConnectionConfig.mTlsVersions = new ArrayList<>();
|
||||
}
|
||||
|
||||
mHomeServerConnectionConfig.mTlsVersions.add(tlsVersion);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force the usage of TlsVersion. This can be usefull for device on Android version < 20
|
||||
*
|
||||
* @param forceUsageOfTlsVersions set to true to force the usage of specified TlsVersions (with {@link #addAcceptedTlsVersion(TlsVersion)}
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder forceUsageOfTlsVersions(boolean forceUsageOfTlsVersions) {
|
||||
mHomeServerConnectionConfig.mForceUsageTlsVersions = forceUsageOfTlsVersions;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a TLS cipher suite to the list of accepted TLS connections with the home server.
|
||||
*
|
||||
* @param tlsCipherSuite the tls cipher suite to add.
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder addAcceptedTlsCipherSuite(@NonNull CipherSuite tlsCipherSuite) {
|
||||
if (mHomeServerConnectionConfig.mTlsCipherSuites == null) {
|
||||
mHomeServerConnectionConfig.mTlsCipherSuites = new ArrayList<>();
|
||||
}
|
||||
|
||||
mHomeServerConnectionConfig.mTlsCipherSuites.add(tlsCipherSuite);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the anti-virus server URI.
|
||||
*
|
||||
* @param antivirusServerUri the new anti-virus uri. Can be null
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder withAntiVirusServerUri(@Nullable Uri antivirusServerUri) {
|
||||
if ((null != antivirusServerUri) && (!"http".equals(antivirusServerUri.getScheme()) && !"https".equals(antivirusServerUri.getScheme()))) {
|
||||
throw new RuntimeException("Invalid antivirus server URI: " + antivirusServerUri);
|
||||
}
|
||||
|
||||
mHomeServerConnectionConfig.mAntiVirusServerUri = antivirusServerUri;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* For test only: allow Http connection
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public Builder withAllowHttpConnection() {
|
||||
mHomeServerConnectionConfig.mAllowHttpExtension = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient method to limit the TLS versions and cipher suites for this Builder
|
||||
* Ref:
|
||||
* - https://www.ssi.gouv.fr/uploads/2017/02/security-recommendations-for-tls_v1.1.pdf
|
||||
* - https://developer.android.com/reference/javax/net/ssl/SSLEngine
|
||||
*
|
||||
* @param tlsLimitations true to use Tls limitations
|
||||
* @param enableCompatibilityMode set to true for Android < 20
|
||||
* @return this builder
|
||||
*/
|
||||
public Builder withTlsLimitations(boolean tlsLimitations, boolean enableCompatibilityMode) {
|
||||
if (tlsLimitations) {
|
||||
withShouldAcceptTlsExtensions(false);
|
||||
|
||||
// Tls versions
|
||||
addAcceptedTlsVersion(TlsVersion.TLS_1_2);
|
||||
addAcceptedTlsVersion(TlsVersion.TLS_1_3);
|
||||
|
||||
forceUsageOfTlsVersions(enableCompatibilityMode);
|
||||
|
||||
// Cipher suites
|
||||
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256);
|
||||
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256);
|
||||
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256);
|
||||
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256);
|
||||
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384);
|
||||
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384);
|
||||
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256);
|
||||
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256);
|
||||
|
||||
if (enableCompatibilityMode) {
|
||||
// Adopt some preceding cipher suites for Android < 20 to be able to negotiate
|
||||
// a TLS session.
|
||||
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA);
|
||||
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the {@link HomeServerConnectionConfig}
|
||||
*/
|
||||
public HomeServerConnectionConfig build() {
|
||||
// Check mandatory parameters
|
||||
if (mHomeServerConnectionConfig.mHsUri == null) {
|
||||
throw new RuntimeException("Home server URI not set");
|
||||
}
|
||||
|
||||
return mHomeServerConnectionConfig;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* This class contains pattern to match the different Matrix ids
|
||||
*/
|
||||
public class MXPatterns {
|
||||
|
||||
private MXPatterns() {
|
||||
// Cannot be instantiated
|
||||
}
|
||||
|
||||
// Note: TLD is not mandatory (localhost, IP address...)
|
||||
private static final String DOMAIN_REGEX = ":[A-Z0-9.-]+(:[0-9]{2,5})?";
|
||||
|
||||
// regex pattern to find matrix user ids in a string.
|
||||
// See https://matrix.org/speculator/spec/HEAD/appendices.html#historical-user-ids
|
||||
private static final String MATRIX_USER_IDENTIFIER_REGEX = "@[A-Z0-9\\x21-\\x39\\x3B-\\x7F]+" + DOMAIN_REGEX;
|
||||
public static final Pattern PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = Pattern.compile(MATRIX_USER_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE);
|
||||
|
||||
// regex pattern to find room ids in a string.
|
||||
private static final String MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9]+" + DOMAIN_REGEX;
|
||||
public static final Pattern PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER = Pattern.compile(MATRIX_ROOM_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE);
|
||||
|
||||
// regex pattern to find room aliases in a string.
|
||||
private static final String MATRIX_ROOM_ALIAS_REGEX = "#[A-Z0-9._%#@=+-]+" + DOMAIN_REGEX;
|
||||
public static final Pattern PATTERN_CONTAIN_MATRIX_ALIAS = Pattern.compile(MATRIX_ROOM_ALIAS_REGEX, Pattern.CASE_INSENSITIVE);
|
||||
|
||||
// regex pattern to find message ids in a string.
|
||||
private static final String MATRIX_EVENT_IDENTIFIER_REGEX = "\\$[A-Z0-9]+" + DOMAIN_REGEX;
|
||||
public static final Pattern PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER = Pattern.compile(MATRIX_EVENT_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE);
|
||||
|
||||
// regex pattern to find group ids in a string.
|
||||
private static final String MATRIX_GROUP_IDENTIFIER_REGEX = "\\+[A-Z0-9=_\\-./]+" + DOMAIN_REGEX;
|
||||
public static final Pattern PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER = Pattern.compile(MATRIX_GROUP_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE);
|
||||
|
||||
// regex pattern to find permalink with message id.
|
||||
// Android does not support in URL so extract it.
|
||||
private static final String PERMALINK_BASE_REGEX = "https://matrix\\.to/#/";
|
||||
private static final String APP_BASE_REGEX = "https://[A-Z0-9.-]+\\.[A-Z]{2,}/[A-Z]{3,}/#/room/";
|
||||
private static final String SEP_REGEX = "/";
|
||||
|
||||
private static final String LINK_TO_ROOM_ID_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX;
|
||||
public static final Pattern PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID = Pattern.compile(LINK_TO_ROOM_ID_REGEXP, Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final String LINK_TO_ROOM_ALIAS_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX;
|
||||
public static final Pattern PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS = Pattern.compile(LINK_TO_ROOM_ALIAS_REGEXP, Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final String LINK_TO_APP_ROOM_ID_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX;
|
||||
public static final Pattern PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID = Pattern.compile(LINK_TO_APP_ROOM_ID_REGEXP, Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final String LINK_TO_APP_ROOM_ALIAS_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX;
|
||||
public static final Pattern PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS = Pattern.compile(LINK_TO_APP_ROOM_ALIAS_REGEXP, Pattern.CASE_INSENSITIVE);
|
||||
|
||||
// list of patterns to find some matrix item.
|
||||
public static final List<Pattern> MATRIX_PATTERNS = Arrays.asList(
|
||||
MXPatterns.PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID,
|
||||
MXPatterns.PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS,
|
||||
MXPatterns.PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID,
|
||||
MXPatterns.PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS,
|
||||
MXPatterns.PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER,
|
||||
MXPatterns.PATTERN_CONTAIN_MATRIX_ALIAS,
|
||||
MXPatterns.PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER,
|
||||
MXPatterns.PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER,
|
||||
MXPatterns.PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER
|
||||
);
|
||||
|
||||
/**
|
||||
* Tells if a string is a valid user Id.
|
||||
*
|
||||
* @param str the string to test
|
||||
* @return true if the string is a valid user id
|
||||
*/
|
||||
public static boolean isUserId(@Nullable final String str) {
|
||||
return str != null && PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER.matcher(str).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if a string is a valid room id.
|
||||
*
|
||||
* @param str the string to test
|
||||
* @return true if the string is a valid room Id
|
||||
*/
|
||||
public static boolean isRoomId(@Nullable final String str) {
|
||||
return str != null && PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER.matcher(str).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if a string is a valid room alias.
|
||||
*
|
||||
* @param str the string to test
|
||||
* @return true if the string is a valid room alias.
|
||||
*/
|
||||
public static boolean isRoomAlias(@Nullable final String str) {
|
||||
return str != null && PATTERN_CONTAIN_MATRIX_ALIAS.matcher(str).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if a string is a valid event id.
|
||||
*
|
||||
* @param str the string to test
|
||||
* @return true if the string is a valid event id.
|
||||
*/
|
||||
public static boolean isEventId(@Nullable final String str) {
|
||||
return str != null && PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER.matcher(str).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if a string is a valid group id.
|
||||
*
|
||||
* @param str the string to test
|
||||
* @return true if the string is a valid group id.
|
||||
*/
|
||||
public static boolean isGroupId(@Nullable final String str) {
|
||||
return str != null && PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER.matcher(str).matches();
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,738 @@
|
||||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy;
|
||||
|
||||
import android.os.Looper;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.data.MyUser;
|
||||
import im.vector.matrix.android.internal.legacy.data.RoomState;
|
||||
import im.vector.matrix.android.internal.legacy.listeners.IMXEventListener;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.User;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.bingrules.BingRule;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
import im.vector.matrix.android.internal.legacy.util.MXOsHandler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Dispatcher for MXDataHandler
|
||||
* This class store a list of listener and dispatch event to every listener on the Ui Thread
|
||||
*/
|
||||
/* package */ class MxEventDispatcher {
|
||||
private static final String LOG_TAG = MxEventDispatcher.class.getSimpleName();
|
||||
|
||||
private final MXOsHandler mUiHandler;
|
||||
|
||||
@Nullable
|
||||
private IMXEventListener mCryptoEventsListener = null;
|
||||
|
||||
private final Set<IMXEventListener> mEventListeners = new HashSet<>();
|
||||
|
||||
MxEventDispatcher() {
|
||||
mUiHandler = new MXOsHandler(Looper.getMainLooper());
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Public utilities
|
||||
* ========================================================================================== */
|
||||
|
||||
/**
|
||||
* Set the crypto events listener, or remove it
|
||||
*
|
||||
* @param listener the listener or null to remove the listener
|
||||
*/
|
||||
public void setCryptoEventsListener(@Nullable IMXEventListener listener) {
|
||||
mCryptoEventsListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener to the listeners list.
|
||||
*
|
||||
* @param listener the listener to add.
|
||||
*/
|
||||
public void addListener(IMXEventListener listener) {
|
||||
mEventListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a listener from the listeners list.
|
||||
*
|
||||
* @param listener to remove.
|
||||
*/
|
||||
public void removeListener(IMXEventListener listener) {
|
||||
mEventListeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any listener
|
||||
*/
|
||||
public void clearListeners() {
|
||||
mEventListeners.clear();
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Dispatchers
|
||||
* ========================================================================================== */
|
||||
|
||||
public void dispatchOnStoreReady() {
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onStoreReady();
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onStoreReady " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnAccountInfoUpdate(final MyUser myUser) {
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onAccountInfoUpdate(myUser);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onAccountInfoUpdate " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnPresenceUpdate(final Event event, final User user) {
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onPresenceUpdate(event, user);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onPresenceUpdate " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnLiveEvent(final Event event, final RoomState roomState) {
|
||||
if (null != mCryptoEventsListener) {
|
||||
mCryptoEventsListener.onLiveEvent(event, roomState);
|
||||
}
|
||||
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onLiveEvent(event, roomState);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onLiveEvent " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnLiveEventsChunkProcessed(final String startToken, final String toToken) {
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onLiveEventsChunkProcessed(startToken, toToken);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onLiveEventsChunkProcessed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnBingEvent(final Event event, final RoomState roomState, final BingRule bingRule, boolean ignoreEvent) {
|
||||
if (ignoreEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onBingEvent(event, roomState, bingRule);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onBingEvent " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnEventSentStateUpdated(final Event event, boolean ignoreEvent) {
|
||||
if (ignoreEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onEventSentStateUpdated(event);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onEventSentStateUpdated " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnEventSent(final Event event, final String prevEventId, boolean ignoreEvent) {
|
||||
if (ignoreEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onEventSent(event, prevEventId);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onEventSent " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnBingRulesUpdate() {
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onBingRulesUpdate();
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onBingRulesUpdate " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnInitialSyncComplete(final String toToken) {
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onInitialSyncComplete(toToken);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onInitialSyncComplete " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnCryptoSyncComplete() {
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onCryptoSyncComplete();
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "OnCryptoSyncComplete " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnSyncError(final MatrixError matrixError) {
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onSyncError(matrixError);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onSyncError " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnNewRoom(final String roomId, boolean ignoreEvent) {
|
||||
if (ignoreEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onNewRoom(roomId);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onNewRoom " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnJoinRoom(final String roomId, boolean ignoreEvent) {
|
||||
if (ignoreEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onJoinRoom(roomId);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onJoinRoom " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnRoomInternalUpdate(final String roomId, boolean ignoreEvent) {
|
||||
if (ignoreEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onRoomInternalUpdate(roomId);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onRoomInternalUpdate " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnLeaveRoom(final String roomId, boolean ignoreEvent) {
|
||||
if (ignoreEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onLeaveRoom(roomId);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onLeaveRoom " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnRoomKick(final String roomId, boolean ignoreEvent) {
|
||||
if (ignoreEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onRoomKick(roomId);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onRoomKick " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnReceiptEvent(final String roomId, final List<String> senderIds, boolean ignoreEvent) {
|
||||
if (ignoreEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onReceiptEvent(roomId, senderIds);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onReceiptEvent " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnRoomTagEvent(final String roomId, boolean ignoreEvent) {
|
||||
if (ignoreEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onRoomTagEvent(roomId);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onRoomTagEvent " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnReadMarkerEvent(final String roomId, boolean ignoreEvent) {
|
||||
if (ignoreEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onReadMarkerEvent(roomId);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onReadMarkerEvent " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnRoomFlush(final String roomId, boolean ignoreEvent) {
|
||||
if (ignoreEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onRoomFlush(roomId);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onRoomFlush " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnIgnoredUsersListUpdate() {
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onIgnoredUsersListUpdate();
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onIgnoredUsersListUpdate " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnToDeviceEvent(final Event event, boolean ignoreEvent) {
|
||||
if (null != mCryptoEventsListener) {
|
||||
mCryptoEventsListener.onToDeviceEvent(event);
|
||||
}
|
||||
|
||||
if (ignoreEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onToDeviceEvent(event);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "OnToDeviceEvent " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnDirectMessageChatRoomsListUpdate() {
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onDirectMessageChatRoomsListUpdate();
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onDirectMessageChatRoomsListUpdate " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnEventDecrypted(final Event event) {
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onEventDecrypted(event);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onDecryptedEvent " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnNewGroupInvitation(final String groupId) {
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onNewGroupInvitation(groupId);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onNewGroupInvitation " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnJoinGroup(final String groupId) {
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onJoinGroup(groupId);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onJoinGroup " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnLeaveGroup(final String groupId) {
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onLeaveGroup(groupId);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onLeaveGroup " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnGroupProfileUpdate(final String groupId) {
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onGroupProfileUpdate(groupId);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onGroupProfileUpdate " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnGroupRoomsListUpdate(final String groupId) {
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onGroupRoomsListUpdate(groupId);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onGroupRoomsListUpdate " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnGroupUsersListUpdate(final String groupId) {
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onGroupUsersListUpdate(groupId);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onGroupUsersListUpdate " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnGroupInvitedUsersListUpdate(final String groupId) {
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onGroupInvitedUsersListUpdate(groupId);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onGroupInvitedUsersListUpdate " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispatchOnNotificationCountUpdate(final String roomId, boolean ignoreEvent) {
|
||||
if (ignoreEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<IMXEventListener> eventListeners = getListenersSnapshot();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (IMXEventListener listener : eventListeners) {
|
||||
try {
|
||||
listener.onNotificationCountUpdate(roomId);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "onNotificationCountUpdate " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Private
|
||||
* ========================================================================================== */
|
||||
|
||||
/**
|
||||
* @return the current MXEvents listeners.
|
||||
*/
|
||||
private List<IMXEventListener> getListenersSnapshot() {
|
||||
List<IMXEventListener> eventListeners;
|
||||
|
||||
synchronized (this) {
|
||||
eventListeners = new ArrayList<>(mEventListeners);
|
||||
}
|
||||
|
||||
return eventListeners;
|
||||
}
|
||||
}
|
@ -0,0 +1,415 @@
|
||||
/*
|
||||
* Copyright 2014 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.internal.legacy;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import im.vector.matrix.android.BuildConfig;
|
||||
import im.vector.matrix.android.internal.legacy.listeners.IMXNetworkEventListener;
|
||||
import im.vector.matrix.android.internal.legacy.network.NetworkConnectivityReceiver;
|
||||
import im.vector.matrix.android.internal.legacy.rest.client.MXRestExecutorService;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.login.Credentials;
|
||||
import im.vector.matrix.android.internal.legacy.ssl.CertUtil;
|
||||
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
import im.vector.matrix.android.internal.legacy.util.PolymorphicRequestBodyConverter;
|
||||
import im.vector.matrix.android.internal.legacy.util.UnsentEventsManager;
|
||||
import okhttp3.Dispatcher;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.logging.HttpLoggingInterceptor;
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.gson.GsonConverterFactory;
|
||||
|
||||
/**
|
||||
* Class for making Matrix API calls.
|
||||
*/
|
||||
public class RestClient<T> {
|
||||
private static final String LOG_TAG = RestClient.class.getSimpleName();
|
||||
|
||||
public static final String URI_API_PREFIX_PATH_MEDIA_R0 = "_matrix/media/r0/";
|
||||
public static final String URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE = "_matrix/media_proxy/unstable/";
|
||||
public static final String URI_API_PREFIX_PATH = "_matrix/client/";
|
||||
public static final String URI_API_PREFIX_PATH_R0 = "_matrix/client/r0/";
|
||||
public static final String URI_API_PREFIX_PATH_UNSTABLE = "_matrix/client/unstable/";
|
||||
|
||||
/**
|
||||
* Prefix used in path of identity server API requests.
|
||||
*/
|
||||
public static final String URI_API_PREFIX_IDENTITY = "_matrix/identity/api/v1/";
|
||||
|
||||
/**
|
||||
* List the servers which should be used to define the base url.
|
||||
*/
|
||||
public enum EndPointServer {
|
||||
HOME_SERVER,
|
||||
IDENTITY_SERVER,
|
||||
ANTIVIRUS_SERVER
|
||||
}
|
||||
|
||||
protected static final int CONNECTION_TIMEOUT_MS = 30000;
|
||||
private static final int READ_TIMEOUT_MS = 60000;
|
||||
private static final int WRITE_TIMEOUT_MS = 60000;
|
||||
|
||||
protected Credentials mCredentials;
|
||||
|
||||
protected T mApi;
|
||||
|
||||
protected Gson gson;
|
||||
|
||||
protected UnsentEventsManager mUnsentEventsManager;
|
||||
|
||||
protected HomeServerConnectionConfig mHsConfig;
|
||||
|
||||
// unitary tests only
|
||||
public static boolean mUseMXExecutor = false;
|
||||
|
||||
// the user agent
|
||||
private static String sUserAgent = null;
|
||||
|
||||
// http client
|
||||
private OkHttpClient mOkHttpClient = new OkHttpClient();
|
||||
|
||||
public RestClient(HomeServerConnectionConfig hsConfig, Class<T> type, String uriPrefix, boolean withNullSerialization) {
|
||||
this(hsConfig, type, uriPrefix, withNullSerialization, EndPointServer.HOME_SERVER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public constructor.
|
||||
*
|
||||
* @param hsConfig the home server configuration.
|
||||
* @param type the REST type
|
||||
* @param uriPrefix the URL request prefix
|
||||
* @param withNullSerialization true to serialise class member with null value
|
||||
* @param useIdentityServer true to use the identity server URL as base request
|
||||
*/
|
||||
public RestClient(HomeServerConnectionConfig hsConfig, Class<T> type, String uriPrefix, boolean withNullSerialization, boolean useIdentityServer) {
|
||||
this(hsConfig, type, uriPrefix, withNullSerialization, useIdentityServer ? EndPointServer.IDENTITY_SERVER : EndPointServer.HOME_SERVER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public constructor.
|
||||
*
|
||||
* @param hsConfig the home server configuration.
|
||||
* @param type the REST type
|
||||
* @param uriPrefix the URL request prefix
|
||||
* @param withNullSerialization true to serialise class member with null value
|
||||
* @param endPointServer tell which server is used to define the base url
|
||||
*/
|
||||
public RestClient(HomeServerConnectionConfig hsConfig, Class<T> type, String uriPrefix, boolean withNullSerialization, EndPointServer endPointServer) {
|
||||
// The JSON -> object mapper
|
||||
gson = JsonUtils.getGson(withNullSerialization);
|
||||
|
||||
mHsConfig = hsConfig;
|
||||
mCredentials = hsConfig.getCredentials();
|
||||
|
||||
Interceptor authentInterceptor = new Interceptor() {
|
||||
|
||||
@Override
|
||||
public Response intercept(Chain chain) throws IOException {
|
||||
Request request = chain.request();
|
||||
Request.Builder newRequestBuilder = request.newBuilder();
|
||||
if (null != sUserAgent) {
|
||||
// set a custom user agent
|
||||
newRequestBuilder.addHeader("User-Agent", sUserAgent);
|
||||
}
|
||||
|
||||
// Add the access token to all requests if it is set
|
||||
if ((mCredentials != null) && (mCredentials.accessToken != null)) {
|
||||
newRequestBuilder.addHeader("Authorization", "Bearer " + mCredentials.accessToken);
|
||||
}
|
||||
|
||||
request = newRequestBuilder.build();
|
||||
|
||||
return chain.proceed(request);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO Remove this, seems so useless
|
||||
Interceptor connectivityInterceptor = new Interceptor() {
|
||||
@Override
|
||||
public Response intercept(Chain chain) throws IOException {
|
||||
if (mUnsentEventsManager != null
|
||||
&& mUnsentEventsManager.getNetworkConnectivityReceiver() != null
|
||||
&& !mUnsentEventsManager.getNetworkConnectivityReceiver().isConnected()) {
|
||||
throw new IOException("Not connected");
|
||||
}
|
||||
return chain.proceed(chain.request());
|
||||
}
|
||||
};
|
||||
|
||||
OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient().newBuilder()
|
||||
.connectTimeout(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(READ_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
||||
.writeTimeout(WRITE_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
||||
.addInterceptor(authentInterceptor)
|
||||
.addInterceptor(connectivityInterceptor);
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
|
||||
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BASIC);
|
||||
okHttpClientBuilder
|
||||
.addInterceptor(loggingInterceptor);
|
||||
}
|
||||
|
||||
|
||||
if (mUseMXExecutor) {
|
||||
okHttpClientBuilder.dispatcher(new Dispatcher(new MXRestExecutorService()));
|
||||
}
|
||||
|
||||
try {
|
||||
Pair<SSLSocketFactory, X509TrustManager> pair = CertUtil.newPinnedSSLSocketFactory(hsConfig);
|
||||
okHttpClientBuilder.sslSocketFactory(pair.first, pair.second);
|
||||
okHttpClientBuilder.hostnameVerifier(CertUtil.newHostnameVerifier(hsConfig));
|
||||
okHttpClientBuilder.connectionSpecs(CertUtil.newConnectionSpecs(hsConfig));
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## RestClient() setSslSocketFactory failed" + e.getMessage(), e);
|
||||
}
|
||||
|
||||
mOkHttpClient = okHttpClientBuilder.build();
|
||||
final String endPoint = makeEndpoint(hsConfig, uriPrefix, endPointServer);
|
||||
|
||||
// Rest adapter for turning API interfaces into actual REST-calling objects
|
||||
Retrofit.Builder builder = new Retrofit.Builder()
|
||||
.baseUrl(endPoint)
|
||||
.addConverterFactory(PolymorphicRequestBodyConverter.FACTORY)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.client(mOkHttpClient);
|
||||
|
||||
Retrofit retrofit = builder.build();
|
||||
|
||||
mApi = retrofit.create(type);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private String makeEndpoint(HomeServerConnectionConfig hsConfig, String uriPrefix, EndPointServer endPointServer) {
|
||||
String baseUrl;
|
||||
switch (endPointServer) {
|
||||
case IDENTITY_SERVER:
|
||||
baseUrl = hsConfig.getIdentityServerUri().toString();
|
||||
break;
|
||||
case ANTIVIRUS_SERVER:
|
||||
baseUrl = hsConfig.getAntiVirusServerUri().toString();
|
||||
break;
|
||||
case HOME_SERVER:
|
||||
default:
|
||||
baseUrl = hsConfig.getHomeserverUri().toString();
|
||||
|
||||
}
|
||||
baseUrl = sanitizeBaseUrl(baseUrl);
|
||||
String dynamicPath = sanitizeDynamicPath(uriPrefix);
|
||||
return baseUrl + dynamicPath;
|
||||
}
|
||||
|
||||
private String sanitizeBaseUrl(String baseUrl) {
|
||||
if (baseUrl.endsWith("/")) {
|
||||
return baseUrl;
|
||||
}
|
||||
return baseUrl + "/";
|
||||
}
|
||||
|
||||
private String sanitizeDynamicPath(String dynamicPath) {
|
||||
// remove any trailing http in the uri prefix
|
||||
if (dynamicPath.startsWith("http://")) {
|
||||
dynamicPath = dynamicPath.substring("http://".length());
|
||||
} else if (dynamicPath.startsWith("https://")) {
|
||||
dynamicPath = dynamicPath.substring("https://".length());
|
||||
}
|
||||
return dynamicPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an user agent with the application version.
|
||||
* Ex: Riot/0.8.12 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour FDroid; MatrixAndroidSDK 0.9.6)
|
||||
*
|
||||
* @param appContext the application context
|
||||
*/
|
||||
public static void initUserAgent(Context appContext) {
|
||||
String appName = "";
|
||||
String appVersion = "";
|
||||
|
||||
if (null != appContext) {
|
||||
try {
|
||||
PackageManager pm = appContext.getPackageManager();
|
||||
ApplicationInfo appInfo = pm.getApplicationInfo(appContext.getApplicationContext().getPackageName(), 0);
|
||||
appName = pm.getApplicationLabel(appInfo).toString();
|
||||
|
||||
PackageInfo pkgInfo = pm.getPackageInfo(appContext.getApplicationContext().getPackageName(), 0);
|
||||
appVersion = pkgInfo.versionName;
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## initUserAgent() : failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
sUserAgent = System.getProperty("http.agent");
|
||||
|
||||
// cannot retrieve the application version
|
||||
if (TextUtils.isEmpty(appName) || TextUtils.isEmpty(appVersion)) {
|
||||
if (null == sUserAgent) {
|
||||
sUserAgent = "Java" + System.getProperty("java.version");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// if there is no user agent or cannot parse it
|
||||
if ((null == sUserAgent) || (sUserAgent.lastIndexOf(")") == -1) || (sUserAgent.indexOf("(") == -1)) {
|
||||
sUserAgent = appName + "/" + appVersion + "; MatrixAndroidSDK " + BuildConfig.VERSION_NAME + ")";
|
||||
} else {
|
||||
// update
|
||||
sUserAgent = appName + "/" + appVersion + " " +
|
||||
sUserAgent.substring(sUserAgent.indexOf("("), sUserAgent.lastIndexOf(")") - 1) +
|
||||
"; MatrixAndroidSDK " + BuildConfig.VERSION_NAME + ")";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user agent
|
||||
*
|
||||
* @return the current user agent, or null in case of error or if not initialized yet
|
||||
*/
|
||||
@Nullable
|
||||
public static String getUserAgent() {
|
||||
return sUserAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the connection timeouts.
|
||||
*
|
||||
* @param networkConnectivityReceiver the network connectivity receiver
|
||||
*/
|
||||
private void refreshConnectionTimeout(NetworkConnectivityReceiver networkConnectivityReceiver) {
|
||||
OkHttpClient.Builder builder = mOkHttpClient.newBuilder();
|
||||
|
||||
if (networkConnectivityReceiver.isConnected()) {
|
||||
float factor = networkConnectivityReceiver.getTimeoutScale();
|
||||
|
||||
builder
|
||||
.connectTimeout((int) (CONNECTION_TIMEOUT_MS * factor), TimeUnit.MILLISECONDS)
|
||||
.readTimeout((int) (READ_TIMEOUT_MS * factor), TimeUnit.MILLISECONDS)
|
||||
.writeTimeout((int) (WRITE_TIMEOUT_MS * factor), TimeUnit.MILLISECONDS);
|
||||
|
||||
Log.d(LOG_TAG, "## refreshConnectionTimeout() : update setConnectTimeout to " + (CONNECTION_TIMEOUT_MS * factor) + " ms");
|
||||
Log.d(LOG_TAG, "## refreshConnectionTimeout() : update setReadTimeout to " + (READ_TIMEOUT_MS * factor) + " ms");
|
||||
Log.d(LOG_TAG, "## refreshConnectionTimeout() : update setWriteTimeout to " + (WRITE_TIMEOUT_MS * factor) + " ms");
|
||||
} else {
|
||||
builder.connectTimeout(1, TimeUnit.MILLISECONDS);
|
||||
Log.d(LOG_TAG, "## refreshConnectionTimeout() : update the requests timeout to 1 ms");
|
||||
}
|
||||
|
||||
// FIXME It has no effect to the rest client
|
||||
mOkHttpClient = builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the connection timeout
|
||||
*
|
||||
* @param aTimeoutMs the connection timeout
|
||||
*/
|
||||
protected void setConnectionTimeout(int aTimeoutMs) {
|
||||
int timeoutMs = aTimeoutMs;
|
||||
|
||||
if (null != mUnsentEventsManager) {
|
||||
NetworkConnectivityReceiver networkConnectivityReceiver = mUnsentEventsManager.getNetworkConnectivityReceiver();
|
||||
|
||||
if (null != networkConnectivityReceiver) {
|
||||
if (networkConnectivityReceiver.isConnected()) {
|
||||
timeoutMs *= networkConnectivityReceiver.getTimeoutScale();
|
||||
} else {
|
||||
timeoutMs = 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (timeoutMs != mOkHttpClient.connectTimeoutMillis()) {
|
||||
// FIXME It has no effect to the rest client
|
||||
mOkHttpClient = mOkHttpClient.newBuilder().connectTimeout(timeoutMs, TimeUnit.MILLISECONDS).build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the unsentEvents manager.
|
||||
*
|
||||
* @param unsentEventsManager The unsentEvents manager.
|
||||
*/
|
||||
public void setUnsentEventsManager(UnsentEventsManager unsentEventsManager) {
|
||||
mUnsentEventsManager = unsentEventsManager;
|
||||
|
||||
final NetworkConnectivityReceiver networkConnectivityReceiver = mUnsentEventsManager.getNetworkConnectivityReceiver();
|
||||
refreshConnectionTimeout(networkConnectivityReceiver);
|
||||
|
||||
networkConnectivityReceiver.addEventListener(new IMXNetworkEventListener() {
|
||||
@Override
|
||||
public void onNetworkConnectionUpdate(boolean isConnected) {
|
||||
Log.d(LOG_TAG, "## setUnsentEventsManager() : update the requests timeout to " + (isConnected ? CONNECTION_TIMEOUT_MS : 1) + " ms");
|
||||
refreshConnectionTimeout(networkConnectivityReceiver);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's credentials. Typically for saving them somewhere persistent.
|
||||
*
|
||||
* @return the user credentials
|
||||
*/
|
||||
public Credentials getCredentials() {
|
||||
return mCredentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the user's credentials. To be called after login or registration.
|
||||
*
|
||||
* @param credentials the user credentials
|
||||
*/
|
||||
public void setCredentials(Credentials credentials) {
|
||||
mCredentials = credentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default protected constructor for unit tests.
|
||||
*/
|
||||
protected RestClient() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Protected setter for injection by unit tests.
|
||||
*
|
||||
* @param api the api object
|
||||
*/
|
||||
@VisibleForTesting()
|
||||
protected void setApi(T api) {
|
||||
mApi = api;
|
||||
}
|
||||
}
|
@ -0,0 +1,797 @@
|
||||
/*
|
||||
* Copyright 2014 OpenMarket Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.call;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaPlayer;
|
||||
import android.media.Ringtone;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.os.Vibrator;
|
||||
import android.provider.MediaStore;
|
||||
|
||||
import im.vector.matrix.android.R;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* This class manages the call sound.
|
||||
* It is in charge of playing ring tones and managing the audio focus.
|
||||
*/
|
||||
public class CallSoundsManager {
|
||||
private static final String LOG_TAG = CallSoundsManager.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Track the audio focus update.
|
||||
*/
|
||||
public interface OnAudioFocusListener {
|
||||
/**
|
||||
* Call back indicating new focus events (ex: {@link AudioManager#AUDIOFOCUS_GAIN},
|
||||
* {@link AudioManager#AUDIOFOCUS_LOSS}..).
|
||||
*
|
||||
* @param aFocusEvent the focus event (see {@link AudioManager.OnAudioFocusChangeListener})
|
||||
*/
|
||||
void onFocusChanged(int aFocusEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track the audio configuration change (like speaker, micro and so on).
|
||||
*/
|
||||
public interface OnAudioConfigurationUpdateListener {
|
||||
void onAudioConfigurationUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Track the media statuses.
|
||||
*/
|
||||
public interface OnMediaListener {
|
||||
|
||||
/**
|
||||
* The media is ready to be played
|
||||
*/
|
||||
void onMediaReadyToPlay();
|
||||
|
||||
/**
|
||||
* The media is playing.
|
||||
*/
|
||||
void onMediaPlay();
|
||||
|
||||
/**
|
||||
* The media has been played
|
||||
*/
|
||||
void onMediaCompleted();
|
||||
}
|
||||
|
||||
private static CallSoundsManager mSharedInstance = null;
|
||||
private final Context mContext;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param context the context
|
||||
*/
|
||||
private CallSoundsManager(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the shared instance.
|
||||
*
|
||||
* @param context the context
|
||||
* @return the shared instance
|
||||
*/
|
||||
public static CallSoundsManager getSharedInstance(Context context) {
|
||||
if (null == mSharedInstance) {
|
||||
mSharedInstance = new CallSoundsManager(context.getApplicationContext());
|
||||
}
|
||||
|
||||
return mSharedInstance;
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// Audio configuration management
|
||||
//==============================================================================================================
|
||||
// audio focus management
|
||||
private final Set<OnAudioConfigurationUpdateListener> mOnAudioConfigurationUpdateListener = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Add an audio configuration update listener.
|
||||
*
|
||||
* @param listener the listener.
|
||||
*/
|
||||
public void addAudioConfigurationListener(OnAudioConfigurationUpdateListener listener) {
|
||||
synchronized (LOG_TAG) {
|
||||
mOnAudioConfigurationUpdateListener.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an audio configuration update listener.
|
||||
*
|
||||
* @param listener the listener.
|
||||
*/
|
||||
public void removeAudioConfigurationListener(OnAudioConfigurationUpdateListener listener) {
|
||||
synchronized (LOG_TAG) {
|
||||
mOnAudioConfigurationUpdateListener.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch that the audio configuration has been updated.
|
||||
*/
|
||||
private void dispatchAudioConfigurationUpdate() {
|
||||
synchronized (LOG_TAG) {
|
||||
// notify listeners
|
||||
for (OnAudioConfigurationUpdateListener listener : mOnAudioConfigurationUpdateListener) {
|
||||
try {
|
||||
listener.onAudioConfigurationUpdate();
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## dispatchAudioConfigurationUpdate() failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// Focus management
|
||||
//==============================================================================================================
|
||||
|
||||
// audio focus management
|
||||
private final Set<OnAudioFocusListener> mAudioFocusListeners = new HashSet<>();
|
||||
|
||||
private final AudioManager.OnAudioFocusChangeListener mFocusListener = new AudioManager.OnAudioFocusChangeListener() {
|
||||
@Override
|
||||
public void onAudioFocusChange(int aFocusEvent) {
|
||||
switch (aFocusEvent) {
|
||||
case AudioManager.AUDIOFOCUS_GAIN:
|
||||
Log.d(LOG_TAG, "## OnAudioFocusChangeListener(): AUDIOFOCUS_GAIN");
|
||||
// TODO resume voip call (ex: ending GSM call)
|
||||
break;
|
||||
|
||||
case AudioManager.AUDIOFOCUS_LOSS:
|
||||
Log.d(LOG_TAG, "## OnAudioFocusChangeListener(): AUDIOFOCUS_LOSS");
|
||||
// TODO pause voip call (ex: incoming GSM call)
|
||||
break;
|
||||
|
||||
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
|
||||
Log.d(LOG_TAG, "## OnAudioFocusChangeListener(): AUDIOFOCUS_GAIN_TRANSIENT");
|
||||
break;
|
||||
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
|
||||
Log.d(LOG_TAG, "## OnAudioFocusChangeListener(): AUDIOFOCUS_LOSS_TRANSIENT");
|
||||
// TODO pause voip call (ex: incoming GSM call)
|
||||
break;
|
||||
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
|
||||
// TODO : continue playing at an attenuated level
|
||||
Log.d(LOG_TAG, "## OnAudioFocusChangeListener(): AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK");
|
||||
break;
|
||||
|
||||
case AudioManager.AUDIOFOCUS_REQUEST_FAILED:
|
||||
Log.d(LOG_TAG, "## OnAudioFocusChangeListener(): AUDIOFOCUS_REQUEST_FAILED");
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
synchronized (LOG_TAG) {
|
||||
// notify listeners
|
||||
for (OnAudioFocusListener listener : mAudioFocusListeners) {
|
||||
try {
|
||||
listener.onFocusChanged(aFocusEvent);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## onFocusChanged() failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a focus listener.
|
||||
*
|
||||
* @param focusListener the listener.
|
||||
*/
|
||||
public void addFocusListener(OnAudioFocusListener focusListener) {
|
||||
synchronized (LOG_TAG) {
|
||||
mAudioFocusListeners.add(focusListener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a focus listener.
|
||||
*
|
||||
* @param focusListener the listener.
|
||||
*/
|
||||
public void removeFocusListener(OnAudioFocusListener focusListener) {
|
||||
synchronized (LOG_TAG) {
|
||||
mAudioFocusListeners.remove(focusListener);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// Ringtone management management
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* @return the audio manager
|
||||
*/
|
||||
private AudioManager getAudioManager() {
|
||||
if (null == mAudioManager) {
|
||||
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
}
|
||||
|
||||
return mAudioManager;
|
||||
}
|
||||
|
||||
// audio focus
|
||||
private boolean mIsFocusGranted = false;
|
||||
|
||||
private static final int VIBRATE_DURATION = 500; // milliseconds
|
||||
private static final int VIBRATE_SLEEP = 1000; // milliseconds
|
||||
private static final long[] VIBRATE_PATTERN = {0, VIBRATE_DURATION, VIBRATE_SLEEP};
|
||||
|
||||
private Ringtone mRingTone;
|
||||
private boolean mIsRinging;
|
||||
private MediaPlayer mMediaPlayer = null;
|
||||
|
||||
// the audio manager (do not use directly, use getAudioManager())
|
||||
private AudioManager mAudioManager = null;
|
||||
|
||||
// the playing sound
|
||||
private int mPlayingSound = -1;
|
||||
|
||||
/**
|
||||
* Tells that the device is ringing.
|
||||
*
|
||||
* @return true if the device is ringing
|
||||
*/
|
||||
public boolean isRinging() {
|
||||
return mIsRinging;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter method.
|
||||
*
|
||||
* @return true is focus is granted, false otherwise.
|
||||
*/
|
||||
public boolean isFocusGranted() {
|
||||
return mIsFocusGranted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop any playing sound.
|
||||
*/
|
||||
public void stopSounds() {
|
||||
mIsRinging = false;
|
||||
|
||||
if (null != mRingTone) {
|
||||
mRingTone.stop();
|
||||
mRingTone = null;
|
||||
}
|
||||
|
||||
if (null != mMediaPlayer) {
|
||||
if (mMediaPlayer.isPlaying()) {
|
||||
mMediaPlayer.stop();
|
||||
}
|
||||
|
||||
mMediaPlayer.release();
|
||||
mMediaPlayer = null;
|
||||
}
|
||||
|
||||
mPlayingSound = -1;
|
||||
|
||||
// stop vibrate
|
||||
enableVibrating(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the ringing sound
|
||||
*/
|
||||
public void stopRinging() {
|
||||
Log.d(LOG_TAG, "stopRinging");
|
||||
stopSounds();
|
||||
|
||||
// stop vibrate
|
||||
enableVibrating(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a permanent audio focus if the focus was not yet granted.
|
||||
*/
|
||||
public void requestAudioFocus() {
|
||||
if (!mIsFocusGranted) {
|
||||
int focusResult;
|
||||
AudioManager audioMgr;
|
||||
|
||||
if ((null != (audioMgr = getAudioManager()))) {
|
||||
// Request permanent audio focus for voice call
|
||||
focusResult = audioMgr.requestAudioFocus(mFocusListener, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN);
|
||||
|
||||
if (AudioManager.AUDIOFOCUS_REQUEST_GRANTED == focusResult) {
|
||||
mIsFocusGranted = true;
|
||||
Log.d(LOG_TAG, "## getAudioFocus(): granted");
|
||||
} else {
|
||||
mIsFocusGranted = false;
|
||||
Log.w(LOG_TAG, "## getAudioFocus(): refused - focusResult=" + focusResult);
|
||||
}
|
||||
}
|
||||
|
||||
dispatchAudioConfigurationUpdate();
|
||||
} else {
|
||||
Log.d(LOG_TAG, "## getAudioFocus(): already granted");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the audio focus if it was granted.
|
||||
*/
|
||||
public void releaseAudioFocus() {
|
||||
if (mIsFocusGranted) {
|
||||
AudioManager audioManager = getAudioManager();
|
||||
|
||||
if ((null != audioManager)) {
|
||||
// release focus
|
||||
int abandonResult = audioManager.abandonAudioFocus(mFocusListener);
|
||||
|
||||
if (AudioManager.AUDIOFOCUS_REQUEST_GRANTED == abandonResult) {
|
||||
Log.d(LOG_TAG, "## releaseAudioFocus(): abandonAudioFocus = AUDIOFOCUS_REQUEST_GRANTED");
|
||||
}
|
||||
|
||||
if (AudioManager.AUDIOFOCUS_REQUEST_FAILED == abandonResult) {
|
||||
Log.d(LOG_TAG, "## releaseAudioFocus(): abandonAudioFocus = AUDIOFOCUS_REQUEST_FAILED");
|
||||
}
|
||||
} else {
|
||||
Log.d(LOG_TAG, "## releaseAudioFocus(): failure - invalid AudioManager");
|
||||
}
|
||||
|
||||
mIsFocusGranted = false;
|
||||
}
|
||||
|
||||
restoreAudioConfig();
|
||||
dispatchAudioConfigurationUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the ringing sound.
|
||||
*
|
||||
* @param resId the ring sound id
|
||||
* @param filename the filename to save the ringtone
|
||||
*/
|
||||
public void startRinging(int resId, String filename) {
|
||||
Log.v(LOG_TAG, "startRinging");
|
||||
if (mRingTone != null) {
|
||||
Log.v(LOG_TAG, "ring tone already ringing");
|
||||
}
|
||||
// stop any playing ringtone
|
||||
stopSounds();
|
||||
mIsRinging = true;
|
||||
// use the ringTone to manage sound volume properly
|
||||
mRingTone = getRingTone(mContext, resId, filename, RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE));
|
||||
if (mRingTone != null) {
|
||||
setSpeakerphoneOn(false, true);
|
||||
mRingTone.play();
|
||||
} else {
|
||||
Log.e(LOG_TAG, "startRinging : fail to retrieve RING_TONE_START_RINGING");
|
||||
}
|
||||
// start vibrate
|
||||
enableVibrating(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same than {@link #startRinging(int, String)}}, but do not play sound, nor vibrate.
|
||||
*/
|
||||
public void startRingingSilently() {
|
||||
mIsRinging = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the vibrate mode.
|
||||
*
|
||||
* @param aIsVibrateEnabled true to force vibrate, false to stop vibrate.
|
||||
*/
|
||||
private void enableVibrating(boolean aIsVibrateEnabled) {
|
||||
Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
|
||||
|
||||
if ((null != vibrator) && vibrator.hasVibrator()) {
|
||||
if (aIsVibrateEnabled) {
|
||||
vibrator.vibrate(VIBRATE_PATTERN, 0 /*repeat till stop*/);
|
||||
Log.d(LOG_TAG, "## startVibrating(): Vibrate started");
|
||||
} else {
|
||||
vibrator.cancel();
|
||||
Log.d(LOG_TAG, "## startVibrating(): Vibrate canceled");
|
||||
}
|
||||
} else {
|
||||
Log.w(LOG_TAG, "## startVibrating(): vibrator access failed");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a sound.
|
||||
*
|
||||
* @param resId the sound resource id
|
||||
* @param isLooping true to loop
|
||||
* @param listener the listener
|
||||
*/
|
||||
public void startSound(int resId, boolean isLooping, final OnMediaListener listener) {
|
||||
Log.d(LOG_TAG, "startSound");
|
||||
|
||||
if (mPlayingSound == resId) {
|
||||
Log.d(LOG_TAG, "## startSound() : already playing " + resId);
|
||||
return;
|
||||
}
|
||||
|
||||
stopSounds();
|
||||
mPlayingSound = resId;
|
||||
|
||||
mMediaPlayer = MediaPlayer.create(mContext, resId);
|
||||
|
||||
if (null != mMediaPlayer) {
|
||||
mMediaPlayer.setLooping(isLooping);
|
||||
|
||||
if (null != listener) {
|
||||
listener.onMediaReadyToPlay();
|
||||
}
|
||||
|
||||
mMediaPlayer.start();
|
||||
|
||||
if (null != listener) {
|
||||
listener.onMediaPlay();
|
||||
}
|
||||
|
||||
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
|
||||
@Override
|
||||
public void onCompletion(MediaPlayer mp) {
|
||||
if (null != listener) {
|
||||
listener.onMediaCompleted();
|
||||
}
|
||||
mPlayingSound = -1;
|
||||
|
||||
if (null != mMediaPlayer) {
|
||||
mMediaPlayer.release();
|
||||
mMediaPlayer = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
Log.e(LOG_TAG, "startSound : failed");
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// resid / filenime to ringtone
|
||||
//==============================================================================================================
|
||||
|
||||
private static final Map<String, Uri> mRingtoneUrlByFileName = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Provide a ringtone uri from a resource and a filename.
|
||||
*
|
||||
* @param context the context
|
||||
* @param resId The audio resource.
|
||||
* @param filename the audio filename
|
||||
* @return the ringtone uri
|
||||
*/
|
||||
private static Uri getRingToneUri(Context context, int resId, String filename) {
|
||||
Uri ringToneUri = mRingtoneUrlByFileName.get(filename);
|
||||
// test if the ring tone has been cached
|
||||
|
||||
if (null != ringToneUri) {
|
||||
// check if the file exists
|
||||
try {
|
||||
File ringFile = new File(ringToneUri.toString());
|
||||
|
||||
// check if the file exists
|
||||
if ((null != ringFile) && ringFile.exists() && ringFile.canRead()) {
|
||||
// provide it
|
||||
return ringToneUri;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## getRingToneUri() failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
File directory = new File(Environment.getExternalStorageDirectory(), "/" + context.getApplicationContext().getPackageName().hashCode() + "/Audio/");
|
||||
|
||||
// create the directory if it does not exist
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs();
|
||||
}
|
||||
|
||||
File file = new File(directory + "/", filename);
|
||||
|
||||
// if the file exists, check if the resource has been created
|
||||
if (file.exists()) {
|
||||
Cursor cursor = context.getContentResolver().query(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||
new String[]{MediaStore.Audio.Media._ID},
|
||||
MediaStore.Audio.Media.DATA + "=? ",
|
||||
new String[]{file.getAbsolutePath()}, null);
|
||||
|
||||
if ((null != cursor) && cursor.moveToFirst()) {
|
||||
int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
|
||||
ringToneUri = Uri.withAppendedPath(Uri.parse("content://media/external/audio/media"), "" + id);
|
||||
}
|
||||
|
||||
if (null != cursor) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
// the Uri has been retrieved
|
||||
if (null == ringToneUri) {
|
||||
// create the file
|
||||
if (!file.exists()) {
|
||||
try {
|
||||
byte[] readData = new byte[1024];
|
||||
InputStream fis = context.getResources().openRawResource(resId);
|
||||
FileOutputStream fos = new FileOutputStream(file);
|
||||
int i = fis.read(readData);
|
||||
|
||||
while (i != -1) {
|
||||
fos.write(readData, 0, i);
|
||||
i = fis.read(readData);
|
||||
}
|
||||
|
||||
fos.close();
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## getRingToneUri(): Exception1 Msg=" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// and the resource Uri
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(MediaStore.MediaColumns.DATA, file.getAbsolutePath());
|
||||
values.put(MediaStore.MediaColumns.TITLE, filename);
|
||||
values.put(MediaStore.MediaColumns.MIME_TYPE, "audio/ogg");
|
||||
values.put(MediaStore.MediaColumns.SIZE, file.length());
|
||||
values.put(MediaStore.Audio.Media.ARTIST, R.string.app_name);
|
||||
values.put(MediaStore.Audio.Media.IS_RINGTONE, true);
|
||||
values.put(MediaStore.Audio.Media.IS_NOTIFICATION, true);
|
||||
values.put(MediaStore.Audio.Media.IS_ALARM, true);
|
||||
values.put(MediaStore.Audio.Media.IS_MUSIC, true);
|
||||
|
||||
ringToneUri = context.getContentResolver().insert(MediaStore.Audio.Media.getContentUriForPath(file.getAbsolutePath()), values);
|
||||
}
|
||||
|
||||
if (null != ringToneUri) {
|
||||
mRingtoneUrlByFileName.put(filename, ringToneUri);
|
||||
return ringToneUri;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## getRingToneUri(): Exception2 Msg=" + e.getLocalizedMessage(), e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a ringtone from an uri
|
||||
*
|
||||
* @param context the context
|
||||
* @param ringToneUri the ringtone URI
|
||||
* @return the ringtone
|
||||
*/
|
||||
private static Ringtone uriToRingTone(Context context, Uri ringToneUri) {
|
||||
if (null != ringToneUri) {
|
||||
try {
|
||||
return RingtoneManager.getRingtone(context, ringToneUri);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## uriToRingTone() failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a ringtone from a resource and a filename.
|
||||
* The audio file must have a ANDROID_LOOP metatada set to true to loop the sound.
|
||||
*
|
||||
* @param context the context
|
||||
* @param resId The audio resource.
|
||||
* @param filename the audio filename
|
||||
* @param defaultRingToneUri the default ring tone
|
||||
* @return a RingTone, null if the operation fails.
|
||||
*/
|
||||
private static Ringtone getRingTone(Context context, int resId, String filename, Uri defaultRingToneUri) {
|
||||
Ringtone ringtone = uriToRingTone(context, getRingToneUri(context, resId, filename));
|
||||
|
||||
if (null == ringtone) {
|
||||
ringtone = uriToRingTone(context, defaultRingToneUri);
|
||||
}
|
||||
|
||||
Log.d(LOG_TAG, "getRingTone() : resId " + resId + " filename " + filename + " defaultRingToneUri " + defaultRingToneUri + " returns " + ringtone);
|
||||
|
||||
return ringtone;
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// speakers management
|
||||
//==============================================================================================================
|
||||
|
||||
// save the audio statuses
|
||||
private Integer mAudioMode = null;
|
||||
private Boolean mIsSpeakerphoneOn = null;
|
||||
|
||||
/**
|
||||
* Back up the current audio config.
|
||||
*/
|
||||
private void backupAudioConfig() {
|
||||
if (null == mAudioMode) {
|
||||
AudioManager audioManager = getAudioManager();
|
||||
|
||||
mAudioMode = audioManager.getMode();
|
||||
mIsSpeakerphoneOn = audioManager.isSpeakerphoneOn();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the audio config.
|
||||
*/
|
||||
private void restoreAudioConfig() {
|
||||
// ensure that something has been saved
|
||||
if ((null != mAudioMode) && (null != mIsSpeakerphoneOn)) {
|
||||
Log.d(LOG_TAG, "## restoreAudioConfig() starts");
|
||||
AudioManager audioManager = getAudioManager();
|
||||
|
||||
if (mAudioMode != audioManager.getMode()) {
|
||||
Log.d(LOG_TAG, "## restoreAudioConfig() : restore audio mode " + mAudioMode);
|
||||
audioManager.setMode(mAudioMode);
|
||||
}
|
||||
|
||||
if (mIsSpeakerphoneOn != audioManager.isSpeakerphoneOn()) {
|
||||
Log.d(LOG_TAG, "## restoreAudioConfig() : restore speaker " + mIsSpeakerphoneOn);
|
||||
audioManager.setSpeakerphoneOn(mIsSpeakerphoneOn);
|
||||
}
|
||||
|
||||
// stop the bluetooth
|
||||
if (audioManager.isBluetoothScoOn()) {
|
||||
Log.d(LOG_TAG, "## restoreAudioConfig() : ends the bluetooth calls");
|
||||
audioManager.stopBluetoothSco();
|
||||
audioManager.setBluetoothScoOn(false);
|
||||
}
|
||||
|
||||
mAudioMode = null;
|
||||
mIsSpeakerphoneOn = null;
|
||||
|
||||
Log.d(LOG_TAG, "## restoreAudioConfig() done");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the speakerphone ON or OFF.
|
||||
*
|
||||
* @param isOn true to enable the speaker (ON), false to disable it (OFF)
|
||||
*/
|
||||
public void setCallSpeakerphoneOn(boolean isOn) {
|
||||
setSpeakerphoneOn(true, isOn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current speaker status and the audio mode, before updating those
|
||||
* values.
|
||||
* The audio mode depends on if there is a call in progress.
|
||||
* If audio mode set to {@link AudioManager#MODE_IN_COMMUNICATION} and
|
||||
* a media player is in ON, the media player will reduce its audio level.
|
||||
*
|
||||
* @param isInCall true when the speaker is updated during call.
|
||||
* @param isSpeakerOn true to turn on the speaker (false to turn it off)
|
||||
*/
|
||||
public void setSpeakerphoneOn(boolean isInCall, boolean isSpeakerOn) {
|
||||
Log.d(LOG_TAG, "setCallSpeakerphoneOn " + isSpeakerOn);
|
||||
|
||||
backupAudioConfig();
|
||||
|
||||
try {
|
||||
AudioManager audioManager = getAudioManager();
|
||||
|
||||
int audioMode = isInCall ? AudioManager.MODE_IN_COMMUNICATION : AudioManager.MODE_RINGTONE;
|
||||
|
||||
if (audioManager.getMode() != audioMode) {
|
||||
audioManager.setMode(audioMode);
|
||||
}
|
||||
|
||||
if (!isSpeakerOn) {
|
||||
try {
|
||||
if (HeadsetConnectionReceiver.isBTHeadsetPlugged()) {
|
||||
audioManager.startBluetoothSco();
|
||||
audioManager.setBluetoothScoOn(true);
|
||||
} else if (audioManager.isBluetoothScoOn()) {
|
||||
audioManager.stopBluetoothSco();
|
||||
audioManager.setBluetoothScoOn(false);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## setSpeakerphoneOn() failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
if (isSpeakerOn != audioManager.isSpeakerphoneOn()) {
|
||||
audioManager.setSpeakerphoneOn(isSpeakerOn);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## setSpeakerphoneOn() failed " + e.getMessage(), e);
|
||||
restoreAudioConfig();
|
||||
}
|
||||
|
||||
dispatchAudioConfigurationUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the speaker
|
||||
*/
|
||||
public void toggleSpeaker() {
|
||||
AudioManager audioManager = getAudioManager();
|
||||
boolean isOn = !audioManager.isSpeakerphoneOn();
|
||||
audioManager.setSpeakerphoneOn(isOn);
|
||||
|
||||
if (!isOn) {
|
||||
try {
|
||||
if (HeadsetConnectionReceiver.isBTHeadsetPlugged()) {
|
||||
audioManager.startBluetoothSco();
|
||||
audioManager.setBluetoothScoOn(true);
|
||||
} else if (audioManager.isBluetoothScoOn()) {
|
||||
audioManager.stopBluetoothSco();
|
||||
audioManager.setBluetoothScoOn(false);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## toggleSpeaker() failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
dispatchAudioConfigurationUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the speaker is turned on.
|
||||
*/
|
||||
public boolean isSpeakerphoneOn() {
|
||||
return getAudioManager().isSpeakerphoneOn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mute the microphone.
|
||||
*
|
||||
* @param mute true to mute the microphone
|
||||
*/
|
||||
public void setMicrophoneMute(boolean mute) {
|
||||
getAudioManager().setMicrophoneMute(mute);
|
||||
dispatchAudioConfigurationUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the microphone is mute.
|
||||
*/
|
||||
public boolean isMicrophoneMute() {
|
||||
return getAudioManager().isMicrophoneMute();
|
||||
}
|
||||
}
|
@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.call;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothProfile;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
// this class detect if the headset is plugged / unplugged
|
||||
public class HeadsetConnectionReceiver extends BroadcastReceiver {
|
||||
|
||||
private static final String LOG_TAG = HeadsetConnectionReceiver.class.getSimpleName();
|
||||
|
||||
private static Boolean mIsHeadsetPlugged = null;
|
||||
|
||||
private static HeadsetConnectionReceiver mSharedInstance = null;
|
||||
|
||||
/**
|
||||
* Track the headset update.
|
||||
*/
|
||||
public interface OnHeadsetStatusUpdateListener {
|
||||
/**
|
||||
* A wire headset has been plugged / unplugged.
|
||||
*
|
||||
* @param isPlugged true if the headset is now plugged.
|
||||
*/
|
||||
void onWiredHeadsetUpdate(boolean isPlugged);
|
||||
|
||||
/**
|
||||
* A bluetooth headset is connected.
|
||||
*
|
||||
* @param isConnected true if the bluetooth headset is connected.
|
||||
*/
|
||||
void onBluetoothHeadsetUpdate(boolean isConnected);
|
||||
|
||||
}
|
||||
|
||||
// listeners
|
||||
private final Set<OnHeadsetStatusUpdateListener> mListeners = new HashSet<>();
|
||||
|
||||
public HeadsetConnectionReceiver() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context the application context
|
||||
* @return the shared instance
|
||||
*/
|
||||
public static HeadsetConnectionReceiver getSharedInstance(Context context) {
|
||||
if (null == mSharedInstance) {
|
||||
mSharedInstance = new HeadsetConnectionReceiver();
|
||||
context.registerReceiver(mSharedInstance, new IntentFilter(Intent.ACTION_HEADSET_PLUG));
|
||||
context.registerReceiver(mSharedInstance, new IntentFilter(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED));
|
||||
context.registerReceiver(mSharedInstance, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
|
||||
context.registerReceiver(mSharedInstance, new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED));
|
||||
context.registerReceiver(mSharedInstance, new IntentFilter(BluetoothDevice.ACTION_ACL_DISCONNECTED));
|
||||
}
|
||||
|
||||
return mSharedInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener.
|
||||
*
|
||||
* @param listener the listener to add.
|
||||
*/
|
||||
public void addListener(OnHeadsetStatusUpdateListener listener) {
|
||||
synchronized (LOG_TAG) {
|
||||
mListeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a listener.
|
||||
*
|
||||
* @param listener the listener to remove.
|
||||
*/
|
||||
public void removeListener(OnHeadsetStatusUpdateListener listener) {
|
||||
synchronized (LOG_TAG) {
|
||||
mListeners.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch onBluetoothHeadsetUpdate to the listeners.
|
||||
*
|
||||
* @param isConnected true if a bluetooth headset is connected.
|
||||
*/
|
||||
private void onBluetoothHeadsetUpdate(boolean isConnected) {
|
||||
synchronized (LOG_TAG) {
|
||||
for (OnHeadsetStatusUpdateListener listener : mListeners) {
|
||||
try {
|
||||
listener.onBluetoothHeadsetUpdate(isConnected);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## onBluetoothHeadsetUpdate()) failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch onWireHeadsetUpdate to the listeners.
|
||||
*
|
||||
* @param isPlugged true if the wire headset is plugged.
|
||||
*/
|
||||
private void onWiredHeadsetUpdate(boolean isPlugged) {
|
||||
synchronized (LOG_TAG) {
|
||||
for (OnHeadsetStatusUpdateListener listener : mListeners) {
|
||||
try {
|
||||
listener.onWiredHeadsetUpdate(isPlugged);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## onWiredHeadsetUpdate()) failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(final Context aContext, final Intent aIntent) {
|
||||
Log.d(LOG_TAG, "## onReceive() : " + aIntent.getExtras());
|
||||
String action = aIntent.getAction();
|
||||
|
||||
if (TextUtils.equals(action, Intent.ACTION_HEADSET_PLUG)
|
||||
|| TextUtils.equals(action, BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)
|
||||
|| TextUtils.equals(action, BluetoothAdapter.ACTION_STATE_CHANGED)
|
||||
|| TextUtils.equals(action, BluetoothDevice.ACTION_ACL_CONNECTED)
|
||||
|| TextUtils.equals(action, BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
|
||||
|
||||
Boolean newState = null;
|
||||
final boolean isBTHeadsetUpdate;
|
||||
|
||||
if (TextUtils.equals(action, Intent.ACTION_HEADSET_PLUG)) {
|
||||
int state = aIntent.getIntExtra("state", -1);
|
||||
|
||||
switch (state) {
|
||||
case 0:
|
||||
Log.d(LOG_TAG, "Headset is unplugged");
|
||||
newState = false;
|
||||
break;
|
||||
case 1:
|
||||
Log.d(LOG_TAG, "Headset is plugged");
|
||||
newState = true;
|
||||
break;
|
||||
default:
|
||||
Log.d(LOG_TAG, "undefined state");
|
||||
}
|
||||
isBTHeadsetUpdate = false;
|
||||
} else {
|
||||
int state = BluetoothAdapter.getDefaultAdapter().getProfileConnectionState(BluetoothProfile.HEADSET);
|
||||
|
||||
Log.d(LOG_TAG, "bluetooth headset state " + state);
|
||||
newState = (BluetoothAdapter.STATE_CONNECTED == state);
|
||||
isBTHeadsetUpdate = mIsHeadsetPlugged != newState;
|
||||
}
|
||||
|
||||
if (newState != mIsHeadsetPlugged) {
|
||||
mIsHeadsetPlugged = newState;
|
||||
|
||||
// wait a little else route to BT headset does not work.
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isBTHeadsetUpdate) {
|
||||
onBluetoothHeadsetUpdate(mIsHeadsetPlugged);
|
||||
} else {
|
||||
onWiredHeadsetUpdate(mIsHeadsetPlugged);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static AudioManager mAudioManager = null;
|
||||
|
||||
/**
|
||||
* @return the audio manager
|
||||
*/
|
||||
private static AudioManager getAudioManager(Context context) {
|
||||
if (null == mAudioManager) {
|
||||
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
}
|
||||
|
||||
return mAudioManager;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param context the context
|
||||
* @return true if the headset is plugged
|
||||
*/
|
||||
@SuppressLint("Deprecation")
|
||||
public static boolean isHeadsetPlugged(Context context) {
|
||||
if (null == mIsHeadsetPlugged) {
|
||||
AudioManager audioManager = getAudioManager(context);
|
||||
mIsHeadsetPlugged = isBTHeadsetPlugged() || audioManager.isWiredHeadsetOn();
|
||||
}
|
||||
|
||||
return mIsHeadsetPlugged;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if bluetooth headset is plugged
|
||||
*/
|
||||
public static boolean isBTHeadsetPlugged() {
|
||||
return (BluetoothAdapter.STATE_CONNECTED == BluetoothAdapter.getDefaultAdapter().getProfileConnectionState(BluetoothProfile.HEADSET));
|
||||
}
|
||||
}
|
@ -0,0 +1,337 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.call;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.MXSession;
|
||||
import im.vector.matrix.android.internal.legacy.data.Room;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
|
||||
/**
|
||||
* Audio/video call interface.
|
||||
* See {@link MXWebRtcCall} and {@link MXChromeCall}.
|
||||
*/
|
||||
public interface IMXCall {
|
||||
|
||||
// call ending use cases (see {@link #dispatchOnCallEnd}):
|
||||
int END_CALL_REASON_UNDEFINED = -1;
|
||||
/**
|
||||
* the callee has rejected the incoming call
|
||||
**/
|
||||
int END_CALL_REASON_PEER_HANG_UP = 0;
|
||||
/**
|
||||
* the callee has rejected the incoming call from another device
|
||||
**/
|
||||
int END_CALL_REASON_PEER_HANG_UP_ELSEWHERE = 1;
|
||||
/**
|
||||
* call ended by the local user himself
|
||||
**/
|
||||
int END_CALL_REASON_USER_HIMSELF = 2;
|
||||
|
||||
// call state events
|
||||
|
||||
// the call is an empty shell nothing has been initialized
|
||||
String CALL_STATE_CREATED = "IMXCall.CALL_STATE_CREATED";
|
||||
|
||||
// the call view is creating and being inserting.
|
||||
String CALL_STATE_CREATING_CALL_VIEW = "IMXCall.CALL_STATE_CREATING_CALL_VIEW";
|
||||
|
||||
// the call view is managed.
|
||||
// the call can start from now.
|
||||
String CALL_STATE_READY = "IMXCall.CALL_STATE_READY";
|
||||
|
||||
// incoming/outgoing calls : initializing the local audio / video
|
||||
String CALL_STATE_WAIT_LOCAL_MEDIA = "IMXCall.CALL_STATE_WAIT_LOCAL_MEDIA";
|
||||
|
||||
// incoming calls : the local media is retrieved
|
||||
String CALL_STATE_WAIT_CREATE_OFFER = "IMXCall.CALL_STATE_WAIT_CREATE_OFFER";
|
||||
|
||||
// outgoing calls : the call invitation is sent
|
||||
String CALL_STATE_INVITE_SENT = "IMXCall.CALL_STATE_INVITE_SENT";
|
||||
|
||||
// the device is ringing
|
||||
// incoming calls : after applying the incoming params
|
||||
// outgoing calls : after getting the m.call.invite echo
|
||||
String CALL_STATE_RINGING = "IMXCall.CALL_STATE_RINGING";
|
||||
|
||||
// incoming calls : create the call answer
|
||||
String CALL_STATE_CREATE_ANSWER = "IMXCall.CALL_STATE_CREATE_ANSWER";
|
||||
|
||||
// the call is connecting
|
||||
String CALL_STATE_CONNECTING = "IMXCall.CALL_STATE_CONNECTING";
|
||||
|
||||
// the call is in progress
|
||||
String CALL_STATE_CONNECTED = "IMXCall.CALL_STATE_CONNECTED";
|
||||
|
||||
// call is ended
|
||||
String CALL_STATE_ENDED = "IMXCall.CALL_STATE_ENDED";
|
||||
|
||||
// error codes
|
||||
// cannot initialize the camera
|
||||
String CALL_ERROR_CAMERA_INIT_FAILED = "IMXCall.CALL_ERROR_CAMERA_INIT_FAILED";
|
||||
|
||||
// cannot initialize the call.
|
||||
String CALL_ERROR_CALL_INIT_FAILED = "IMXCall.CALL_ERROR_CALL_INIT_FAILED";
|
||||
|
||||
// ICE error
|
||||
String CALL_ERROR_ICE_FAILED = "IMXCall.CALL_ERROR_ICE_FAILED";
|
||||
|
||||
// the user did not respond to the call.
|
||||
String CALL_ERROR_USER_NOT_RESPONDING = "IMXCall.CALL_ERROR_USER_NOT_RESPONDING";
|
||||
|
||||
// creator
|
||||
|
||||
/**
|
||||
* Create the callview
|
||||
*/
|
||||
void createCallView();
|
||||
|
||||
/**
|
||||
* The activity is paused.
|
||||
*/
|
||||
void onPause();
|
||||
|
||||
/**
|
||||
* The activity is resumed.
|
||||
*/
|
||||
void onResume();
|
||||
|
||||
// actions (must be done after dispatchOnViewReady()
|
||||
|
||||
/**
|
||||
* Start a call.
|
||||
*
|
||||
* @param aLocalVideoPosition position of the local video attendee
|
||||
*/
|
||||
void placeCall(VideoLayoutConfiguration aLocalVideoPosition);
|
||||
|
||||
/**
|
||||
* Prepare a call reception.
|
||||
*
|
||||
* @param aCallInviteParams the invitation Event content
|
||||
* @param aCallId the call ID
|
||||
* @param aLocalVideoPosition position of the local video attendee
|
||||
*/
|
||||
void prepareIncomingCall(JsonObject aCallInviteParams, String aCallId, VideoLayoutConfiguration aLocalVideoPosition);
|
||||
|
||||
/**
|
||||
* The call has been detected as an incoming one.
|
||||
* The application launched the dedicated activity and expects to launch the incoming call.
|
||||
*
|
||||
* @param aLocalVideoPosition position of the local video attendee
|
||||
*/
|
||||
void launchIncomingCall(VideoLayoutConfiguration aLocalVideoPosition);
|
||||
|
||||
/**
|
||||
* The video will be displayed according to the values set in aConfigurationToApply.
|
||||
*
|
||||
* @param aConfigurationToApply the new position to be applied
|
||||
*/
|
||||
void updateLocalVideoRendererPosition(VideoLayoutConfiguration aConfigurationToApply);
|
||||
|
||||
// events thread
|
||||
|
||||
/**
|
||||
* Manage the call events.
|
||||
*
|
||||
* @param event the call event.
|
||||
*/
|
||||
void handleCallEvent(Event event);
|
||||
|
||||
// user actions
|
||||
|
||||
/**
|
||||
* The call is accepted.
|
||||
*/
|
||||
void answer();
|
||||
|
||||
/**
|
||||
* The call has been has answered on another device.
|
||||
*/
|
||||
void onAnsweredElsewhere();
|
||||
|
||||
/**
|
||||
* The call is hung up.
|
||||
*
|
||||
* @param reason the reason
|
||||
*/
|
||||
void hangup(String reason);
|
||||
|
||||
/**
|
||||
* Add a listener to the call manager.
|
||||
*
|
||||
* @param callListener the call listener
|
||||
*/
|
||||
void addListener(IMXCallListener callListener);
|
||||
|
||||
/**
|
||||
* Remove a listener from the call manager.
|
||||
*
|
||||
* @param callListener the call listener
|
||||
*/
|
||||
void removeListener(IMXCallListener callListener);
|
||||
|
||||
// getters / setters
|
||||
|
||||
/**
|
||||
* @return the callId
|
||||
*/
|
||||
String getCallId();
|
||||
|
||||
/**
|
||||
* Set the callId
|
||||
*
|
||||
* @param callId the call id
|
||||
*/
|
||||
void setCallId(String callId);
|
||||
|
||||
/**
|
||||
* @return the linked room
|
||||
*/
|
||||
Room getRoom();
|
||||
|
||||
/**
|
||||
* Set the linked rooms (conference call)
|
||||
*
|
||||
* @param room the room
|
||||
* @param callSignalingRoom the call signaling room.
|
||||
*/
|
||||
void setRooms(Room room, Room callSignalingRoom);
|
||||
|
||||
/**
|
||||
* @return the call signaling room
|
||||
*/
|
||||
Room getCallSignalingRoom();
|
||||
|
||||
/**
|
||||
* @return the session
|
||||
*/
|
||||
MXSession getSession();
|
||||
|
||||
/**
|
||||
* @return true if the call is an incoming call.
|
||||
*/
|
||||
boolean isIncoming();
|
||||
|
||||
/**
|
||||
* Set the call type: video or voice
|
||||
*
|
||||
* @param isVideo true for video call, false for VoIP
|
||||
*/
|
||||
void setIsVideo(boolean isVideo);
|
||||
|
||||
/**
|
||||
* @return true if the call is a video call.
|
||||
*/
|
||||
boolean isVideo();
|
||||
|
||||
/**
|
||||
* Defines the call conference status
|
||||
*
|
||||
* @param isConference the conference status
|
||||
*/
|
||||
void setIsConference(boolean isConference);
|
||||
|
||||
/**
|
||||
* @return true if the call is a conference call.
|
||||
*/
|
||||
boolean isConference();
|
||||
|
||||
/**
|
||||
* @return the callstate (must be a CALL_STATE_XX value)
|
||||
*/
|
||||
String getCallState();
|
||||
|
||||
/**
|
||||
* @return the callView
|
||||
*/
|
||||
View getCallView();
|
||||
|
||||
/**
|
||||
* @return the callView visibility
|
||||
*/
|
||||
int getVisibility();
|
||||
|
||||
/**
|
||||
* Set the callview visibility
|
||||
*
|
||||
* @param visibility true to make the callview visible
|
||||
* @return true if the operation succeeds
|
||||
*/
|
||||
boolean setVisibility(int visibility);
|
||||
|
||||
/**
|
||||
* @return the call start time in ms since epoch, -1 if not defined.
|
||||
*/
|
||||
long getCallStartTime();
|
||||
|
||||
/**
|
||||
* @return the call elapsed time in seconds, -1 if not defined.
|
||||
*/
|
||||
long getCallElapsedTime();
|
||||
|
||||
/**
|
||||
* Switch between device cameras. The transmitted stream is modified
|
||||
* according to the new camera in use.
|
||||
* If the camera used in the video call is the front one, calling
|
||||
* switchRearFrontCamera(), will make the rear one to be used, and vice versa.
|
||||
* If only one camera is available, nothing is done.
|
||||
*
|
||||
* @return true if the switch succeed, false otherwise.
|
||||
*/
|
||||
boolean switchRearFrontCamera();
|
||||
|
||||
/**
|
||||
* Indicate if a camera switch was performed or not.
|
||||
* For some reason switching the camera from front to rear and
|
||||
* vice versa, could not be performed (ie. only one camera is available).
|
||||
* <p>
|
||||
* <br>See {@link #switchRearFrontCamera()}.
|
||||
*
|
||||
* @return true if camera was switched, false otherwise
|
||||
*/
|
||||
boolean isCameraSwitched();
|
||||
|
||||
/**
|
||||
* Indicate if the device supports camera switching.
|
||||
* <p>See {@link #switchRearFrontCamera()}.
|
||||
*
|
||||
* @return true if switch camera is supported, false otherwise
|
||||
*/
|
||||
boolean isSwitchCameraSupported();
|
||||
|
||||
/**
|
||||
* Mute/Unmute the recording of the local video attendee. Set isVideoMuted
|
||||
* to true to enable the recording of the video, if set to false no recording
|
||||
* is performed.
|
||||
*
|
||||
* @param isVideoMuted true to mute the video recording, false to unmute
|
||||
*/
|
||||
void muteVideoRecording(boolean isVideoMuted);
|
||||
|
||||
/**
|
||||
* Return the recording mute status of the local video attendee.
|
||||
* <p>
|
||||
* <br>See {@link #muteVideoRecording(boolean)}.
|
||||
*
|
||||
* @return true if video recording is muted, false otherwise
|
||||
*/
|
||||
boolean isVideoRecordingMuted();
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.call;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* This class tracks the call update.
|
||||
*/
|
||||
public interface IMXCallListener {
|
||||
|
||||
/**
|
||||
* Called when the call state change
|
||||
*
|
||||
* @param state the new call state
|
||||
*/
|
||||
void onStateDidChange(String state);
|
||||
|
||||
/**
|
||||
* Called when the call fails.
|
||||
*
|
||||
* @param error the failure reason
|
||||
*/
|
||||
void onCallError(String error);
|
||||
|
||||
/**
|
||||
* The call view has been created.
|
||||
* It can be inserted in a custom parent view.
|
||||
*
|
||||
* @param callView the call view
|
||||
*/
|
||||
void onCallViewCreated(View callView);
|
||||
|
||||
/**
|
||||
* The call view has been inserted.
|
||||
* The call is ready to be started.
|
||||
* For an outgoing call, use placeCall().
|
||||
* For an incoming call, use launchIncomingCall().
|
||||
*/
|
||||
void onReady();
|
||||
|
||||
/**
|
||||
* The call was answered on another device.
|
||||
*/
|
||||
void onCallAnsweredElsewhere();
|
||||
|
||||
/**
|
||||
* Warn that the call is ended
|
||||
*
|
||||
* @param aReasonId the reason of the call ending
|
||||
*/
|
||||
void onCallEnd(final int aReasonId);
|
||||
|
||||
/**
|
||||
* The video preview size has been updated.
|
||||
*
|
||||
* @param width the new width (non scaled size)
|
||||
* @param height the new height (non scaled size)
|
||||
*/
|
||||
void onPreviewSizeChanged(int width, int height);
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.call;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXDeviceInfo;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXUsersDevicesMap;
|
||||
|
||||
/**
|
||||
* This class manages the calls events.
|
||||
*/
|
||||
public interface IMXCallsManagerListener {
|
||||
/**
|
||||
* Called when there is an incoming call within the room.
|
||||
*
|
||||
* @param call the incoming call
|
||||
* @param unknownDevices the unknown e2e devices list
|
||||
*/
|
||||
void onIncomingCall(IMXCall call, MXUsersDevicesMap<MXDeviceInfo> unknownDevices);
|
||||
|
||||
/**
|
||||
* An outgoing call is started.
|
||||
*
|
||||
* @param call the outgoing call
|
||||
*/
|
||||
void onOutgoingCall(IMXCall call);
|
||||
|
||||
/**
|
||||
* Called when a called has been hung up
|
||||
*
|
||||
* @param call the incoming call
|
||||
*/
|
||||
void onCallHangUp(IMXCall call);
|
||||
|
||||
/**
|
||||
* A voip conference started in a room.
|
||||
*
|
||||
* @param roomId the room id
|
||||
*/
|
||||
void onVoipConferenceStarted(String roomId);
|
||||
|
||||
/**
|
||||
* A voip conference finished in a room.
|
||||
*
|
||||
* @param roomId the room id
|
||||
*/
|
||||
void onVoipConferenceFinished(String roomId);
|
||||
}
|
@ -0,0 +1,706 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.call;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.MXSession;
|
||||
import im.vector.matrix.android.internal.legacy.data.Room;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.Timer;
|
||||
|
||||
/**
|
||||
* This class is the default implementation
|
||||
*/
|
||||
public class MXCall implements IMXCall {
|
||||
private static final String LOG_TAG = MXCall.class.getSimpleName();
|
||||
|
||||
// defines the call timeout
|
||||
public static final int CALL_TIMEOUT_MS = 120 * 1000;
|
||||
|
||||
/**
|
||||
* The session
|
||||
*/
|
||||
protected MXSession mSession;
|
||||
|
||||
/**
|
||||
* The context
|
||||
*/
|
||||
protected Context mContext;
|
||||
|
||||
/**
|
||||
* the turn servers
|
||||
*/
|
||||
protected JsonElement mTurnServer;
|
||||
|
||||
/**
|
||||
* The room in which the call is performed.
|
||||
*/
|
||||
protected Room mCallingRoom;
|
||||
|
||||
/**
|
||||
* The room in which the call events are sent.
|
||||
* It might differ from mCallingRoom if it is a conference call.
|
||||
* For a 1:1 call, it will be equal to mCallingRoom.
|
||||
*/
|
||||
protected Room mCallSignalingRoom;
|
||||
|
||||
/**
|
||||
* The call events listeners
|
||||
*/
|
||||
private final Set<IMXCallListener> mCallListeners = new HashSet<>();
|
||||
|
||||
/**
|
||||
* the call id
|
||||
*/
|
||||
protected String mCallId;
|
||||
|
||||
/**
|
||||
* Tells if it is a video call
|
||||
*/
|
||||
protected boolean mIsVideoCall = false;
|
||||
|
||||
/**
|
||||
* Tells if it is an incoming call
|
||||
*/
|
||||
protected boolean mIsIncoming = false;
|
||||
|
||||
/**
|
||||
* Tells if it is a conference call.
|
||||
*/
|
||||
private boolean mIsConference = false;
|
||||
|
||||
/**
|
||||
* List of events to sends to mCallSignalingRoom
|
||||
*/
|
||||
protected final List<Event> mPendingEvents = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* The sending eevent.
|
||||
*/
|
||||
private Event mPendingEvent;
|
||||
|
||||
/**
|
||||
* The not responding timer
|
||||
*/
|
||||
protected Timer mCallTimeoutTimer;
|
||||
|
||||
// call start time
|
||||
private long mStartTime = -1;
|
||||
|
||||
// UI thread handler
|
||||
final Handler mUIThreadHandler = new Handler();
|
||||
|
||||
/**
|
||||
* Create the call view
|
||||
*/
|
||||
public void createCallView() {
|
||||
}
|
||||
|
||||
/**
|
||||
* The activity is paused.
|
||||
*/
|
||||
public void onPause() {
|
||||
}
|
||||
|
||||
/**
|
||||
* The activity is resumed.
|
||||
*/
|
||||
public void onResume() {
|
||||
}
|
||||
|
||||
// actions (must be done after dispatchOnViewReady()
|
||||
|
||||
/**
|
||||
* Start a call.
|
||||
*/
|
||||
public void placeCall(VideoLayoutConfiguration aLocalVideoPosition) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a call reception.
|
||||
*
|
||||
* @param aCallInviteParams the invitation Event content
|
||||
* @param aCallId the call ID
|
||||
* @param aLocalVideoPosition position of the local video attendee
|
||||
*/
|
||||
public void prepareIncomingCall(JsonObject aCallInviteParams, String aCallId, VideoLayoutConfiguration aLocalVideoPosition) {
|
||||
setIsIncoming(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* The call has been detected as an incoming one.
|
||||
* The application launched the dedicated activity and expects to launch the incoming call.
|
||||
*
|
||||
* @param aLocalVideoPosition position of the local video attendee
|
||||
*/
|
||||
public void launchIncomingCall(VideoLayoutConfiguration aLocalVideoPosition) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateLocalVideoRendererPosition(VideoLayoutConfiguration aLocalVideoPosition) {
|
||||
Log.w(LOG_TAG, "## updateLocalVideoRendererPosition(): not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean switchRearFrontCamera() {
|
||||
Log.w(LOG_TAG, "## switchRearFrontCamera(): not implemented");
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCameraSwitched() {
|
||||
Log.w(LOG_TAG, "## isCameraSwitched(): not implemented");
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSwitchCameraSupported() {
|
||||
Log.w(LOG_TAG, "## isSwitchCameraSupported(): not implemented");
|
||||
return false;
|
||||
}
|
||||
// events thread
|
||||
|
||||
/**
|
||||
* Manage the call events.
|
||||
*
|
||||
* @param event the call event.
|
||||
*/
|
||||
public void handleCallEvent(Event event) {
|
||||
}
|
||||
|
||||
// user actions
|
||||
|
||||
/**
|
||||
* The call is accepted.
|
||||
*/
|
||||
public void answer() {
|
||||
}
|
||||
|
||||
/**
|
||||
* The call has been has answered on another device.
|
||||
*/
|
||||
public void onAnsweredElsewhere() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The call is hung up.
|
||||
*/
|
||||
public void hangup(String reason) {
|
||||
}
|
||||
|
||||
// getters / setters
|
||||
|
||||
/**
|
||||
* @return the callId
|
||||
*/
|
||||
public String getCallId() {
|
||||
return mCallId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callId
|
||||
*/
|
||||
public void setCallId(String callId) {
|
||||
mCallId = callId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the linked room
|
||||
*/
|
||||
public Room getRoom() {
|
||||
return mCallingRoom;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the call signaling room
|
||||
*/
|
||||
public Room getCallSignalingRoom() {
|
||||
return mCallSignalingRoom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the linked rooms.
|
||||
*
|
||||
* @param room the room where the conference take place
|
||||
* @param callSignalingRoom the call signaling room.
|
||||
*/
|
||||
public void setRooms(Room room, Room callSignalingRoom) {
|
||||
mCallingRoom = room;
|
||||
mCallSignalingRoom = callSignalingRoom;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the session
|
||||
*/
|
||||
public MXSession getSession() {
|
||||
return mSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the call is an incoming call.
|
||||
*/
|
||||
public boolean isIncoming() {
|
||||
return mIsIncoming;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isIncoming true if the call is an incoming one.
|
||||
*/
|
||||
private void setIsIncoming(boolean isIncoming) {
|
||||
mIsIncoming = isIncoming;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the call type
|
||||
*/
|
||||
public void setIsVideo(boolean isVideo) {
|
||||
mIsVideoCall = isVideo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the call is a video call.
|
||||
*/
|
||||
public boolean isVideo() {
|
||||
return mIsVideoCall;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the call conference status
|
||||
*/
|
||||
public void setIsConference(boolean isConference) {
|
||||
mIsConference = isConference;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the call is a conference call.
|
||||
*/
|
||||
public boolean isConference() {
|
||||
return mIsConference;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the callstate (must be a CALL_STATE_XX value)
|
||||
*/
|
||||
public String getCallState() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the callView
|
||||
*/
|
||||
public View getCallView() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the callView visibility
|
||||
*/
|
||||
public int getVisibility() {
|
||||
return View.GONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callview visibility
|
||||
*
|
||||
* @return true if the operation succeeds
|
||||
*/
|
||||
public boolean setVisibility(int visibility) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if the call is ended.
|
||||
*/
|
||||
public boolean isCallEnded() {
|
||||
return TextUtils.equals(CALL_STATE_ENDED, getCallState());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the call start time in ms since epoch, -1 if not defined.
|
||||
*/
|
||||
public long getCallStartTime() {
|
||||
return mStartTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the call elapsed time in seconds, -1 if not defined.
|
||||
*/
|
||||
public long getCallElapsedTime() {
|
||||
if (-1 == mStartTime) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return (System.currentTimeMillis() - mStartTime) / 1000;
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// call events listener
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* Add a listener.
|
||||
*
|
||||
* @param callListener the listener to add
|
||||
*/
|
||||
public void addListener(IMXCallListener callListener) {
|
||||
if (null != callListener) {
|
||||
synchronized (LOG_TAG) {
|
||||
mCallListeners.add(callListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a listener
|
||||
*
|
||||
* @param callListener the listener to remove
|
||||
*/
|
||||
public void removeListener(IMXCallListener callListener) {
|
||||
if (null != callListener) {
|
||||
synchronized (LOG_TAG) {
|
||||
mCallListeners.remove(callListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the listeners
|
||||
*/
|
||||
public void clearListeners() {
|
||||
synchronized (LOG_TAG) {
|
||||
mCallListeners.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the call listeners
|
||||
*/
|
||||
private Collection<IMXCallListener> getCallListeners() {
|
||||
Collection<IMXCallListener> listeners;
|
||||
|
||||
synchronized (LOG_TAG) {
|
||||
listeners = new HashSet<>(mCallListeners);
|
||||
}
|
||||
|
||||
return listeners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the onCallViewCreated event to the listeners.
|
||||
*
|
||||
* @param callView the call view
|
||||
*/
|
||||
protected void dispatchOnCallViewCreated(View callView) {
|
||||
if (isCallEnded()) {
|
||||
Log.d(LOG_TAG, "## dispatchOnCallViewCreated(): the call is ended");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(LOG_TAG, "## dispatchOnCallViewCreated()");
|
||||
|
||||
Collection<IMXCallListener> listeners = getCallListeners();
|
||||
|
||||
for (IMXCallListener listener : listeners) {
|
||||
try {
|
||||
listener.onCallViewCreated(callView);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## dispatchOnCallViewCreated(): Exception Msg=" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the onViewReady event to the listeners.
|
||||
*/
|
||||
protected void dispatchOnReady() {
|
||||
if (isCallEnded()) {
|
||||
Log.d(LOG_TAG, "## dispatchOnReady() : the call is ended");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(LOG_TAG, "## dispatchOnReady()");
|
||||
|
||||
Collection<IMXCallListener> listeners = getCallListeners();
|
||||
|
||||
for (IMXCallListener listener : listeners) {
|
||||
try {
|
||||
listener.onReady();
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## dispatchOnReady(): Exception Msg=" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the onCallError event to the listeners.
|
||||
*
|
||||
* @param error error message
|
||||
*/
|
||||
protected void dispatchOnCallError(String error) {
|
||||
if (isCallEnded()) {
|
||||
Log.d(LOG_TAG, "## dispatchOnCallError() : the call is ended");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(LOG_TAG, "## dispatchOnCallError()");
|
||||
|
||||
Collection<IMXCallListener> listeners = getCallListeners();
|
||||
|
||||
for (IMXCallListener listener : listeners) {
|
||||
try {
|
||||
listener.onCallError(error);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## dispatchOnCallError(): " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the onStateDidChange event to the listeners.
|
||||
*
|
||||
* @param newState the new state
|
||||
*/
|
||||
protected void dispatchOnStateDidChange(String newState) {
|
||||
Log.d(LOG_TAG, "## dispatchOnCallErrorOnStateDidChange(): " + newState);
|
||||
|
||||
// set the call start time
|
||||
if (TextUtils.equals(CALL_STATE_CONNECTED, newState) && (-1 == mStartTime)) {
|
||||
mStartTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
// the call is ended.
|
||||
if (TextUtils.equals(CALL_STATE_ENDED, newState)) {
|
||||
mStartTime = -1;
|
||||
}
|
||||
|
||||
Collection<IMXCallListener> listeners = getCallListeners();
|
||||
|
||||
for (IMXCallListener listener : listeners) {
|
||||
try {
|
||||
listener.onStateDidChange(newState);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## dispatchOnStateDidChange(): Exception Msg=" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the onCallAnsweredElsewhere event to the listeners.
|
||||
*/
|
||||
protected void dispatchAnsweredElsewhere() {
|
||||
Log.d(LOG_TAG, "## dispatchAnsweredElsewhere()");
|
||||
|
||||
Collection<IMXCallListener> listeners = getCallListeners();
|
||||
|
||||
for (IMXCallListener listener : listeners) {
|
||||
try {
|
||||
listener.onCallAnsweredElsewhere();
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## dispatchAnsweredElsewhere(): Exception Msg=" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the onCallEnd event to the listeners.
|
||||
*
|
||||
* @param aEndCallReasonId the reason of the call ending
|
||||
*/
|
||||
protected void dispatchOnCallEnd(int aEndCallReasonId) {
|
||||
Log.d(LOG_TAG, "## dispatchOnCallEnd(): endReason=" + aEndCallReasonId);
|
||||
|
||||
Collection<IMXCallListener> listeners = getCallListeners();
|
||||
|
||||
for (IMXCallListener listener : listeners) {
|
||||
try {
|
||||
listener.onCallEnd(aEndCallReasonId);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## dispatchOnCallEnd(): Exception Msg=" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the next pending events
|
||||
*/
|
||||
protected void sendNextEvent() {
|
||||
mUIThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// do not send any new message
|
||||
if (isCallEnded() && (null != mPendingEvents)) {
|
||||
mPendingEvents.clear();
|
||||
}
|
||||
|
||||
// ready to send
|
||||
if ((null == mPendingEvent) && (0 != mPendingEvents.size())) {
|
||||
mPendingEvent = mPendingEvents.get(0);
|
||||
mPendingEvents.remove(mPendingEvent);
|
||||
|
||||
Log.d(LOG_TAG, "## sendNextEvent() : sending event of type " + mPendingEvent.getType() + " event id " + mPendingEvent.eventId);
|
||||
mCallSignalingRoom.sendEvent(mPendingEvent, new ApiCallback<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void info) {
|
||||
mUIThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Log.d(LOG_TAG, "## sendNextEvent() : event " + mPendingEvent.eventId + " is sent");
|
||||
|
||||
mPendingEvent = null;
|
||||
sendNextEvent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void commonFailure(String reason) {
|
||||
Log.d(LOG_TAG, "## sendNextEvent() : event " + mPendingEvent.eventId + " failed to be sent " + reason);
|
||||
|
||||
// let try next candidate event
|
||||
if (TextUtils.equals(mPendingEvent.getType(), Event.EVENT_TYPE_CALL_CANDIDATES)) {
|
||||
mUIThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mPendingEvent = null;
|
||||
sendNextEvent();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
hangup(reason);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
commonFailure(e.getLocalizedMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
commonFailure(e.getLocalizedMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
commonFailure(e.getLocalizedMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the onPreviewSizeChanged event to the listeners.
|
||||
*
|
||||
* @param width the preview width
|
||||
* @param height the preview height
|
||||
*/
|
||||
protected void dispatchOnPreviewSizeChanged(int width, int height) {
|
||||
Log.d(LOG_TAG, "## dispatchOnPreviewSizeChanged(): width =" + width + " - height =" + height);
|
||||
|
||||
Collection<IMXCallListener> listeners = getCallListeners();
|
||||
|
||||
for (IMXCallListener listener : listeners) {
|
||||
try {
|
||||
listener.onPreviewSizeChanged(width, height);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## dispatchOnPreviewSizeChanged(): Exception Msg=" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* send an hang up event
|
||||
*
|
||||
* @param reason the reason
|
||||
*/
|
||||
protected void sendHangup(String reason) {
|
||||
JsonObject hangupContent = new JsonObject();
|
||||
|
||||
hangupContent.add("version", new JsonPrimitive(0));
|
||||
hangupContent.add("call_id", new JsonPrimitive(mCallId));
|
||||
|
||||
if (!TextUtils.isEmpty(reason)) {
|
||||
hangupContent.add("reason", new JsonPrimitive(reason));
|
||||
}
|
||||
|
||||
Event event = new Event(Event.EVENT_TYPE_CALL_HANGUP, hangupContent, mSession.getCredentials().userId, mCallSignalingRoom.getRoomId());
|
||||
|
||||
// local notification to indicate the end of call
|
||||
mUIThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
dispatchOnCallEnd(END_CALL_REASON_USER_HIMSELF);
|
||||
}
|
||||
});
|
||||
|
||||
Log.d(LOG_TAG, "## sendHangup(): reason=" + reason);
|
||||
|
||||
// send hang up event to the server
|
||||
mCallSignalingRoom.sendEvent(event, new ApiCallback<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void info) {
|
||||
Log.d(LOG_TAG, "## sendHangup(): onSuccess");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
Log.e(LOG_TAG, "## sendHangup(): onNetworkError Msg=" + e.getMessage(), e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
Log.e(LOG_TAG, "## sendHangup(): onMatrixError Msg=" + e.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
Log.e(LOG_TAG, "## sendHangup(): onUnexpectedError Msg=" + e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void muteVideoRecording(boolean isVideoMuted) {
|
||||
Log.w(LOG_TAG, "## muteVideoRecording(): not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVideoRecordingMuted() {
|
||||
Log.w(LOG_TAG, "## muteVideoRecording(): not implemented - default value = false");
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.call;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* This class is the default implementation of IMXCallListener.
|
||||
*/
|
||||
public class MXCallListener implements IMXCallListener {
|
||||
|
||||
@Override
|
||||
public void onStateDidChange(String state) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallError(String error) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallViewCreated(View callView) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReady() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallAnsweredElsewhere() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallEnd(final int aReasonId) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreviewSizeChanged(int width, int height) {
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.call;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXDeviceInfo;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXUsersDevicesMap;
|
||||
|
||||
/**
|
||||
* This class is the default implementation of MXCallsManagerListener
|
||||
*/
|
||||
public class MXCallsManagerListener implements IMXCallsManagerListener {
|
||||
|
||||
@Override
|
||||
public void onIncomingCall(IMXCall call, MXUsersDevicesMap<MXDeviceInfo> unknownDevices) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOutgoingCall(IMXCall call) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallHangUp(IMXCall call) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVoipConferenceStarted(String roomId) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVoipConferenceFinished(String roomId) {
|
||||
}
|
||||
}
|
@ -0,0 +1,687 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.call;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import android.webkit.PermissionRequest;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.MXSession;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
public class MXChromeCall extends MXCall {
|
||||
private static final String LOG_TAG = MXChromeCall.class.getSimpleName();
|
||||
|
||||
private WebView mWebView = null;
|
||||
private CallWebAppInterface mCallWebAppInterface = null;
|
||||
|
||||
private boolean mIsIncomingPrepared = false;
|
||||
|
||||
private JsonObject mCallInviteParams = null;
|
||||
|
||||
private JsonArray mPendingCandidates = new JsonArray();
|
||||
|
||||
/**
|
||||
* @return true if this stack can perform calls.
|
||||
*/
|
||||
public static boolean isSupported() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
|
||||
}
|
||||
|
||||
// creator
|
||||
public MXChromeCall(MXSession session, Context context, JsonElement turnServer) {
|
||||
if (!isSupported()) {
|
||||
throw new AssertionError("MXChromeCall : not supported with the current android version");
|
||||
}
|
||||
|
||||
if (null == session) {
|
||||
throw new AssertionError("MXChromeCall : session cannot be null");
|
||||
}
|
||||
|
||||
if (null == context) {
|
||||
throw new AssertionError("MXChromeCall : context cannot be null");
|
||||
}
|
||||
|
||||
mCallId = "c" + System.currentTimeMillis();
|
||||
mSession = session;
|
||||
mContext = context;
|
||||
mTurnServer = turnServer;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("NewApi")
|
||||
public void createCallView() {
|
||||
super.createCallView();
|
||||
mUIThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mWebView = new WebView(mContext);
|
||||
mWebView.setBackgroundColor(Color.BLACK);
|
||||
|
||||
// warn that the webview must be added in an activity/fragment
|
||||
dispatchOnCallViewCreated(mWebView);
|
||||
|
||||
mUIThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mCallWebAppInterface = new CallWebAppInterface();
|
||||
mWebView.addJavascriptInterface(mCallWebAppInterface, "Android");
|
||||
|
||||
WebView.setWebContentsDebuggingEnabled(true);
|
||||
WebSettings settings = mWebView.getSettings();
|
||||
|
||||
// Enable Javascript
|
||||
settings.setJavaScriptEnabled(true);
|
||||
|
||||
// Use WideViewport and Zoom out if there is no viewport defined
|
||||
settings.setUseWideViewPort(true);
|
||||
settings.setLoadWithOverviewMode(true);
|
||||
|
||||
// Enable pinch to zoom without the zoom buttons
|
||||
settings.setBuiltInZoomControls(true);
|
||||
|
||||
// Allow use of Local Storage
|
||||
settings.setDomStorageEnabled(true);
|
||||
|
||||
settings.setAllowFileAccessFromFileURLs(true);
|
||||
settings.setAllowUniversalAccessFromFileURLs(true);
|
||||
|
||||
settings.setDisplayZoomControls(false);
|
||||
|
||||
mWebView.setWebViewClient(new WebViewClient());
|
||||
|
||||
// AppRTC requires third party cookies to work
|
||||
android.webkit.CookieManager cookieManager = android.webkit.CookieManager.getInstance();
|
||||
cookieManager.setAcceptThirdPartyCookies(mWebView, true);
|
||||
|
||||
final String url = "file:///android_asset/www/call.html";
|
||||
mWebView.loadUrl(url);
|
||||
|
||||
mWebView.setWebChromeClient(new WebChromeClient() {
|
||||
@Override
|
||||
public void onPermissionRequest(final PermissionRequest request) {
|
||||
mUIThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
request.grant(request.getResources());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a call.
|
||||
*/
|
||||
@Override
|
||||
public void placeCall(VideoLayoutConfiguration aLocalVideoPosition) {
|
||||
super.placeCall(aLocalVideoPosition);
|
||||
if (CALL_STATE_READY.equals(getCallState())) {
|
||||
mIsIncoming = false;
|
||||
|
||||
mUIThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mWebView.loadUrl(mIsVideoCall ? "javascript:placeVideoCall()" : "javascript:placeVoiceCall()");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a call reception.
|
||||
*
|
||||
* @param aCallInviteParams the invitation Event content
|
||||
* @param aCallId the call ID
|
||||
* @param aLocalVideoPosition position of the local video attendee
|
||||
*/
|
||||
@Override
|
||||
public void prepareIncomingCall(final JsonObject aCallInviteParams, final String aCallId, VideoLayoutConfiguration aLocalVideoPosition) {
|
||||
Log.d(LOG_TAG, "## prepareIncomingCall : call state " + getCallState());
|
||||
super.prepareIncomingCall(aCallInviteParams, aCallId, aLocalVideoPosition);
|
||||
mCallId = aCallId;
|
||||
|
||||
if (CALL_STATE_READY.equals(getCallState())) {
|
||||
mIsIncoming = true;
|
||||
|
||||
mUIThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mWebView.loadUrl("javascript:initWithInvite('" + aCallId + "'," + aCallInviteParams.toString() + ")");
|
||||
mIsIncomingPrepared = true;
|
||||
|
||||
mWebView.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
checkPendingCandidates();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (CALL_STATE_CREATED.equals(getCallState())) {
|
||||
mCallInviteParams = aCallInviteParams;
|
||||
|
||||
// detect call type from the sdp
|
||||
try {
|
||||
JsonObject offer = mCallInviteParams.get("offer").getAsJsonObject();
|
||||
JsonElement sdp = offer.get("sdp");
|
||||
String sdpValue = sdp.getAsString();
|
||||
setIsVideo(sdpValue.contains("m=video"));
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## prepareIncomingCall() ; " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The call has been detected as an incoming one.
|
||||
* The application launched the dedicated activity and expects to launch the incoming call.
|
||||
*
|
||||
* @param aLocalVideoPosition local video position
|
||||
*/
|
||||
@Override
|
||||
public void launchIncomingCall(VideoLayoutConfiguration aLocalVideoPosition) {
|
||||
super.launchIncomingCall(aLocalVideoPosition);
|
||||
if (CALL_STATE_READY.equals(getCallState())) {
|
||||
prepareIncomingCall(mCallInviteParams, mCallId, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The callee accepts the call.
|
||||
*
|
||||
* @param event the event
|
||||
*/
|
||||
private void onCallAnswer(final Event event) {
|
||||
if (!CALL_STATE_CREATED.equals(getCallState()) && (null != mWebView)) {
|
||||
mUIThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mWebView.loadUrl("javascript:receivedAnswer(" + event.getContent().toString() + ")");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The other call member hangs up the call.
|
||||
*
|
||||
* @param event the event
|
||||
*/
|
||||
private void onCallHangup(final Event event) {
|
||||
if (!CALL_STATE_CREATED.equals(getCallState()) && (null != mWebView)) {
|
||||
mUIThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mWebView.loadUrl("javascript:onHangupReceived(" + event.getContent().toString() + ")");
|
||||
|
||||
mWebView.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
dispatchOnCallEnd(END_CALL_REASON_PEER_HANG_UP);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A new Ice candidate is received
|
||||
*
|
||||
* @param candidates the ice candidates
|
||||
*/
|
||||
public void onNewCandidates(final JsonElement candidates) {
|
||||
if (!CALL_STATE_CREATED.equals(getCallState()) && (null != mWebView)) {
|
||||
mWebView.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mWebView.loadUrl("javascript:gotRemoteCandidates(" + candidates.toString() + ")");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ice candidates
|
||||
*
|
||||
* @param candidates ic candidates
|
||||
*/
|
||||
private void addCandidates(JsonArray candidates) {
|
||||
if (mIsIncomingPrepared || !isIncoming()) {
|
||||
onNewCandidates(candidates);
|
||||
} else {
|
||||
synchronized (LOG_TAG) {
|
||||
mPendingCandidates.addAll(candidates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Some Ice candidates could have been received while creating the call view.
|
||||
* Check if some of them have been defined.
|
||||
*/
|
||||
public void checkPendingCandidates() {
|
||||
synchronized (LOG_TAG) {
|
||||
onNewCandidates(mPendingCandidates);
|
||||
mPendingCandidates = new JsonArray();
|
||||
}
|
||||
}
|
||||
|
||||
// events thread
|
||||
|
||||
/**
|
||||
* Manage the call events.
|
||||
*
|
||||
* @param event the call event.
|
||||
*/
|
||||
@Override
|
||||
public void handleCallEvent(Event event) {
|
||||
super.handleCallEvent(event);
|
||||
|
||||
String eventType = event.getType();
|
||||
|
||||
if (event.isCallEvent()) {
|
||||
// event from other member
|
||||
if (!TextUtils.equals(event.getSender(), mSession.getMyUserId())) {
|
||||
if (Event.EVENT_TYPE_CALL_ANSWER.equals(eventType) && !mIsIncoming) {
|
||||
onCallAnswer(event);
|
||||
} else if (Event.EVENT_TYPE_CALL_CANDIDATES.equals(eventType)) {
|
||||
JsonArray candidates = event.getContentAsJsonObject().getAsJsonArray("candidates");
|
||||
addCandidates(candidates);
|
||||
} else if (Event.EVENT_TYPE_CALL_HANGUP.equals(eventType)) {
|
||||
onCallHangup(event);
|
||||
}
|
||||
} else if (Event.EVENT_TYPE_CALL_INVITE.equals(eventType)) {
|
||||
// server echo : assume that the other device is ringing
|
||||
mCallWebAppInterface.mCallState = IMXCall.CALL_STATE_RINGING;
|
||||
|
||||
// warn in the UI thread
|
||||
mUIThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
dispatchOnStateDidChange(mCallWebAppInterface.mCallState);
|
||||
}
|
||||
});
|
||||
|
||||
} else if (Event.EVENT_TYPE_CALL_ANSWER.equals(eventType)) {
|
||||
// check if the call has not been answer in another device
|
||||
mUIThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// ring on this side
|
||||
if (getCallState().equals(IMXCall.CALL_STATE_RINGING)) {
|
||||
onAnsweredElsewhere();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// user actions
|
||||
|
||||
/**
|
||||
* The call is accepted.
|
||||
*/
|
||||
@Override
|
||||
public void answer() {
|
||||
super.answer();
|
||||
if (!CALL_STATE_CREATED.equals(getCallState()) && (null != mWebView)) {
|
||||
mUIThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mWebView.loadUrl("javascript:answerCall()");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The call is hung up.
|
||||
*/
|
||||
@Override
|
||||
public void hangup(String reason) {
|
||||
super.hangup(reason);
|
||||
|
||||
if (!CALL_STATE_CREATED.equals(getCallState()) && (null != mWebView)) {
|
||||
mUIThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mWebView.loadUrl("javascript:hangup()");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
sendHangup(reason);
|
||||
}
|
||||
}
|
||||
|
||||
// getters / setters
|
||||
|
||||
/**
|
||||
* @return the callstate (must be a CALL_STATE_XX value)
|
||||
*/
|
||||
@Override
|
||||
public String getCallState() {
|
||||
if (null != mCallWebAppInterface) {
|
||||
return mCallWebAppInterface.mCallState;
|
||||
} else {
|
||||
return CALL_STATE_CREATED;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the callView
|
||||
*/
|
||||
@Override
|
||||
public View getCallView() {
|
||||
return mWebView;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the callView visibility
|
||||
*/
|
||||
@Override
|
||||
public int getVisibility() {
|
||||
if (null != mWebView) {
|
||||
return mWebView.getVisibility();
|
||||
} else {
|
||||
return View.GONE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callview visibility
|
||||
*
|
||||
* @return true if the operation succeeds
|
||||
*/
|
||||
public boolean setVisibility(int visibility) {
|
||||
if (null != mWebView) {
|
||||
mWebView.setVisibility(visibility);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnsweredElsewhere() {
|
||||
super.onAnsweredElsewhere();
|
||||
mUIThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mWebView.loadUrl("javascript:onAnsweredElsewhere()");
|
||||
}
|
||||
});
|
||||
|
||||
dispatchAnsweredElsewhere();
|
||||
}
|
||||
|
||||
// private class
|
||||
private class CallWebAppInterface {
|
||||
public String mCallState = CALL_STATE_CREATING_CALL_VIEW;
|
||||
private Timer mCallTimeoutTimer = null;
|
||||
|
||||
CallWebAppInterface() {
|
||||
if (null == mCallingRoom) {
|
||||
throw new AssertionError("MXChromeCall : room cannot be null");
|
||||
}
|
||||
}
|
||||
|
||||
// JS <-> android calls
|
||||
@JavascriptInterface
|
||||
public String wgetCallId() {
|
||||
return mCallId;
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public String wgetRoomId() {
|
||||
return mCallSignalingRoom.getRoomId();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public String wgetTurnServer() {
|
||||
if (null != mTurnServer) {
|
||||
return mTurnServer.toString();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void wlog(String message) {
|
||||
Log.d(LOG_TAG, "WebView Message : " + message);
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void wCallError(String message) {
|
||||
Log.e(LOG_TAG, "WebView error Message : " + message);
|
||||
if ("ice_failed".equals(message)) {
|
||||
dispatchOnCallError(CALL_ERROR_ICE_FAILED);
|
||||
} else if ("user_media_failed".equals(message)) {
|
||||
dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void wOnStateUpdate(String jsstate) {
|
||||
String nextState = null;
|
||||
|
||||
if ("fledgling".equals(jsstate)) {
|
||||
nextState = CALL_STATE_READY;
|
||||
} else if ("wait_local_media".equals(jsstate)) {
|
||||
nextState = CALL_STATE_WAIT_LOCAL_MEDIA;
|
||||
} else if ("create_offer".equals(jsstate)) {
|
||||
nextState = CALL_STATE_WAIT_CREATE_OFFER;
|
||||
} else if ("invite_sent".equals(jsstate)) {
|
||||
nextState = CALL_STATE_INVITE_SENT;
|
||||
} else if ("ringing".equals(jsstate)) {
|
||||
nextState = CALL_STATE_RINGING;
|
||||
} else if ("create_answer".equals(jsstate)) {
|
||||
nextState = CALL_STATE_CREATE_ANSWER;
|
||||
} else if ("connecting".equals(jsstate)) {
|
||||
nextState = CALL_STATE_CONNECTING;
|
||||
} else if ("connected".equals(jsstate)) {
|
||||
nextState = CALL_STATE_CONNECTED;
|
||||
} else if ("ended".equals(jsstate)) {
|
||||
nextState = CALL_STATE_ENDED;
|
||||
}
|
||||
|
||||
// is there any state update ?
|
||||
if ((null != nextState) && !mCallState.equals(nextState)) {
|
||||
mCallState = nextState;
|
||||
|
||||
// warn in the UI thread
|
||||
mUIThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// call timeout management
|
||||
if (CALL_STATE_CONNECTING.equals(mCallState) || CALL_STATE_CONNECTING.equals(mCallState)) {
|
||||
if (null != mCallTimeoutTimer) {
|
||||
mCallTimeoutTimer.cancel();
|
||||
mCallTimeoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
dispatchOnStateDidChange(mCallState);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void wOnLoaded() {
|
||||
mCallState = CALL_STATE_READY;
|
||||
|
||||
mUIThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
dispatchOnReady();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void sendHangup(final Event event) {
|
||||
if (null != mCallTimeoutTimer) {
|
||||
mCallTimeoutTimer.cancel();
|
||||
mCallTimeoutTimer = null;
|
||||
}
|
||||
|
||||
mUIThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
dispatchOnCallEnd(END_CALL_REASON_UNDEFINED);
|
||||
}
|
||||
});
|
||||
|
||||
mPendingEvents.clear();
|
||||
|
||||
mCallSignalingRoom.sendEvent(event, new ApiCallback<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void info) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
// try again
|
||||
sendHangup(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void wSendEvent(final String roomId, final String eventType, final String jsonContent) {
|
||||
mUIThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
boolean addIt = true;
|
||||
JsonObject content = (JsonObject) new JsonParser().parse(jsonContent);
|
||||
|
||||
// merge candidates
|
||||
if (TextUtils.equals(eventType, Event.EVENT_TYPE_CALL_CANDIDATES) && (mPendingEvents.size() > 0)) {
|
||||
try {
|
||||
Event lastEvent = mPendingEvents.get(mPendingEvents.size() - 1);
|
||||
|
||||
if (TextUtils.equals(lastEvent.getType(), Event.EVENT_TYPE_CALL_CANDIDATES)) {
|
||||
JsonObject lastContent = lastEvent.getContentAsJsonObject();
|
||||
|
||||
JsonArray lastContentCandidates = lastContent.get("candidates").getAsJsonArray();
|
||||
JsonArray newContentCandidates = content.get("candidates").getAsJsonArray();
|
||||
|
||||
Log.d(LOG_TAG, "Merge candidates from " + lastContentCandidates.size()
|
||||
+ " to " + (lastContentCandidates.size() + newContentCandidates.size() + " items."));
|
||||
|
||||
lastContentCandidates.addAll(newContentCandidates);
|
||||
|
||||
lastContent.remove("candidates");
|
||||
lastContent.add("candidates", lastContentCandidates);
|
||||
addIt = false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## wSendEvent() ; " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
if (addIt) {
|
||||
Event event = new Event(eventType, content, mSession.getCredentials().userId, mCallSignalingRoom.getRoomId());
|
||||
|
||||
if (null != event) {
|
||||
// receive an hangup -> close the window asap
|
||||
if (TextUtils.equals(eventType, Event.EVENT_TYPE_CALL_HANGUP)) {
|
||||
sendHangup(event);
|
||||
} else {
|
||||
mPendingEvents.add(event);
|
||||
}
|
||||
|
||||
// the calleee has 30s to answer to call
|
||||
if (TextUtils.equals(eventType, Event.EVENT_TYPE_CALL_INVITE)) {
|
||||
try {
|
||||
mCallTimeoutTimer = new Timer();
|
||||
mCallTimeoutTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (getCallState().equals(IMXCall.CALL_STATE_RINGING)
|
||||
|| getCallState().equals(IMXCall.CALL_STATE_INVITE_SENT)) {
|
||||
dispatchOnCallError(CALL_ERROR_USER_NOT_RESPONDING);
|
||||
hangup(null);
|
||||
}
|
||||
|
||||
// cancel the timer
|
||||
mCallTimeoutTimer.cancel();
|
||||
mCallTimeoutTimer = null;
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## wSendEvent() ; " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}, CALL_TIMEOUT_MS);
|
||||
} catch (Throwable throwable) {
|
||||
if (null != mCallTimeoutTimer) {
|
||||
mCallTimeoutTimer.cancel();
|
||||
mCallTimeoutTimer = null;
|
||||
}
|
||||
Log.e(LOG_TAG, "## wSendEvent() ; " + throwable.getMessage(), throwable);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// send events
|
||||
sendNextEvent();
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## wSendEvent() ; " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,520 @@
|
||||
package im.vector.matrix.android.internal.legacy.call;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.graphics.Point;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.oney.WebRTCModule.EglUtils;
|
||||
import com.oney.WebRTCModule.WebRTCView;
|
||||
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.MediaStream;
|
||||
import org.webrtc.RendererCommon;
|
||||
import org.webrtc.RendererCommon.RendererEvents;
|
||||
import org.webrtc.RendererCommon.ScalingType;
|
||||
import org.webrtc.SurfaceViewRenderer;
|
||||
import org.webrtc.VideoRenderer;
|
||||
import org.webrtc.VideoTrack;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Use the older implementation of WebRtcView.
|
||||
* The latest version a stream URL instead of a stream.
|
||||
* It implies to have a React context.
|
||||
*/
|
||||
public class MXWebRtcView extends ViewGroup {
|
||||
/**
|
||||
* The scaling type to be utilized by default.
|
||||
* <p>
|
||||
* The default value is in accord with
|
||||
* https://www.w3.org/TR/html5/embedded-content-0.html#the-video-element:
|
||||
* <p>
|
||||
* In the absence of style rules to the contrary, video content should be
|
||||
* rendered inside the element's playback area such that the video content
|
||||
* is shown centered in the playback area at the largest possible size that
|
||||
* fits completely within it, with the video content's aspect ratio being
|
||||
* preserved. Thus, if the aspect ratio of the playback area does not match
|
||||
* the aspect ratio of the video, the video will be shown letterboxed or
|
||||
* pillarboxed. Areas of the element's playback area that do not contain the
|
||||
* video represent nothing.
|
||||
*/
|
||||
private static final ScalingType DEFAULT_SCALING_TYPE
|
||||
= ScalingType.SCALE_ASPECT_FIT;
|
||||
|
||||
/**
|
||||
* {@link View#isInLayout()} as a <tt>Method</tt> to be invoked via
|
||||
* reflection in order to accommodate its lack of availability before API
|
||||
* level 18. {@link ViewCompat#isInLayout(View)} is the best solution but I
|
||||
* could not make it available along with
|
||||
* {@link ViewCompat#isAttachedToWindow(View)} at the time of this writing.
|
||||
*/
|
||||
private static final Method IS_IN_LAYOUT;
|
||||
|
||||
private static final String LOG_TAG = MXWebRtcView.class.getSimpleName();
|
||||
|
||||
static {
|
||||
// IS_IN_LAYOUT
|
||||
Method isInLayout = null;
|
||||
|
||||
try {
|
||||
Method m = MXWebRtcView.class.getMethod("isInLayout");
|
||||
|
||||
if (boolean.class.isAssignableFrom(m.getReturnType())) {
|
||||
isInLayout = m;
|
||||
}
|
||||
} catch (NoSuchMethodException e) {
|
||||
// Fall back to the behavior of ViewCompat#isInLayout(View).
|
||||
}
|
||||
IS_IN_LAYOUT = isInLayout;
|
||||
}
|
||||
|
||||
/**
|
||||
* The height of the last video frame rendered by
|
||||
* {@link #surfaceViewRenderer}.
|
||||
*/
|
||||
private int frameHeight;
|
||||
|
||||
/**
|
||||
* The rotation (degree) of the last video frame rendered by
|
||||
* {@link #surfaceViewRenderer}.
|
||||
*/
|
||||
private int frameRotation;
|
||||
|
||||
/**
|
||||
* The width of the last video frame rendered by
|
||||
* {@link #surfaceViewRenderer}.
|
||||
*/
|
||||
private int frameWidth;
|
||||
|
||||
/**
|
||||
* The {@code Object} which synchronizes the access to the layout-related
|
||||
* state of this instance such as {@link #frameHeight},
|
||||
* {@link #frameRotation}, {@link #frameWidth}, and {@link #scalingType}.
|
||||
*/
|
||||
private final Object layoutSyncRoot = new Object();
|
||||
|
||||
/**
|
||||
* The indicator which determines whether this {@code WebRTCView} is to
|
||||
* mirror the video represented by {@link #videoTrack} during its rendering.
|
||||
*/
|
||||
private boolean mirror;
|
||||
|
||||
|
||||
/**
|
||||
* The {@code RendererEvents} which listens to rendering events reported by
|
||||
* {@link #surfaceViewRenderer}.
|
||||
*/
|
||||
private final RendererEvents rendererEvents
|
||||
= new RendererEvents() {
|
||||
@Override
|
||||
public void onFirstFrameRendered() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFrameResolutionChanged(
|
||||
int videoWidth, int videoHeight,
|
||||
int rotation) {
|
||||
MXWebRtcView.this.onFrameResolutionChanged(
|
||||
videoWidth, videoHeight,
|
||||
rotation);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The {@code Runnable} representation of
|
||||
* {@link #requestSurfaceViewRendererLayout()}. Explicitly defined in order
|
||||
* to allow the use of the latter with {@link #post(Runnable)} without
|
||||
* initializing new instances on every (method) call.
|
||||
*/
|
||||
private final Runnable requestSurfaceViewRendererLayoutRunnable
|
||||
= new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
requestSurfaceViewRendererLayout();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The scaling type this {@code WebRTCView} is to apply to the video
|
||||
* represented by {@link #videoTrack} during its rendering. An expression of
|
||||
* the CSS property {@code object-fit} in the terms of WebRTC.
|
||||
*/
|
||||
private ScalingType scalingType;
|
||||
|
||||
/**
|
||||
* The {@link View} and {@link VideoRenderer} implementation which
|
||||
* actually renders {@link #videoTrack} on behalf of this instance.
|
||||
*/
|
||||
private final SurfaceViewRenderer surfaceViewRenderer;
|
||||
|
||||
/**
|
||||
* The {@code VideoRenderer}, if any, which renders {@link #videoTrack} on
|
||||
* this {@code View}.
|
||||
*/
|
||||
private VideoRenderer videoRenderer;
|
||||
|
||||
/**
|
||||
* The {@code VideoTrack}, if any, rendered by this {@code MXWebRTCView}.
|
||||
*/
|
||||
private VideoTrack videoTrack;
|
||||
|
||||
public MXWebRtcView(Context context) {
|
||||
super(context);
|
||||
|
||||
surfaceViewRenderer = new SurfaceViewRenderer(context);
|
||||
addView(surfaceViewRenderer);
|
||||
|
||||
setMirror(false);
|
||||
setScalingType(DEFAULT_SCALING_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@code SurfaceViewRenderer} which renders {@link #videoTrack}.
|
||||
* Explicitly defined and used in order to facilitate switching the instance
|
||||
* at compile time. For example, reduces the number of modifications
|
||||
* necessary to switch the implementation from a {@code SurfaceViewRenderer}
|
||||
* that is a child of a {@code WebRTCView} to {@code WebRTCView} extending
|
||||
* {@code SurfaceViewRenderer}.
|
||||
*
|
||||
* @return The {@code SurfaceViewRenderer} which renders {@code videoTrack}.
|
||||
*/
|
||||
private final SurfaceViewRenderer getSurfaceViewRenderer() {
|
||||
return surfaceViewRenderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* If this <tt>View</tt> has {@link View#isInLayout()}, invokes it and
|
||||
* returns its return value; otherwise, returns <tt>false</tt> like
|
||||
* {@link ViewCompat#isInLayout(View)}.
|
||||
*
|
||||
* @return If this <tt>View</tt> has <tt>View#isInLayout()</tt>, invokes it
|
||||
* and returns its return value; otherwise, returns <tt>false</tt>.
|
||||
*/
|
||||
private boolean invokeIsInLayout() {
|
||||
Method m = IS_IN_LAYOUT;
|
||||
boolean b = false;
|
||||
|
||||
if (m != null) {
|
||||
try {
|
||||
b = (boolean) m.invoke(this);
|
||||
} catch (Throwable e) {
|
||||
// Fall back to the behavior of ViewCompat#isInLayout(View).
|
||||
}
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
try {
|
||||
// Generally, OpenGL is only necessary while this View is attached
|
||||
// to a window so there is no point in having the whole rendering
|
||||
// infrastructure hooked up while this View is not attached to a
|
||||
// window. Additionally, a memory leak was solved in a similar way
|
||||
// on iOS.
|
||||
tryAddRendererToVideoTrack();
|
||||
} finally {
|
||||
super.onAttachedToWindow();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
try {
|
||||
// Generally, OpenGL is only necessary while this View is attached
|
||||
// to a window so there is no point in having the whole rendering
|
||||
// infrastructure hooked up while this View is not attached to a
|
||||
// window. Additionally, a memory leak was solved in a similar way
|
||||
// on iOS.
|
||||
removeRendererFromVideoTrack();
|
||||
} finally {
|
||||
super.onDetachedFromWindow();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback fired by {@link #surfaceViewRenderer} when the resolution or
|
||||
* rotation of the frame it renders has changed.
|
||||
*
|
||||
* @param videoWidth The new width of the rendered video frame.
|
||||
* @param videoHeight The new height of the rendered video frame.
|
||||
* @param rotation The new rotation of the rendered video frame.
|
||||
*/
|
||||
private void onFrameResolutionChanged(int videoWidth,
|
||||
int videoHeight,
|
||||
int rotation) {
|
||||
boolean changed = false;
|
||||
|
||||
synchronized (layoutSyncRoot) {
|
||||
if (frameHeight != videoHeight) {
|
||||
frameHeight = videoHeight;
|
||||
changed = true;
|
||||
}
|
||||
if (frameRotation != rotation) {
|
||||
frameRotation = rotation;
|
||||
changed = true;
|
||||
}
|
||||
if (frameWidth != videoWidth) {
|
||||
frameWidth = videoWidth;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
// The onFrameResolutionChanged method call executes on the
|
||||
// surfaceViewRenderer's render Thread.
|
||||
post(requestSurfaceViewRendererLayoutRunnable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
int height = b - t;
|
||||
int width = r - l;
|
||||
|
||||
if (height == 0 || width == 0) {
|
||||
l = t = r = b = 0;
|
||||
} else {
|
||||
int frameHeight;
|
||||
int frameRotation;
|
||||
int frameWidth;
|
||||
ScalingType scalingType;
|
||||
|
||||
synchronized (layoutSyncRoot) {
|
||||
frameHeight = this.frameHeight;
|
||||
frameRotation = this.frameRotation;
|
||||
frameWidth = this.frameWidth;
|
||||
scalingType = this.scalingType;
|
||||
}
|
||||
|
||||
SurfaceViewRenderer surfaceViewRenderer = getSurfaceViewRenderer();
|
||||
|
||||
switch (scalingType) {
|
||||
case SCALE_ASPECT_FILL:
|
||||
// Fill this ViewGroup with surfaceViewRenderer and the latter
|
||||
// will take care of filling itself with the video similarly to
|
||||
// the cover value the CSS property object-fit.
|
||||
r = width;
|
||||
l = 0;
|
||||
b = height;
|
||||
t = 0;
|
||||
break;
|
||||
case SCALE_ASPECT_FIT:
|
||||
default:
|
||||
// Lay surfaceViewRenderer out inside this ViewGroup in accord
|
||||
// with the contain value of the CSS property object-fit.
|
||||
// SurfaceViewRenderer will fill itself with the video similarly
|
||||
// to the cover or contain value of the CSS property object-fit
|
||||
// (which will not matter, eventually).
|
||||
if (frameHeight == 0 || frameWidth == 0) {
|
||||
l = t = r = b = 0;
|
||||
} else {
|
||||
float frameAspectRatio
|
||||
= (frameRotation % 180 == 0)
|
||||
? frameWidth / (float) frameHeight
|
||||
: frameHeight / (float) frameWidth;
|
||||
Point frameDisplaySize
|
||||
= RendererCommon.getDisplaySize(
|
||||
scalingType,
|
||||
frameAspectRatio,
|
||||
width, height);
|
||||
|
||||
l = (width - frameDisplaySize.x) / 2;
|
||||
t = (height - frameDisplaySize.y) / 2;
|
||||
r = l + frameDisplaySize.x;
|
||||
b = t + frameDisplaySize.y;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
surfaceViewRenderer.layout(l, t, r, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops rendering {@link #videoTrack} and releases the associated acquired
|
||||
* resources (if rendering is in progress).
|
||||
*/
|
||||
private void removeRendererFromVideoTrack() {
|
||||
if (videoRenderer != null) {
|
||||
videoTrack.removeRenderer(videoRenderer);
|
||||
videoRenderer.dispose();
|
||||
videoRenderer = null;
|
||||
|
||||
getSurfaceViewRenderer().release();
|
||||
|
||||
// Since this WebRTCView is no longer rendering anything, make sure
|
||||
// surfaceViewRenderer displays nothing as well.
|
||||
synchronized (layoutSyncRoot) {
|
||||
frameHeight = 0;
|
||||
frameRotation = 0;
|
||||
frameWidth = 0;
|
||||
}
|
||||
requestSurfaceViewRendererLayout();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request that {@link #surfaceViewRenderer} be laid out (as soon as
|
||||
* possible) because layout-related state either of this instance or of
|
||||
* {@code surfaceViewRenderer} has changed.
|
||||
*/
|
||||
@SuppressLint("WrongCall")
|
||||
private void requestSurfaceViewRendererLayout() {
|
||||
// Google/WebRTC just call requestLayout() on surfaceViewRenderer when
|
||||
// they change the value of its mirror or surfaceType property.
|
||||
getSurfaceViewRenderer().requestLayout();
|
||||
// The above is not enough though when the video frame's dimensions or
|
||||
// rotation change. The following will suffice.
|
||||
if (!invokeIsInLayout()) {
|
||||
onLayout(
|
||||
/* changed */ false,
|
||||
getLeft(), getTop(), getRight(), getBottom());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the indicator which determines whether this {@code WebRTCView} is to
|
||||
* mirror the video represented by {@link #videoTrack} during its rendering.
|
||||
*
|
||||
* @param mirror If this {@code WebRTCView} is to mirror the video
|
||||
* represented by {@code videoTrack} during its rendering, {@code true};
|
||||
* otherwise, {@code false}.
|
||||
*/
|
||||
public void setMirror(boolean mirror) {
|
||||
if (this.mirror != mirror) {
|
||||
this.mirror = mirror;
|
||||
|
||||
SurfaceViewRenderer surfaceViewRenderer = getSurfaceViewRenderer();
|
||||
|
||||
surfaceViewRenderer.setMirror(mirror);
|
||||
// SurfaceViewRenderer takes the value of its mirror property into
|
||||
// account upon its layout.
|
||||
requestSurfaceViewRendererLayout();
|
||||
}
|
||||
}
|
||||
|
||||
private void setScalingType(ScalingType scalingType) {
|
||||
SurfaceViewRenderer surfaceViewRenderer;
|
||||
|
||||
synchronized (layoutSyncRoot) {
|
||||
if (this.scalingType == scalingType) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.scalingType = scalingType;
|
||||
|
||||
surfaceViewRenderer = getSurfaceViewRenderer();
|
||||
surfaceViewRenderer.setScalingType(scalingType);
|
||||
}
|
||||
// Both this instance ant its SurfaceViewRenderer take the value of
|
||||
// their scalingType properties into account upon their layouts.
|
||||
requestSurfaceViewRendererLayout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code MediaStream} to be rendered by this {@code WebRTCView}.
|
||||
* The implementation renders the first {@link VideoTrack}, if any, of the
|
||||
* specified {@code mediaStream}.
|
||||
*
|
||||
* @param mediaStream The {@code MediaStream} to be rendered by this
|
||||
* {@code WebRTCView} or {@code null}.
|
||||
*/
|
||||
public void setStream(MediaStream mediaStream) {
|
||||
VideoTrack videoTrack;
|
||||
|
||||
if (mediaStream == null) {
|
||||
videoTrack = null;
|
||||
} else {
|
||||
List<VideoTrack> videoTracks = mediaStream.videoTracks;
|
||||
|
||||
videoTrack = videoTracks.isEmpty() ? null : videoTracks.get(0);
|
||||
}
|
||||
|
||||
setVideoTrack(videoTrack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code VideoTrack} to be rendered by this {@code WebRTCView}.
|
||||
*
|
||||
* @param videoTrack The {@code VideoTrack} to be rendered by this
|
||||
* {@code WebRTCView} or {@code null}.
|
||||
*/
|
||||
private void setVideoTrack(VideoTrack videoTrack) {
|
||||
VideoTrack oldValue = this.videoTrack;
|
||||
|
||||
if (oldValue != videoTrack) {
|
||||
if (oldValue != null) {
|
||||
removeRendererFromVideoTrack();
|
||||
}
|
||||
|
||||
this.videoTrack = videoTrack;
|
||||
|
||||
if (videoTrack != null) {
|
||||
tryAddRendererToVideoTrack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the z-order of this {@link WebRTCView} in the stacking space of all
|
||||
* {@code WebRTCView}s. For more details, refer to the documentation of the
|
||||
* {@code zOrder} property of the JavaScript counterpart of
|
||||
* {@code WebRTCView} i.e. {@code RTCView}.
|
||||
*
|
||||
* @param zOrder The z-order to set on this {@code WebRTCView}.
|
||||
*/
|
||||
public void setZOrder(int zOrder) {
|
||||
SurfaceViewRenderer surfaceViewRenderer = getSurfaceViewRenderer();
|
||||
|
||||
switch (zOrder) {
|
||||
case 0:
|
||||
surfaceViewRenderer.setZOrderMediaOverlay(false);
|
||||
break;
|
||||
case 1:
|
||||
surfaceViewRenderer.setZOrderMediaOverlay(true);
|
||||
break;
|
||||
case 2:
|
||||
surfaceViewRenderer.setZOrderOnTop(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts rendering {@link #videoTrack} if rendering is not in progress and
|
||||
* all preconditions for the start of rendering are met.
|
||||
*/
|
||||
private void tryAddRendererToVideoTrack() {
|
||||
if (videoRenderer == null
|
||||
&& videoTrack != null
|
||||
&& ViewCompat.isAttachedToWindow(this)) {
|
||||
EglBase.Context sharedContext = EglUtils.getRootEglBaseContext();
|
||||
|
||||
if (sharedContext == null) {
|
||||
// If SurfaceViewRenderer#init() is invoked, it will throw a
|
||||
// RuntimeException which will very likely kill the application.
|
||||
Log.e(LOG_TAG, "Failed to render a VideoTrack!");
|
||||
return;
|
||||
}
|
||||
|
||||
SurfaceViewRenderer surfaceViewRenderer = getSurfaceViewRenderer();
|
||||
surfaceViewRenderer.init(sharedContext, rendererEvents);
|
||||
|
||||
videoRenderer = new VideoRenderer(surfaceViewRenderer);
|
||||
videoTrack.addRenderer(videoRenderer);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.call;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Defines the video call view layout.
|
||||
*/
|
||||
public class VideoLayoutConfiguration implements Serializable {
|
||||
public final static int INVALID_VALUE = -1;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "VideoLayoutConfiguration{" +
|
||||
"mIsPortrait=" + mIsPortrait +
|
||||
", X=" + mX +
|
||||
", Y=" + mY +
|
||||
", Width=" + mWidth +
|
||||
", Height=" + mHeight +
|
||||
'}';
|
||||
}
|
||||
|
||||
// parameters of the video of the local user (small video)
|
||||
/**
|
||||
* margin left in percentage of the screen resolution for the local user video
|
||||
**/
|
||||
public int mX;
|
||||
/**
|
||||
* margin top in percentage of the screen resolution for the local user video
|
||||
**/
|
||||
public int mY;
|
||||
|
||||
/**
|
||||
* width in percentage of the screen resolution for the local user video
|
||||
**/
|
||||
public int mWidth;
|
||||
/**
|
||||
* video height in percentage of the screen resolution for the local user video
|
||||
**/
|
||||
public int mHeight;
|
||||
|
||||
/**
|
||||
* the area size in which the video in displayed
|
||||
**/
|
||||
public int mDisplayWidth;
|
||||
public int mDisplayHeight;
|
||||
|
||||
/**
|
||||
* tells if the display in is a portrait orientation
|
||||
**/
|
||||
public boolean mIsPortrait;
|
||||
|
||||
public VideoLayoutConfiguration(int aX, int aY, int aWidth, int aHeight) {
|
||||
this(aX, aY, aWidth, aHeight, INVALID_VALUE, INVALID_VALUE);
|
||||
}
|
||||
|
||||
public VideoLayoutConfiguration(int aX, int aY, int aWidth, int aHeight, int aDisplayWidth, int aDisplayHeight) {
|
||||
mX = aX;
|
||||
mY = aY;
|
||||
mWidth = aWidth;
|
||||
mHeight = aHeight;
|
||||
mDisplayWidth = aDisplayWidth;
|
||||
mDisplayHeight = aDisplayHeight;
|
||||
}
|
||||
|
||||
public VideoLayoutConfiguration() {
|
||||
mX = INVALID_VALUE;
|
||||
mY = INVALID_VALUE;
|
||||
mWidth = INVALID_VALUE;
|
||||
mHeight = INVALID_VALUE;
|
||||
mDisplayWidth = INVALID_VALUE;
|
||||
mDisplayHeight = INVALID_VALUE;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.crypto.RoomKeyRequest;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.crypto.RoomKeyRequestBody;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* IncomingRoomKeyRequest class defines the incoming room keys request.
|
||||
*/
|
||||
public class IncomingRoomKeyRequest implements Serializable {
|
||||
/**
|
||||
* The user id
|
||||
*/
|
||||
public String mUserId;
|
||||
|
||||
/**
|
||||
* The device id
|
||||
*/
|
||||
public String mDeviceId;
|
||||
|
||||
/**
|
||||
* The request id
|
||||
*/
|
||||
public String mRequestId;
|
||||
|
||||
/**
|
||||
* The request body
|
||||
*/
|
||||
public RoomKeyRequestBody mRequestBody;
|
||||
|
||||
/**
|
||||
* The runnable to call to accept to share the keys
|
||||
*/
|
||||
public transient Runnable mShare;
|
||||
|
||||
/**
|
||||
* The runnable to call to ignore the key share request.
|
||||
*/
|
||||
public transient Runnable mIgnore;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param event the event
|
||||
*/
|
||||
public IncomingRoomKeyRequest(Event event) {
|
||||
mUserId = event.getSender();
|
||||
|
||||
RoomKeyRequest roomKeyRequest = JsonUtils.toRoomKeyRequest(event.getContentAsJsonObject());
|
||||
mDeviceId = roomKeyRequest.requesting_device_id;
|
||||
mRequestId = roomKeyRequest.request_id;
|
||||
mRequestBody = (null != roomKeyRequest.body) ? roomKeyRequest.body : new RoomKeyRequestBody();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
|
||||
/**
|
||||
* IncomingRoomKeyRequestCancellation describes the incoming room key cancellation.
|
||||
*/
|
||||
public class IncomingRoomKeyRequestCancellation extends IncomingRoomKeyRequest {
|
||||
|
||||
public IncomingRoomKeyRequestCancellation(Event event) {
|
||||
super(event);
|
||||
mRequestBody = null;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.crypto.algorithms.IMXDecrypting;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.algorithms.IMXEncrypting;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class MXCryptoAlgorithms {
|
||||
|
||||
private static final String LOG_TAG = MXCryptoAlgorithms.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Matrix algorithm tag for olm.
|
||||
*/
|
||||
public static final String MXCRYPTO_ALGORITHM_OLM = "m.olm.v1.curve25519-aes-sha2";
|
||||
|
||||
/**
|
||||
* Matrix algorithm tag for megolm.
|
||||
*/
|
||||
public static final String MXCRYPTO_ALGORITHM_MEGOLM = "m.megolm.v1.aes-sha2";
|
||||
|
||||
// encryptors map
|
||||
private final Map<String, Class<IMXEncrypting>> mEncryptors;
|
||||
|
||||
// decryptors map
|
||||
private final Map<String, Class<IMXDecrypting>> mDecryptors;
|
||||
|
||||
// shared instance
|
||||
private static MXCryptoAlgorithms mSharedInstance = null;
|
||||
|
||||
/**
|
||||
* @return the shared instance
|
||||
*/
|
||||
public static MXCryptoAlgorithms sharedAlgorithms() {
|
||||
if (null == mSharedInstance) {
|
||||
mSharedInstance = new MXCryptoAlgorithms();
|
||||
}
|
||||
|
||||
return mSharedInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private MXCryptoAlgorithms() {
|
||||
// encryptos
|
||||
mEncryptors = new HashMap<>();
|
||||
try {
|
||||
mEncryptors.put(MXCRYPTO_ALGORITHM_MEGOLM,
|
||||
(Class<IMXEncrypting>) Class.forName("org.matrix.androidsdk.crypto.algorithms.megolm.MXMegolmEncryption"));
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## MXCryptoAlgorithms() : fails to add MXCRYPTO_ALGORITHM_MEGOLM " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
try {
|
||||
mEncryptors.put(MXCRYPTO_ALGORITHM_OLM,
|
||||
(Class<IMXEncrypting>) Class.forName("org.matrix.androidsdk.crypto.algorithms.olm.MXOlmEncryption"));
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## MXCryptoAlgorithms() : fails to add MXCRYPTO_ALGORITHM_OLM " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
mDecryptors = new HashMap<>();
|
||||
try {
|
||||
mDecryptors.put(MXCRYPTO_ALGORITHM_MEGOLM,
|
||||
(Class<IMXDecrypting>) Class.forName("org.matrix.androidsdk.crypto.algorithms.megolm.MXMegolmDecryption"));
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## MXCryptoAlgorithms() : fails to add MXCRYPTO_ALGORITHM_MEGOLM " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
try {
|
||||
mDecryptors.put(MXCRYPTO_ALGORITHM_OLM,
|
||||
(Class<IMXDecrypting>) Class.forName("org.matrix.androidsdk.crypto.algorithms.olm.MXOlmDecryption"));
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## MXCryptoAlgorithms() : fails to add MXCRYPTO_ALGORITHM_OLM " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the class implementing encryption for the provided algorithm.
|
||||
*
|
||||
* @param algorithm the algorithm tag.
|
||||
* @return A class implementing 'IMXEncrypting'.
|
||||
*/
|
||||
public Class<IMXEncrypting> encryptorClassForAlgorithm(String algorithm) {
|
||||
if (!TextUtils.isEmpty(algorithm)) {
|
||||
return mEncryptors.get(algorithm);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the class implementing decryption for the provided algorithm.
|
||||
*
|
||||
* @param algorithm the algorithm tag.
|
||||
* @return A class implementing 'IMXDecrypting'.
|
||||
*/
|
||||
|
||||
public Class<IMXDecrypting> decryptorClassForAlgorithm(String algorithm) {
|
||||
if (!TextUtils.isEmpty(algorithm)) {
|
||||
return mDecryptors.get(algorithm);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The list of registered algorithms.
|
||||
*/
|
||||
public List<String> supportedAlgorithms() {
|
||||
return new ArrayList<>(mEncryptors.keySet());
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.internal.legacy.crypto;
|
||||
|
||||
/**
|
||||
* Class to define the parameters used to customize or configure the end-to-end crypto.
|
||||
*/
|
||||
public class MXCryptoConfig {
|
||||
// Tell whether the encryption of the event content is enabled for the invited members.
|
||||
// By default, we encrypt messages only for the joined members.
|
||||
// The encryption for the invited members will be blocked if the history visibility is "joined".
|
||||
public boolean mEnableEncryptionForInvitedMembers = false;
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
|
||||
|
||||
/**
|
||||
* Represents a standard error response.
|
||||
*/
|
||||
public class MXCryptoError extends MatrixError {
|
||||
|
||||
/**
|
||||
* Error codes
|
||||
*/
|
||||
public static final String ENCRYPTING_NOT_ENABLED_ERROR_CODE = "ENCRYPTING_NOT_ENABLED";
|
||||
public static final String UNABLE_TO_ENCRYPT_ERROR_CODE = "UNABLE_TO_ENCRYPT";
|
||||
public static final String UNABLE_TO_DECRYPT_ERROR_CODE = "UNABLE_TO_DECRYPT";
|
||||
public static final String UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE = "UNKNOWN_INBOUND_SESSION_ID";
|
||||
public static final String INBOUND_SESSION_MISMATCH_ROOM_ID_ERROR_CODE = "INBOUND_SESSION_MISMATCH_ROOM_ID";
|
||||
public static final String MISSING_FIELDS_ERROR_CODE = "MISSING_FIELDS";
|
||||
public static final String MISSING_CIPHER_TEXT_ERROR_CODE = "MISSING_CIPHER_TEXT";
|
||||
public static final String NOT_INCLUDE_IN_RECIPIENTS_ERROR_CODE = "NOT_INCLUDE_IN_RECIPIENTS";
|
||||
public static final String BAD_RECIPIENT_ERROR_CODE = "BAD_RECIPIENT";
|
||||
public static final String BAD_RECIPIENT_KEY_ERROR_CODE = "BAD_RECIPIENT_KEY";
|
||||
public static final String FORWARDED_MESSAGE_ERROR_CODE = "FORWARDED_MESSAGE";
|
||||
public static final String BAD_ROOM_ERROR_CODE = "BAD_ROOM";
|
||||
public static final String BAD_ENCRYPTED_MESSAGE_ERROR_CODE = "BAD_ENCRYPTED_MESSAGE";
|
||||
public static final String DUPLICATED_MESSAGE_INDEX_ERROR_CODE = "DUPLICATED_MESSAGE_INDEX";
|
||||
public static final String MISSING_PROPERTY_ERROR_CODE = "MISSING_PROPERTY";
|
||||
public static final String OLM_ERROR_CODE = "OLM_ERROR_CODE";
|
||||
public static final String UNKNOWN_DEVICES_CODE = "UNKNOWN_DEVICES_CODE";
|
||||
|
||||
/**
|
||||
* short error reasons
|
||||
*/
|
||||
public static final String UNABLE_TO_DECRYPT = "Unable to decrypt";
|
||||
public static final String UNABLE_TO_ENCRYPT = "Unable to encrypt";
|
||||
|
||||
/**
|
||||
* Detailed error reasons
|
||||
*/
|
||||
public static final String ENCRYPTING_NOT_ENABLED_REASON = "Encryption not enabled";
|
||||
public static final String UNABLE_TO_ENCRYPT_REASON = "Unable to encrypt %s";
|
||||
public static final String UNABLE_TO_DECRYPT_REASON = "Unable to decrypt %1$s. Algorithm: %2$s";
|
||||
public static final String OLM_REASON = "OLM error: %1$s";
|
||||
public static final String DETAILLED_OLM_REASON = "Unable to decrypt %1$s. OLM error: %2$s";
|
||||
public static final String UNKNOWN_INBOUND_SESSION_ID_REASON = "Unknown inbound session id";
|
||||
public static final String INBOUND_SESSION_MISMATCH_ROOM_ID_REASON = "Mismatched room_id for inbound group session (expected %1$s, was %2$s)";
|
||||
public static final String MISSING_FIELDS_REASON = "Missing fields in input";
|
||||
public static final String MISSING_CIPHER_TEXT_REASON = "Missing ciphertext";
|
||||
public static final String NOT_INCLUDED_IN_RECIPIENT_REASON = "Not included in recipients";
|
||||
public static final String BAD_RECIPIENT_REASON = "Message was intended for %1$s";
|
||||
public static final String BAD_RECIPIENT_KEY_REASON = "Message not intended for this device";
|
||||
public static final String FORWARDED_MESSAGE_REASON = "Message forwarded from %1$s";
|
||||
public static final String BAD_ROOM_REASON = "Message intended for room %1$s";
|
||||
public static final String BAD_ENCRYPTED_MESSAGE_REASON = "Bad Encrypted Message";
|
||||
public static final String DUPLICATE_MESSAGE_INDEX_REASON = "Duplicate message index, possible replay attack %1$s";
|
||||
public static final String ERROR_MISSING_PROPERTY_REASON = "No '%1$s' property. Cannot prevent unknown-key attack";
|
||||
public static final String UNKNOWN_DEVICES_REASON = "This room contains unknown devices which have not been verified.\n" +
|
||||
"We strongly recommend you verify them before continuing.";
|
||||
public static final String NO_MORE_ALGORITHM_REASON = "Room was previously configured to use encryption, but is no longer." +
|
||||
" Perhaps the homeserver is hiding the configuration event.";
|
||||
|
||||
/**
|
||||
* Describe the error with more details
|
||||
*/
|
||||
private String mDetailedErrorDescription = null;
|
||||
|
||||
/**
|
||||
* Data exception.
|
||||
* Some exceptions provide some data to describe the exception
|
||||
*/
|
||||
public Object mExceptionData = null;
|
||||
|
||||
/**
|
||||
* Create a crypto error
|
||||
*
|
||||
* @param code the error code (see XX_ERROR_CODE)
|
||||
* @param shortErrorDescription the short error description
|
||||
* @param detailedErrorDescription the detailed error description
|
||||
*/
|
||||
public MXCryptoError(String code, String shortErrorDescription, String detailedErrorDescription) {
|
||||
errcode = code;
|
||||
error = shortErrorDescription;
|
||||
mDetailedErrorDescription = detailedErrorDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a crypto error
|
||||
*
|
||||
* @param code the error code (see XX_ERROR_CODE)
|
||||
* @param shortErrorDescription the short error description
|
||||
* @param detailedErrorDescription the detailed error description
|
||||
* @param exceptionData the exception data
|
||||
*/
|
||||
public MXCryptoError(String code, String shortErrorDescription, String detailedErrorDescription, Object exceptionData) {
|
||||
errcode = code;
|
||||
error = shortErrorDescription;
|
||||
mDetailedErrorDescription = detailedErrorDescription;
|
||||
mExceptionData = exceptionData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the current error is an olm one.
|
||||
*/
|
||||
public boolean isOlmError() {
|
||||
return TextUtils.equals(OLM_ERROR_CODE, errcode);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return the detailed error description
|
||||
*/
|
||||
public String getDetailedErrorDescription() {
|
||||
if (TextUtils.isEmpty(mDetailedErrorDescription)) {
|
||||
return error;
|
||||
}
|
||||
|
||||
return mDetailedErrorDescription;
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto;
|
||||
|
||||
/**
|
||||
* This class represents a decryption exception
|
||||
*/
|
||||
public class MXDecryptionException extends Exception {
|
||||
|
||||
/**
|
||||
* Describe the decryption error.
|
||||
*/
|
||||
private MXCryptoError mCryptoError;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param cryptoError the linked crypto error
|
||||
*/
|
||||
public MXDecryptionException(MXCryptoError cryptoError) {
|
||||
mCryptoError = cryptoError;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the linked crypto error
|
||||
*/
|
||||
public MXCryptoError getCryptoError() {
|
||||
return mCryptoError;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
if (null != mCryptoError) {
|
||||
return mCryptoError.getMessage();
|
||||
}
|
||||
|
||||
return super.getMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLocalizedMessage() {
|
||||
if (null != mCryptoError) {
|
||||
return mCryptoError.getLocalizedMessage();
|
||||
}
|
||||
return super.getLocalizedMessage();
|
||||
}
|
||||
}
|
@ -0,0 +1,835 @@
|
||||
/*
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.MXPatterns;
|
||||
import im.vector.matrix.android.internal.legacy.MXSession;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXDeviceInfo;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXUsersDevicesMap;
|
||||
import im.vector.matrix.android.internal.legacy.data.cryptostore.IMXCryptoStore;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.crypto.KeysQueryResponse;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class MXDeviceList {
|
||||
private static final String LOG_TAG = MXDeviceList.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* State transition diagram for DeviceList.deviceTrackingStatus
|
||||
* <p>
|
||||
* |
|
||||
* stopTrackingDeviceList V
|
||||
* +---------------------> NOT_TRACKED
|
||||
* | |
|
||||
* +<--------------------+ | startTrackingDeviceList
|
||||
* | | V
|
||||
* | +-------------> PENDING_DOWNLOAD <--------------------+-+
|
||||
* | | ^ | | |
|
||||
* | | restart download | | start download | | invalidateUserDeviceList
|
||||
* | | client failed | | | |
|
||||
* | | | V | |
|
||||
* | +------------ DOWNLOAD_IN_PROGRESS -------------------+ |
|
||||
* | | | |
|
||||
* +<-------------------+ | download successful |
|
||||
* ^ V |
|
||||
* +----------------------- UP_TO_DATE ------------------------+
|
||||
**/
|
||||
|
||||
public static final int TRACKING_STATUS_NOT_TRACKED = -1;
|
||||
public static final int TRACKING_STATUS_PENDING_DOWNLOAD = 1;
|
||||
public static final int TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2;
|
||||
public static final int TRACKING_STATUS_UP_TO_DATE = 3;
|
||||
public static final int TRACKING_STATUS_UNREACHABLE_SERVER = 4;
|
||||
|
||||
// keys in progress
|
||||
private final Set<String> mUserKeyDownloadsInProgress = new HashSet<>();
|
||||
|
||||
// HS not ready for retry
|
||||
private final Set<String> mNotReadyToRetryHS = new HashSet<>();
|
||||
|
||||
// indexed by UserId
|
||||
private final Map<String, String> mPendingDownloadKeysRequestToken = new HashMap<>();
|
||||
|
||||
// download keys queue
|
||||
class DownloadKeysPromise {
|
||||
// list of remain pending device keys
|
||||
final List<String> mPendingUserIdsList;
|
||||
|
||||
// the unfiltered user ids list
|
||||
final List<String> mUserIdsList;
|
||||
|
||||
// the request callback
|
||||
final ApiCallback<MXUsersDevicesMap<MXDeviceInfo>> mCallback;
|
||||
|
||||
/**
|
||||
* Creator
|
||||
*
|
||||
* @param userIds the user ids list
|
||||
* @param callback the asynchronous callback
|
||||
*/
|
||||
DownloadKeysPromise(List<String> userIds, ApiCallback<MXUsersDevicesMap<MXDeviceInfo>> callback) {
|
||||
mPendingUserIdsList = new ArrayList<>(userIds);
|
||||
mUserIdsList = new ArrayList<>(userIds);
|
||||
mCallback = callback;
|
||||
}
|
||||
}
|
||||
|
||||
// pending queues list
|
||||
private final List<DownloadKeysPromise> mDownloadKeysQueues = new ArrayList<>();
|
||||
|
||||
private final MXCrypto mxCrypto;
|
||||
|
||||
private final MXSession mxSession;
|
||||
|
||||
private final IMXCryptoStore mCryptoStore;
|
||||
|
||||
// tells if there is a download keys request in progress
|
||||
private boolean mIsDownloadingKeys = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param session the session
|
||||
* @param crypto the crypto session
|
||||
*/
|
||||
public MXDeviceList(MXSession session, MXCrypto crypto) {
|
||||
mxSession = session;
|
||||
mxCrypto = crypto;
|
||||
mCryptoStore = crypto.getCryptoStore();
|
||||
|
||||
boolean isUpdated = false;
|
||||
|
||||
Map<String, Integer> deviceTrackingStatuses = mCryptoStore.getDeviceTrackingStatuses();
|
||||
for (String userId : deviceTrackingStatuses.keySet()) {
|
||||
int status = deviceTrackingStatuses.get(userId);
|
||||
|
||||
if ((TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == status) || (TRACKING_STATUS_UNREACHABLE_SERVER == status)) {
|
||||
// if a download was in progress when we got shut down, it isn't any more.
|
||||
deviceTrackingStatuses.put(userId, TRACKING_STATUS_PENDING_DOWNLOAD);
|
||||
isUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isUpdated) {
|
||||
mCryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the key downloads should be tried
|
||||
*
|
||||
* @param userId the userId
|
||||
* @return true if the keys download can be retrieved
|
||||
*/
|
||||
private boolean canRetryKeysDownload(String userId) {
|
||||
boolean res = false;
|
||||
|
||||
if (!TextUtils.isEmpty(userId) && userId.contains(":")) {
|
||||
try {
|
||||
synchronized (mNotReadyToRetryHS) {
|
||||
res = !mNotReadyToRetryHS.contains(userId.substring(userId.lastIndexOf(":") + 1));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## canRetryKeysDownload() failed : " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a download keys promise
|
||||
*
|
||||
* @param userIds the user ids list
|
||||
* @param callback the asynchronous callback
|
||||
* @return the filtered user ids list i.e the one which require a remote request
|
||||
*/
|
||||
private List<String> addDownloadKeysPromise(List<String> userIds, ApiCallback<MXUsersDevicesMap<MXDeviceInfo>> callback) {
|
||||
if (null != userIds) {
|
||||
List<String> filteredUserIds = new ArrayList<>();
|
||||
List<String> invalidUserIds = new ArrayList<>();
|
||||
|
||||
for (String userId : userIds) {
|
||||
if (MXPatterns.isUserId(userId)) {
|
||||
filteredUserIds.add(userId);
|
||||
} else {
|
||||
Log.e(LOG_TAG, "## userId " + userId + "is not a valid user id");
|
||||
invalidUserIds.add(userId);
|
||||
}
|
||||
}
|
||||
|
||||
synchronized (mUserKeyDownloadsInProgress) {
|
||||
filteredUserIds.removeAll(mUserKeyDownloadsInProgress);
|
||||
mUserKeyDownloadsInProgress.addAll(userIds);
|
||||
// got some email addresses instead of matrix ids
|
||||
mUserKeyDownloadsInProgress.removeAll(invalidUserIds);
|
||||
userIds.removeAll(invalidUserIds);
|
||||
}
|
||||
|
||||
mDownloadKeysQueues.add(new DownloadKeysPromise(userIds, callback));
|
||||
|
||||
return filteredUserIds;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the unavailable server lists
|
||||
*/
|
||||
private void clearUnavailableServersList() {
|
||||
synchronized (mNotReadyToRetryHS) {
|
||||
mNotReadyToRetryHS.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the cached device list for the given user outdated
|
||||
* flag the given user for device-list tracking, if they are not already.
|
||||
*
|
||||
* @param userIds the user ids list
|
||||
*/
|
||||
public void startTrackingDeviceList(List<String> userIds) {
|
||||
if (null != userIds) {
|
||||
boolean isUpdated = false;
|
||||
Map<String, Integer> deviceTrackingStatuses = mCryptoStore.getDeviceTrackingStatuses();
|
||||
|
||||
for (String userId : userIds) {
|
||||
if (!deviceTrackingStatuses.containsKey(userId) || (TRACKING_STATUS_NOT_TRACKED == deviceTrackingStatuses.get(userId))) {
|
||||
Log.d(LOG_TAG, "## startTrackingDeviceList() : Now tracking device list for " + userId);
|
||||
deviceTrackingStatuses.put(userId, TRACKING_STATUS_PENDING_DOWNLOAD);
|
||||
isUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isUpdated) {
|
||||
mCryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the devices list statuses
|
||||
*
|
||||
* @param changed the user ids list which have new devices
|
||||
* @param left the user ids list which left a room
|
||||
*/
|
||||
public void handleDeviceListsChanges(List<String> changed, List<String> left) {
|
||||
boolean isUpdated = false;
|
||||
Map<String, Integer> deviceTrackingStatuses = mCryptoStore.getDeviceTrackingStatuses();
|
||||
|
||||
if ((null != changed) && (0 != changed.size())) {
|
||||
clearUnavailableServersList();
|
||||
|
||||
for (String userId : changed) {
|
||||
if (deviceTrackingStatuses.containsKey(userId)) {
|
||||
Log.d(LOG_TAG, "## invalidateUserDeviceList() : Marking device list outdated for " + userId);
|
||||
deviceTrackingStatuses.put(userId, TRACKING_STATUS_PENDING_DOWNLOAD);
|
||||
isUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((null != left) && (0 != left.size())) {
|
||||
clearUnavailableServersList();
|
||||
|
||||
for (String userId : left) {
|
||||
if (deviceTrackingStatuses.containsKey(userId)) {
|
||||
Log.d(LOG_TAG, "## invalidateUserDeviceList() : No longer tracking device list for " + userId);
|
||||
deviceTrackingStatuses.put(userId, TRACKING_STATUS_NOT_TRACKED);
|
||||
isUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isUpdated) {
|
||||
mCryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This will flag each user whose devices we are tracking as in need of an
|
||||
* + update
|
||||
*/
|
||||
public void invalidateAllDeviceLists() {
|
||||
handleDeviceListsChanges(new ArrayList<>(mCryptoStore.getDeviceTrackingStatuses().keySet()), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* The keys download failed
|
||||
*
|
||||
* @param userIds the user ids list
|
||||
*/
|
||||
private void onKeysDownloadFailed(final List<String> userIds) {
|
||||
if (null != userIds) {
|
||||
synchronized (mUserKeyDownloadsInProgress) {
|
||||
Map<String, Integer> deviceTrackingStatuses = mCryptoStore.getDeviceTrackingStatuses();
|
||||
|
||||
for (String userId : userIds) {
|
||||
mUserKeyDownloadsInProgress.remove(userId);
|
||||
deviceTrackingStatuses.put(userId, TRACKING_STATUS_PENDING_DOWNLOAD);
|
||||
}
|
||||
|
||||
mCryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses);
|
||||
}
|
||||
}
|
||||
|
||||
mIsDownloadingKeys = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The keys download succeeded.
|
||||
*
|
||||
* @param userIds the userIds list
|
||||
* @param failures the failure map.
|
||||
*/
|
||||
private void onKeysDownloadSucceed(List<String> userIds, Map<String, Map<String, Object>> failures) {
|
||||
if (null != failures) {
|
||||
Set<String> keys = failures.keySet();
|
||||
|
||||
for (String k : keys) {
|
||||
Map<String, Object> value = failures.get(k);
|
||||
|
||||
if (value.containsKey("status")) {
|
||||
Object statusCodeAsVoid = value.get("status");
|
||||
int statusCode = 0;
|
||||
|
||||
if (statusCodeAsVoid instanceof Double) {
|
||||
statusCode = ((Double) statusCodeAsVoid).intValue();
|
||||
} else if (statusCodeAsVoid instanceof Integer) {
|
||||
statusCode = ((Integer) statusCodeAsVoid).intValue();
|
||||
}
|
||||
|
||||
if (statusCode == 503) {
|
||||
synchronized (mNotReadyToRetryHS) {
|
||||
mNotReadyToRetryHS.add(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Integer> deviceTrackingStatuses = mCryptoStore.getDeviceTrackingStatuses();
|
||||
|
||||
if (null != userIds) {
|
||||
if (mDownloadKeysQueues.size() > 0) {
|
||||
List<DownloadKeysPromise> promisesToRemove = new ArrayList<>();
|
||||
|
||||
for (DownloadKeysPromise promise : mDownloadKeysQueues) {
|
||||
promise.mPendingUserIdsList.removeAll(userIds);
|
||||
|
||||
if (promise.mPendingUserIdsList.size() == 0) {
|
||||
// private members
|
||||
final MXUsersDevicesMap<MXDeviceInfo> usersDevicesInfoMap = new MXUsersDevicesMap<>();
|
||||
|
||||
for (String userId : promise.mUserIdsList) {
|
||||
Map<String, MXDeviceInfo> devices = mCryptoStore.getUserDevices(userId);
|
||||
if (null == devices) {
|
||||
if (canRetryKeysDownload(userId)) {
|
||||
deviceTrackingStatuses.put(userId, TRACKING_STATUS_PENDING_DOWNLOAD);
|
||||
Log.e(LOG_TAG, "failed to retry the devices of " + userId + " : retry later");
|
||||
} else {
|
||||
if (deviceTrackingStatuses.containsKey(userId)
|
||||
&& (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == deviceTrackingStatuses.get(userId))) {
|
||||
deviceTrackingStatuses.put(userId, TRACKING_STATUS_UNREACHABLE_SERVER);
|
||||
Log.e(LOG_TAG, "failed to retry the devices of " + userId + " : the HS is not available");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (deviceTrackingStatuses.containsKey(userId)
|
||||
&& (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == deviceTrackingStatuses.get(userId))) {
|
||||
// we didn't get any new invalidations since this download started:
|
||||
// this user's device list is now up to date.
|
||||
deviceTrackingStatuses.put(userId, TRACKING_STATUS_UP_TO_DATE);
|
||||
Log.d(LOG_TAG, "Device list for " + userId + " now up to date");
|
||||
}
|
||||
|
||||
// And the response result
|
||||
usersDevicesInfoMap.setObjects(devices, userId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!mxCrypto.hasBeenReleased()) {
|
||||
final ApiCallback<MXUsersDevicesMap<MXDeviceInfo>> callback = promise.mCallback;
|
||||
|
||||
if (null != callback) {
|
||||
mxCrypto.getUIHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onSuccess(usersDevicesInfoMap);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
promisesToRemove.add(promise);
|
||||
}
|
||||
}
|
||||
mDownloadKeysQueues.removeAll(promisesToRemove);
|
||||
}
|
||||
|
||||
for (String userId : userIds) {
|
||||
mUserKeyDownloadsInProgress.remove(userId);
|
||||
}
|
||||
|
||||
mCryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses);
|
||||
}
|
||||
|
||||
mIsDownloadingKeys = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the device keys for a list of users and stores the keys in the MXStore.
|
||||
* It must be called in getEncryptingThreadHandler() thread.
|
||||
* The callback is called in the UI thread.
|
||||
*
|
||||
* @param userIds The users to fetch.
|
||||
* @param forceDownload Always download the keys even if cached.
|
||||
* @param callback the asynchronous callback
|
||||
*/
|
||||
public void downloadKeys(List<String> userIds, boolean forceDownload, final ApiCallback<MXUsersDevicesMap<MXDeviceInfo>> callback) {
|
||||
Log.d(LOG_TAG, "## downloadKeys() : forceDownload " + forceDownload + " : " + userIds);
|
||||
|
||||
// Map from userid -> deviceid -> DeviceInfo
|
||||
final MXUsersDevicesMap<MXDeviceInfo> stored = new MXUsersDevicesMap<>();
|
||||
|
||||
// List of user ids we need to download keys for
|
||||
final List<String> downloadUsers = new ArrayList<>();
|
||||
|
||||
if (null != userIds) {
|
||||
if (forceDownload) {
|
||||
downloadUsers.addAll(userIds);
|
||||
} else {
|
||||
for (String userId : userIds) {
|
||||
Integer status = mCryptoStore.getDeviceTrackingStatus(userId, TRACKING_STATUS_NOT_TRACKED);
|
||||
|
||||
// downloading keys ->the keys download won't be triggered twice but the callback requires the dedicated keys
|
||||
// not yet retrieved
|
||||
if (mUserKeyDownloadsInProgress.contains(userId)
|
||||
|| ((TRACKING_STATUS_UP_TO_DATE != status) && (TRACKING_STATUS_UNREACHABLE_SERVER != status))) {
|
||||
downloadUsers.add(userId);
|
||||
} else {
|
||||
Map<String, MXDeviceInfo> devices = mCryptoStore.getUserDevices(userId);
|
||||
|
||||
// should always be true
|
||||
if (null != devices) {
|
||||
stored.setObjects(devices, userId);
|
||||
} else {
|
||||
downloadUsers.add(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (0 == downloadUsers.size()) {
|
||||
Log.d(LOG_TAG, "## downloadKeys() : no new user device");
|
||||
|
||||
if (null != callback) {
|
||||
mxCrypto.getUIHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onSuccess(stored);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Log.d(LOG_TAG, "## downloadKeys() : starts");
|
||||
final long t0 = System.currentTimeMillis();
|
||||
|
||||
doKeyDownloadForUsers(downloadUsers, new ApiCallback<MXUsersDevicesMap<MXDeviceInfo>>() {
|
||||
public void onSuccess(MXUsersDevicesMap<MXDeviceInfo> usersDevicesInfoMap) {
|
||||
Log.d(LOG_TAG, "## downloadKeys() : doKeyDownloadForUsers succeeds after " + (System.currentTimeMillis() - t0) + " ms");
|
||||
|
||||
usersDevicesInfoMap.addEntriesFromMap(stored);
|
||||
|
||||
if (null != callback) {
|
||||
callback.onSuccess(usersDevicesInfoMap);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
Log.e(LOG_TAG, "## downloadKeys() : doKeyDownloadForUsers onNetworkError " + e.getMessage(), e);
|
||||
if (null != callback) {
|
||||
callback.onNetworkError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
Log.e(LOG_TAG, "## downloadKeys() : doKeyDownloadForUsers onMatrixError " + e.getMessage());
|
||||
if (null != callback) {
|
||||
callback.onMatrixError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
Log.e(LOG_TAG, "## downloadKeys() : doKeyDownloadForUsers onUnexpectedError " + e.getMessage(), e);
|
||||
if (null != callback) {
|
||||
callback.onUnexpectedError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the devices keys for a set of users.
|
||||
* It must be called in getEncryptingThreadHandler() thread.
|
||||
* The callback is called in the UI thread.
|
||||
*
|
||||
* @param downloadUsers the user ids list
|
||||
* @param callback the asynchronous callback
|
||||
*/
|
||||
private void doKeyDownloadForUsers(final List<String> downloadUsers, final ApiCallback<MXUsersDevicesMap<MXDeviceInfo>> callback) {
|
||||
Log.d(LOG_TAG, "## doKeyDownloadForUsers() : doKeyDownloadForUsers " + downloadUsers);
|
||||
|
||||
// get the user ids which did not already trigger a keys download
|
||||
final List<String> filteredUsers = addDownloadKeysPromise(downloadUsers, callback);
|
||||
|
||||
// if there is no new keys request
|
||||
if (0 == filteredUsers.size()) {
|
||||
// trigger nothing
|
||||
return;
|
||||
}
|
||||
|
||||
// sanity check
|
||||
if ((null == mxSession.getDataHandler()) || (null == mxSession.getDataHandler().getStore())) {
|
||||
return;
|
||||
}
|
||||
|
||||
mIsDownloadingKeys = true;
|
||||
|
||||
// track the race condition while sending requests
|
||||
// we defines a tag for each request
|
||||
// and test if the response is the latest request one
|
||||
final String downloadToken = filteredUsers.hashCode() + " " + System.currentTimeMillis();
|
||||
|
||||
for (String userId : filteredUsers) {
|
||||
mPendingDownloadKeysRequestToken.put(userId, downloadToken);
|
||||
}
|
||||
|
||||
mxSession.getCryptoRestClient()
|
||||
.downloadKeysForUsers(filteredUsers, mxSession.getDataHandler().getStore().getEventStreamToken(), new ApiCallback<KeysQueryResponse>() {
|
||||
@Override
|
||||
public void onSuccess(final KeysQueryResponse keysQueryResponse) {
|
||||
mxCrypto.getEncryptingThreadHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Log.d(LOG_TAG, "## doKeyDownloadForUsers() : Got keys for " + filteredUsers.size() + " users");
|
||||
MXDeviceInfo myDevice = mxCrypto.getMyDevice();
|
||||
IMXCryptoStore cryptoStore = mxCrypto.getCryptoStore();
|
||||
|
||||
List<String> userIdsList = new ArrayList<>(filteredUsers);
|
||||
|
||||
for (String userId : userIdsList) {
|
||||
// test if the response is the latest request one
|
||||
if (!TextUtils.equals(mPendingDownloadKeysRequestToken.get(userId), downloadToken)) {
|
||||
Log.e(LOG_TAG, "## doKeyDownloadForUsers() : Another update in the queue for "
|
||||
+ userId + " not marking up-to-date");
|
||||
filteredUsers.remove(userId);
|
||||
} else {
|
||||
Map<String, MXDeviceInfo> devices = keysQueryResponse.deviceKeys.get(userId);
|
||||
|
||||
Log.d(LOG_TAG, "## doKeyDownloadForUsers() : Got keys for " + userId + " : " + devices);
|
||||
|
||||
if (null != devices) {
|
||||
Map<String, MXDeviceInfo> mutableDevices = new HashMap<>(devices);
|
||||
List<String> deviceIds = new ArrayList<>(mutableDevices.keySet());
|
||||
|
||||
for (String deviceId : deviceIds) {
|
||||
// the user has been logged out
|
||||
if (null == cryptoStore) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Get the potential previously store device keys for this device
|
||||
MXDeviceInfo previouslyStoredDeviceKeys = cryptoStore.getUserDevice(deviceId, userId);
|
||||
MXDeviceInfo deviceInfo = mutableDevices.get(deviceId);
|
||||
|
||||
// in some race conditions (like unit tests)
|
||||
// the self device must be seen as verified
|
||||
if (TextUtils.equals(deviceInfo.deviceId, myDevice.deviceId)
|
||||
&& TextUtils.equals(userId, myDevice.userId)) {
|
||||
deviceInfo.mVerified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED;
|
||||
}
|
||||
|
||||
// Validate received keys
|
||||
if (!validateDeviceKeys(deviceInfo, userId, deviceId, previouslyStoredDeviceKeys)) {
|
||||
// New device keys are not valid. Do not store them
|
||||
mutableDevices.remove(deviceId);
|
||||
|
||||
if (null != previouslyStoredDeviceKeys) {
|
||||
// But keep old validated ones if any
|
||||
mutableDevices.put(deviceId, previouslyStoredDeviceKeys);
|
||||
}
|
||||
} else if (null != previouslyStoredDeviceKeys) {
|
||||
// The verified status is not sync'ed with hs.
|
||||
// This is a client side information, valid only for this client.
|
||||
// So, transfer its previous value
|
||||
mutableDevices.get(deviceId).mVerified = previouslyStoredDeviceKeys.mVerified;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the store
|
||||
// Note that devices which aren't in the response will be removed from the stores
|
||||
cryptoStore.storeUserDevices(userId, mutableDevices);
|
||||
}
|
||||
|
||||
// the response is the latest request one
|
||||
mPendingDownloadKeysRequestToken.remove(userId);
|
||||
}
|
||||
}
|
||||
|
||||
onKeysDownloadSucceed(filteredUsers, keysQueryResponse.failures);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onFailed() {
|
||||
mxCrypto.getEncryptingThreadHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
List<String> userIdsList = new ArrayList<>(filteredUsers);
|
||||
|
||||
// test if the response is the latest request one
|
||||
for (String userId : userIdsList) {
|
||||
if (!TextUtils.equals(mPendingDownloadKeysRequestToken.get(userId), downloadToken)) {
|
||||
Log.e(LOG_TAG, "## doKeyDownloadForUsers() : Another update in the queue for " + userId + " not marking up-to-date");
|
||||
filteredUsers.remove(userId);
|
||||
} else {
|
||||
// the response is the latest request one
|
||||
mPendingDownloadKeysRequestToken.remove(userId);
|
||||
}
|
||||
}
|
||||
|
||||
onKeysDownloadFailed(filteredUsers);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
Log.e(LOG_TAG, "##doKeyDownloadForUsers() : onNetworkError " + e.getMessage(), e);
|
||||
|
||||
onFailed();
|
||||
|
||||
if (null != callback) {
|
||||
callback.onNetworkError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
Log.e(LOG_TAG, "##doKeyDownloadForUsers() : onMatrixError " + e.getMessage());
|
||||
|
||||
onFailed();
|
||||
|
||||
if (null != callback) {
|
||||
callback.onMatrixError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
Log.e(LOG_TAG, "##doKeyDownloadForUsers() : onUnexpectedError " + e.getMessage(), e);
|
||||
|
||||
onFailed();
|
||||
|
||||
if (null != callback) {
|
||||
callback.onUnexpectedError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate device keys.
|
||||
* This method must called on getEncryptingThreadHandler() thread.
|
||||
*
|
||||
* @param deviceKeys the device keys to validate.
|
||||
* @param userId the id of the user of the device.
|
||||
* @param deviceId the id of the device.
|
||||
* @param previouslyStoredDeviceKeys the device keys we received before for this device
|
||||
* @return true if succeeds
|
||||
*/
|
||||
private boolean validateDeviceKeys(MXDeviceInfo deviceKeys, String userId, String deviceId, MXDeviceInfo previouslyStoredDeviceKeys) {
|
||||
if (null == deviceKeys) {
|
||||
Log.e(LOG_TAG, "## validateDeviceKeys() : deviceKeys is null from " + userId + ":" + deviceId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (null == deviceKeys.keys) {
|
||||
Log.e(LOG_TAG, "## validateDeviceKeys() : deviceKeys.keys is null from " + userId + ":" + deviceId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (null == deviceKeys.signatures) {
|
||||
Log.e(LOG_TAG, "## validateDeviceKeys() : deviceKeys.signatures is null from " + userId + ":" + deviceId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check that the user_id and device_id in the received deviceKeys are correct
|
||||
if (!TextUtils.equals(deviceKeys.userId, userId)) {
|
||||
Log.e(LOG_TAG, "## validateDeviceKeys() : Mismatched user_id " + deviceKeys.userId + " from " + userId + ":" + deviceId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TextUtils.equals(deviceKeys.deviceId, deviceId)) {
|
||||
Log.e(LOG_TAG, "## validateDeviceKeys() : Mismatched device_id " + deviceKeys.deviceId + " from " + userId + ":" + deviceId);
|
||||
return false;
|
||||
}
|
||||
|
||||
String signKeyId = "ed25519:" + deviceKeys.deviceId;
|
||||
String signKey = deviceKeys.keys.get(signKeyId);
|
||||
|
||||
if (null == signKey) {
|
||||
Log.e(LOG_TAG, "## validateDeviceKeys() : Device " + userId + ":" + deviceKeys.deviceId + " has no ed25519 key");
|
||||
return false;
|
||||
}
|
||||
|
||||
Map<String, String> signatureMap = deviceKeys.signatures.get(userId);
|
||||
|
||||
if (null == signatureMap) {
|
||||
Log.e(LOG_TAG, "## validateDeviceKeys() : Device " + userId + ":" + deviceKeys.deviceId + " has no map for " + userId);
|
||||
return false;
|
||||
}
|
||||
|
||||
String signature = signatureMap.get(signKeyId);
|
||||
|
||||
if (null == signature) {
|
||||
Log.e(LOG_TAG, "## validateDeviceKeys() : Device " + userId + ":" + deviceKeys.deviceId + " is not signed");
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean isVerified = false;
|
||||
String errorMessage = null;
|
||||
|
||||
try {
|
||||
mxCrypto.getOlmDevice().verifySignature(signKey, deviceKeys.signalableJSONDictionary(), signature);
|
||||
isVerified = true;
|
||||
} catch (Exception e) {
|
||||
errorMessage = e.getMessage();
|
||||
}
|
||||
|
||||
if (!isVerified) {
|
||||
Log.e(LOG_TAG, "## validateDeviceKeys() : Unable to verify signature on device " + userId + ":"
|
||||
+ deviceKeys.deviceId + " with error " + errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (null != previouslyStoredDeviceKeys) {
|
||||
if (!TextUtils.equals(previouslyStoredDeviceKeys.fingerprint(), signKey)) {
|
||||
// This should only happen if the list has been MITMed; we are
|
||||
// best off sticking with the original keys.
|
||||
//
|
||||
// Should we warn the user about it somehow?
|
||||
Log.e(LOG_TAG, "## validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":"
|
||||
+ deviceKeys.deviceId + " has changed : "
|
||||
+ previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey);
|
||||
|
||||
Log.e(LOG_TAG, "## validateDeviceKeys() : " + previouslyStoredDeviceKeys + " -> " + deviceKeys);
|
||||
Log.e(LOG_TAG, "## validateDeviceKeys() : " + previouslyStoredDeviceKeys.keys + " -> " + deviceKeys.keys);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start device queries for any users who sent us an m.new_device recently
|
||||
* This method must be called on getEncryptingThreadHandler() thread.
|
||||
*/
|
||||
public void refreshOutdatedDeviceLists() {
|
||||
final List<String> users = new ArrayList<>();
|
||||
|
||||
Map<String, Integer> deviceTrackingStatuses = mCryptoStore.getDeviceTrackingStatuses();
|
||||
|
||||
for (String userId : deviceTrackingStatuses.keySet()) {
|
||||
if (TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses.get(userId)) {
|
||||
users.add(userId);
|
||||
}
|
||||
}
|
||||
|
||||
if (users.size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mIsDownloadingKeys) {
|
||||
// request already in progress - do nothing. (We will automatically
|
||||
// make another request if there are more users with outdated
|
||||
// device lists when the current request completes).
|
||||
return;
|
||||
}
|
||||
|
||||
// update the statuses
|
||||
for (String userId : users) {
|
||||
Integer status = deviceTrackingStatuses.get(userId);
|
||||
|
||||
if ((null != status) && (TRACKING_STATUS_PENDING_DOWNLOAD == status)) {
|
||||
deviceTrackingStatuses.put(userId, TRACKING_STATUS_DOWNLOAD_IN_PROGRESS);
|
||||
}
|
||||
}
|
||||
|
||||
mCryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses);
|
||||
|
||||
doKeyDownloadForUsers(users, new ApiCallback<MXUsersDevicesMap<MXDeviceInfo>>() {
|
||||
@Override
|
||||
public void onSuccess(final MXUsersDevicesMap<MXDeviceInfo> response) {
|
||||
mxCrypto.getEncryptingThreadHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Log.d(LOG_TAG, "## refreshOutdatedDeviceLists() : done");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onError(String error) {
|
||||
Log.e(LOG_TAG, "## refreshOutdatedDeviceLists() : ERROR updating device keys for users " + users + " : " + error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(final Exception e) {
|
||||
onError(e.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(final MatrixError e) {
|
||||
onError(e.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(final Exception e) {
|
||||
onError(e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,270 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Base64;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.crypto.EncryptedFileInfo;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.crypto.EncryptedFileKey;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.Serializable;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class MXEncryptedAttachments implements Serializable {
|
||||
private static final String LOG_TAG = MXEncryptedAttachments.class.getSimpleName();
|
||||
|
||||
private static final int CRYPTO_BUFFER_SIZE = 32 * 1024;
|
||||
private static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
|
||||
private static final String SECRET_KEY_SPEC_ALGORITHM = "AES";
|
||||
private static final String MESSAGE_DIGEST_ALGORITHM = "SHA-256";
|
||||
|
||||
/**
|
||||
* Define the result of an encryption file
|
||||
*/
|
||||
public static class EncryptionResult {
|
||||
public EncryptedFileInfo mEncryptedFileInfo;
|
||||
public InputStream mEncryptedStream;
|
||||
|
||||
public EncryptionResult() {
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
* Encrypt an attachment stream.
|
||||
* @param attachmentStream the attachment stream
|
||||
* @param mimetype the mime type
|
||||
* @return the encryption file info
|
||||
*/
|
||||
public static EncryptionResult encryptAttachment(InputStream attachmentStream, String mimetype) {
|
||||
long t0 = System.currentTimeMillis();
|
||||
SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
// generate a random iv key
|
||||
// Half of the IV is random, the lower order bits are zeroed
|
||||
// such that the counter never wraps.
|
||||
// See https://github.com/matrix-org/matrix-ios-kit/blob/3dc0d8e46b4deb6669ed44f72ad79be56471354c/MatrixKit/Models/Room/MXEncryptedAttachments.m#L75
|
||||
byte[] initVectorBytes = new byte[16];
|
||||
Arrays.fill(initVectorBytes, (byte) 0);
|
||||
|
||||
byte[] ivRandomPart = new byte[8];
|
||||
secureRandom.nextBytes(ivRandomPart);
|
||||
|
||||
System.arraycopy(ivRandomPart, 0, initVectorBytes, 0, ivRandomPart.length);
|
||||
|
||||
byte[] key = new byte[32];
|
||||
secureRandom.nextBytes(key);
|
||||
|
||||
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
Cipher encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM);
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM);
|
||||
IvParameterSpec ivParameterSpec = new IvParameterSpec(initVectorBytes);
|
||||
encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
|
||||
|
||||
MessageDigest messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM);
|
||||
|
||||
byte[] data = new byte[CRYPTO_BUFFER_SIZE];
|
||||
int read;
|
||||
byte[] encodedBytes;
|
||||
|
||||
while (-1 != (read = attachmentStream.read(data))) {
|
||||
encodedBytes = encryptCipher.update(data, 0, read);
|
||||
messageDigest.update(encodedBytes, 0, encodedBytes.length);
|
||||
outStream.write(encodedBytes);
|
||||
}
|
||||
|
||||
// encrypt the latest chunk
|
||||
encodedBytes = encryptCipher.doFinal();
|
||||
messageDigest.update(encodedBytes, 0, encodedBytes.length);
|
||||
outStream.write(encodedBytes);
|
||||
|
||||
EncryptionResult result = new EncryptionResult();
|
||||
result.mEncryptedFileInfo = new EncryptedFileInfo();
|
||||
result.mEncryptedFileInfo.key = new EncryptedFileKey();
|
||||
result.mEncryptedFileInfo.mimetype = mimetype;
|
||||
result.mEncryptedFileInfo.key.alg = "A256CTR";
|
||||
result.mEncryptedFileInfo.key.ext = true;
|
||||
result.mEncryptedFileInfo.key.key_ops = Arrays.asList("encrypt", "decrypt");
|
||||
result.mEncryptedFileInfo.key.kty = "oct";
|
||||
result.mEncryptedFileInfo.key.k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT));
|
||||
result.mEncryptedFileInfo.iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", "");
|
||||
result.mEncryptedFileInfo.v = "v2";
|
||||
|
||||
result.mEncryptedFileInfo.hashes = new HashMap<>();
|
||||
result.mEncryptedFileInfo.hashes.put("sha256", base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT)));
|
||||
|
||||
result.mEncryptedStream = new ByteArrayInputStream(outStream.toByteArray());
|
||||
outStream.close();
|
||||
|
||||
Log.d(LOG_TAG, "Encrypt in " + (System.currentTimeMillis() - t0) + " ms");
|
||||
return result;
|
||||
} catch (OutOfMemoryError oom) {
|
||||
Log.e(LOG_TAG, "## encryptAttachment failed " + oom.getMessage(), oom);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## encryptAttachment failed " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
try {
|
||||
outStream.close();
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## encryptAttachment() : fail to close outStream", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt an attachment
|
||||
*
|
||||
* @param attachmentStream the attachment stream
|
||||
* @param encryptedFileInfo the encryption file info
|
||||
* @return the decrypted attachment stream
|
||||
*/
|
||||
public static InputStream decryptAttachment(InputStream attachmentStream, EncryptedFileInfo encryptedFileInfo) {
|
||||
// sanity checks
|
||||
if ((null == attachmentStream) || (null == encryptedFileInfo)) {
|
||||
Log.e(LOG_TAG, "## decryptAttachment() : null parameters");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(encryptedFileInfo.iv)
|
||||
|| (null == encryptedFileInfo.key)
|
||||
|| (null == encryptedFileInfo.hashes)
|
||||
|| !encryptedFileInfo.hashes.containsKey("sha256")) {
|
||||
Log.e(LOG_TAG, "## decryptAttachment() : some fields are not defined");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!TextUtils.equals(encryptedFileInfo.key.alg, "A256CTR")
|
||||
|| !TextUtils.equals(encryptedFileInfo.key.kty, "oct")
|
||||
|| TextUtils.isEmpty(encryptedFileInfo.key.k)) {
|
||||
Log.e(LOG_TAG, "## decryptAttachment() : invalid key fields");
|
||||
return null;
|
||||
}
|
||||
|
||||
// detect if there is no data to decrypt
|
||||
try {
|
||||
if (0 == attachmentStream.available()) {
|
||||
return new ByteArrayInputStream(new byte[0]);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "Fail to retrieve the file size", e);
|
||||
}
|
||||
|
||||
long t0 = System.currentTimeMillis();
|
||||
|
||||
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
byte[] key = Base64.decode(base64UrlToBase64(encryptedFileInfo.key.k), Base64.DEFAULT);
|
||||
byte[] initVectorBytes = Base64.decode(encryptedFileInfo.iv, Base64.DEFAULT);
|
||||
|
||||
Cipher decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM);
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM);
|
||||
IvParameterSpec ivParameterSpec = new IvParameterSpec(initVectorBytes);
|
||||
decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
|
||||
|
||||
MessageDigest messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM);
|
||||
|
||||
int read;
|
||||
byte[] data = new byte[CRYPTO_BUFFER_SIZE];
|
||||
byte[] decodedBytes;
|
||||
|
||||
while (-1 != (read = attachmentStream.read(data))) {
|
||||
messageDigest.update(data, 0, read);
|
||||
decodedBytes = decryptCipher.update(data, 0, read);
|
||||
outStream.write(decodedBytes);
|
||||
}
|
||||
|
||||
// decrypt the last chunk
|
||||
decodedBytes = decryptCipher.doFinal();
|
||||
outStream.write(decodedBytes);
|
||||
|
||||
String currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT));
|
||||
|
||||
if (!TextUtils.equals(encryptedFileInfo.hashes.get("sha256"), currentDigestValue)) {
|
||||
Log.e(LOG_TAG, "## decryptAttachment() : Digest value mismatch");
|
||||
outStream.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
InputStream decryptedStream = new ByteArrayInputStream(outStream.toByteArray());
|
||||
outStream.close();
|
||||
|
||||
Log.d(LOG_TAG, "Decrypt in " + (System.currentTimeMillis() - t0) + " ms");
|
||||
|
||||
return decryptedStream;
|
||||
} catch (OutOfMemoryError oom) {
|
||||
Log.e(LOG_TAG, "## decryptAttachment() : failed " + oom.getMessage(), oom);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## decryptAttachment() : failed " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
try {
|
||||
outStream.close();
|
||||
} catch (Exception closeException) {
|
||||
Log.e(LOG_TAG, "## decryptAttachment() : fail to close the file", closeException);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 URL conversion methods
|
||||
*/
|
||||
|
||||
private static String base64UrlToBase64(String base64Url) {
|
||||
if (null != base64Url) {
|
||||
base64Url = base64Url.replaceAll("-", "+");
|
||||
base64Url = base64Url.replaceAll("_", "/");
|
||||
}
|
||||
|
||||
return base64Url;
|
||||
}
|
||||
|
||||
private static String base64ToBase64Url(String base64) {
|
||||
if (null != base64) {
|
||||
base64 = base64.replaceAll("\n", "");
|
||||
base64 = base64.replaceAll("\\+", "-");
|
||||
base64 = base64.replaceAll("/", "_");
|
||||
base64 = base64.replaceAll("=", "");
|
||||
}
|
||||
return base64;
|
||||
}
|
||||
|
||||
private static String base64ToUnpaddedBase64(String base64) {
|
||||
if (null != base64) {
|
||||
base64 = base64.replaceAll("\n", "");
|
||||
base64 = base64.replaceAll("=", "");
|
||||
}
|
||||
|
||||
return base64;
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The result of a (successful) call to decryptEvent.
|
||||
*/
|
||||
public class MXEventDecryptionResult {
|
||||
|
||||
/**
|
||||
* The plaintext payload for the event (typically containing "type" and "content" fields).
|
||||
*/
|
||||
public JsonElement mClearEvent;
|
||||
|
||||
/**
|
||||
* Key owned by the sender of this event.
|
||||
* See MXEvent.senderKey.
|
||||
*/
|
||||
public String mSenderCurve25519Key;
|
||||
|
||||
/**
|
||||
* Ed25519 key claimed by the sender of this event.
|
||||
* See MXEvent.claimedEd25519Key.
|
||||
*/
|
||||
public String mClaimedEd25519Key;
|
||||
|
||||
/**
|
||||
* List of curve25519 keys involved in telling us about the senderCurve25519Key and
|
||||
* claimedEd25519Key. See MXEvent.forwardingCurve25519KeyChain.
|
||||
*/
|
||||
public List<String> mForwardingCurve25519KeyChain = new ArrayList<>();
|
||||
}
|
@ -0,0 +1,370 @@
|
||||
/*
|
||||
* Copyright 2017 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Base64;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* Utility class to import/export the crypto data
|
||||
*/
|
||||
public class MXMegolmExportEncryption {
|
||||
private static final String LOG_TAG = MXMegolmExportEncryption.class.getSimpleName();
|
||||
|
||||
private static final String HEADER_LINE = "-----BEGIN MEGOLM SESSION DATA-----";
|
||||
private static final String TRAILER_LINE = "-----END MEGOLM SESSION DATA-----";
|
||||
// we split into lines before base64ing, because encodeBase64 doesn't deal
|
||||
// terribly well with large arrays.
|
||||
private static final int LINE_LENGTH = (72 * 4 / 3);
|
||||
|
||||
// default iteration count to export the e2e keys
|
||||
public static final int DEFAULT_ITERATION_COUNT = 500000;
|
||||
|
||||
/**
|
||||
* Convert a signed byte to a int value
|
||||
*
|
||||
* @param bVal the byte value to convert
|
||||
* @return the matched int value
|
||||
*/
|
||||
private static int byteToInt(byte bVal) {
|
||||
return bVal & 0xFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the AES key from the deriveKeys result.
|
||||
*
|
||||
* @param keyBits the deriveKeys result.
|
||||
* @return the AES key
|
||||
*/
|
||||
private static byte[] getAesKey(byte[] keyBits) {
|
||||
return Arrays.copyOfRange(keyBits, 0, 32);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the Hmac key from the deriveKeys result.
|
||||
*
|
||||
* @param keyBits the deriveKeys result.
|
||||
* @return the Hmac key.
|
||||
*/
|
||||
private static byte[] getHmacKey(byte[] keyBits) {
|
||||
return Arrays.copyOfRange(keyBits, 32, keyBits.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt a megolm key file
|
||||
*
|
||||
* @param data the data to decrypt
|
||||
* @param password the password.
|
||||
* @return the decrypted output.
|
||||
* @throws Exception the failure reason
|
||||
*/
|
||||
public static String decryptMegolmKeyFile(byte[] data, String password) throws Exception {
|
||||
byte[] body = unpackMegolmKeyFile(data);
|
||||
|
||||
// check we have a version byte
|
||||
if ((null == body) || (body.length == 0)) {
|
||||
Log.e(LOG_TAG, "## decryptMegolmKeyFile() : Invalid file: too short");
|
||||
throw new Exception("Invalid file: too short");
|
||||
}
|
||||
|
||||
byte version = body[0];
|
||||
if (version != 1) {
|
||||
Log.e(LOG_TAG, "## decryptMegolmKeyFile() : Invalid file: too short");
|
||||
throw new Exception("Unsupported version");
|
||||
}
|
||||
|
||||
int ciphertextLength = body.length - (1 + 16 + 16 + 4 + 32);
|
||||
if (ciphertextLength < 0) {
|
||||
throw new Exception("Invalid file: too short");
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(password)) {
|
||||
throw new Exception("Empty password is not supported");
|
||||
}
|
||||
|
||||
byte[] salt = Arrays.copyOfRange(body, 1, 1 + 16);
|
||||
byte[] iv = Arrays.copyOfRange(body, 17, 17 + 16);
|
||||
int iterations = byteToInt(body[33]) << 24 | byteToInt(body[34]) << 16 | byteToInt(body[35]) << 8 | byteToInt(body[36]);
|
||||
byte[] ciphertext = Arrays.copyOfRange(body, 37, 37 + ciphertextLength);
|
||||
byte[] hmac = Arrays.copyOfRange(body, body.length - 32, body.length);
|
||||
|
||||
byte[] deriveKey = deriveKeys(salt, iterations, password);
|
||||
|
||||
byte[] toVerify = Arrays.copyOfRange(body, 0, body.length - 32);
|
||||
|
||||
SecretKey macKey = new SecretKeySpec(getHmacKey(deriveKey), "HmacSHA256");
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(macKey);
|
||||
byte[] digest = mac.doFinal(toVerify);
|
||||
|
||||
if (!Arrays.equals(hmac, digest)) {
|
||||
Log.e(LOG_TAG, "## decryptMegolmKeyFile() : Authentication check failed: incorrect password?");
|
||||
throw new Exception("Authentication check failed: incorrect password?");
|
||||
}
|
||||
|
||||
Cipher decryptCipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(getAesKey(deriveKey), "AES");
|
||||
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
|
||||
decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
|
||||
|
||||
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
|
||||
outStream.write(decryptCipher.update(ciphertext));
|
||||
outStream.write(decryptCipher.doFinal());
|
||||
|
||||
String decodedString = new String(outStream.toByteArray(), "UTF-8");
|
||||
outStream.close();
|
||||
|
||||
return decodedString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a string into the megolm export format.
|
||||
*
|
||||
* @param data the data to encrypt.
|
||||
* @param password the password
|
||||
* @return the encrypted data
|
||||
* @throws Exception the failure reason
|
||||
*/
|
||||
public static byte[] encryptMegolmKeyFile(String data, String password) throws Exception {
|
||||
return encryptMegolmKeyFile(data, password, DEFAULT_ITERATION_COUNT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a string into the megolm export format.
|
||||
*
|
||||
* @param data the data to encrypt.
|
||||
* @param password the password
|
||||
* @param kdf_rounds the iteration count
|
||||
* @return the encrypted data
|
||||
* @throws Exception the failure reason
|
||||
*/
|
||||
public static byte[] encryptMegolmKeyFile(String data, String password, int kdf_rounds) throws Exception {
|
||||
if (TextUtils.isEmpty(password)) {
|
||||
throw new Exception("Empty password is not supported");
|
||||
}
|
||||
|
||||
SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
byte[] salt = new byte[16];
|
||||
secureRandom.nextBytes(salt);
|
||||
|
||||
byte[] iv = new byte[16];
|
||||
secureRandom.nextBytes(iv);
|
||||
|
||||
// clear bit 63 of the salt to stop us hitting the 64-bit counter boundary
|
||||
// (which would mean we wouldn't be able to decrypt on Android). The loss
|
||||
// of a single bit of salt is a price we have to pay.
|
||||
iv[9] &= 0x7f;
|
||||
|
||||
byte[] deriveKey = deriveKeys(salt, kdf_rounds, password);
|
||||
|
||||
Cipher decryptCipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(getAesKey(deriveKey), "AES");
|
||||
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
|
||||
decryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
|
||||
|
||||
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
|
||||
outStream.write(decryptCipher.update(data.getBytes("UTF-8")));
|
||||
outStream.write(decryptCipher.doFinal());
|
||||
|
||||
byte[] cipherArray = outStream.toByteArray();
|
||||
int bodyLength = (1 + salt.length + iv.length + 4 + cipherArray.length + 32);
|
||||
|
||||
byte[] resultBuffer = new byte[bodyLength];
|
||||
int idx = 0;
|
||||
resultBuffer[idx++] = 1; // version
|
||||
|
||||
System.arraycopy(salt, 0, resultBuffer, idx, salt.length);
|
||||
idx += salt.length;
|
||||
|
||||
System.arraycopy(iv, 0, resultBuffer, idx, iv.length);
|
||||
idx += iv.length;
|
||||
|
||||
resultBuffer[idx++] = (byte) ((kdf_rounds >> 24) & 0xff);
|
||||
resultBuffer[idx++] = (byte) ((kdf_rounds >> 16) & 0xff);
|
||||
resultBuffer[idx++] = (byte) ((kdf_rounds >> 8) & 0xff);
|
||||
resultBuffer[idx++] = (byte) ((kdf_rounds) & 0xff);
|
||||
|
||||
System.arraycopy(cipherArray, 0, resultBuffer, idx, cipherArray.length);
|
||||
idx += cipherArray.length;
|
||||
|
||||
byte[] toSign = Arrays.copyOfRange(resultBuffer, 0, idx);
|
||||
|
||||
SecretKey macKey = new SecretKeySpec(getHmacKey(deriveKey), "HmacSHA256");
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(macKey);
|
||||
byte[] digest = mac.doFinal(toSign);
|
||||
System.arraycopy(digest, 0, resultBuffer, idx, digest.length);
|
||||
|
||||
return packMegolmKeyFile(resultBuffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbase64 an ascii-armoured megolm key file
|
||||
* Strips the header and trailer lines, and unbase64s the content
|
||||
*
|
||||
* @param data the input data
|
||||
* @return unbase64ed content
|
||||
*/
|
||||
private static byte[] unpackMegolmKeyFile(byte[] data) throws Exception {
|
||||
String fileStr = new String(data, "UTF-8");
|
||||
|
||||
// look for the start line
|
||||
int lineStart = 0;
|
||||
|
||||
while (true) {
|
||||
int lineEnd = fileStr.indexOf('\n', lineStart);
|
||||
|
||||
if (lineEnd < 0) {
|
||||
Log.e(LOG_TAG, "## unpackMegolmKeyFile() : Header line not found");
|
||||
throw new Exception("Header line not found");
|
||||
}
|
||||
|
||||
String line = fileStr.substring(lineStart, lineEnd).trim();
|
||||
|
||||
// start the next line after the newline
|
||||
lineStart = lineEnd + 1;
|
||||
|
||||
if (TextUtils.equals(line, HEADER_LINE)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int dataStart = lineStart;
|
||||
|
||||
// look for the end line
|
||||
while (true) {
|
||||
int lineEnd = fileStr.indexOf('\n', lineStart);
|
||||
String line;
|
||||
|
||||
if (lineEnd < 0) {
|
||||
line = fileStr.substring(lineStart).trim();
|
||||
} else {
|
||||
line = fileStr.substring(lineStart, lineEnd).trim();
|
||||
}
|
||||
|
||||
if (TextUtils.equals(line, TRAILER_LINE)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (lineEnd < 0) {
|
||||
Log.e(LOG_TAG, "## unpackMegolmKeyFile() : Trailer line not found");
|
||||
throw new Exception("Trailer line not found");
|
||||
}
|
||||
|
||||
// start the next line after the newline
|
||||
lineStart = lineEnd + 1;
|
||||
}
|
||||
|
||||
int dataEnd = lineStart;
|
||||
|
||||
// Receiving side
|
||||
return Base64.decode(fileStr.substring(dataStart, dataEnd), Base64.DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack the megolm data.
|
||||
*
|
||||
* @param data the data to pack.
|
||||
* @return the packed data
|
||||
* @throws Exception the failure reason.
|
||||
*/
|
||||
private static byte[] packMegolmKeyFile(byte[] data) throws Exception {
|
||||
int nLines = (data.length + LINE_LENGTH - 1) / LINE_LENGTH;
|
||||
|
||||
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
|
||||
outStream.write(HEADER_LINE.getBytes());
|
||||
|
||||
int o = 0;
|
||||
|
||||
for (int i = 1; i <= nLines; i++) {
|
||||
outStream.write("\n".getBytes());
|
||||
|
||||
int len = Math.min(LINE_LENGTH, data.length - o);
|
||||
outStream.write(Base64.encode(data, o, len, Base64.DEFAULT));
|
||||
o += LINE_LENGTH;
|
||||
}
|
||||
|
||||
outStream.write("\n".getBytes());
|
||||
outStream.write(TRAILER_LINE.getBytes());
|
||||
outStream.write("\n".getBytes());
|
||||
|
||||
return outStream.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the AES and HMAC-SHA-256 keys for the file
|
||||
*
|
||||
* @param salt salt for pbkdf
|
||||
* @param iterations number of pbkdf iterations
|
||||
* @param password password
|
||||
* @return the derived keys
|
||||
*/
|
||||
private static byte[] deriveKeys(byte[] salt, int iterations, String password) throws Exception {
|
||||
Long t0 = System.currentTimeMillis();
|
||||
|
||||
// based on https://en.wikipedia.org/wiki/PBKDF2 algorithm
|
||||
// it is simpler than the generic algorithm because the expected key length is equal to the mac key length.
|
||||
// noticed as dklen/hlen
|
||||
Mac prf = Mac.getInstance("HmacSHA512");
|
||||
prf.init(new SecretKeySpec(password.getBytes("UTF-8"), "HmacSHA512"));
|
||||
|
||||
// 512 bits key length
|
||||
byte[] key = new byte[64];
|
||||
byte[] Uc = new byte[64];
|
||||
|
||||
// U1 = PRF(Password, Salt || INT_32_BE(i))
|
||||
prf.update(salt);
|
||||
byte[] int32BE = new byte[4];
|
||||
Arrays.fill(int32BE, (byte) 0);
|
||||
int32BE[3] = (byte) 1;
|
||||
prf.update(int32BE);
|
||||
prf.doFinal(Uc, 0);
|
||||
|
||||
// copy to the key
|
||||
System.arraycopy(Uc, 0, key, 0, Uc.length);
|
||||
|
||||
for (int index = 2; index <= iterations; index++) {
|
||||
// Uc = PRF(Password, Uc-1)
|
||||
prf.update(Uc);
|
||||
prf.doFinal(Uc, 0);
|
||||
|
||||
// F(Password, Salt, c, i) = U1 ^ U2 ^ ... ^ Uc
|
||||
for (int byteIndex = 0; byteIndex < Uc.length; byteIndex++) {
|
||||
key[byteIndex] ^= Uc[byteIndex];
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(LOG_TAG, "## deriveKeys() : " + iterations + " in " + (System.currentTimeMillis() - t0) + " ms");
|
||||
|
||||
return key;
|
||||
}
|
||||
}
|
@ -0,0 +1,830 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.crypto.algorithms.MXDecryptionResult;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXOlmInboundGroupSession2;
|
||||
import im.vector.matrix.android.internal.legacy.data.cryptostore.IMXCryptoStore;
|
||||
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
import org.matrix.olm.OlmAccount;
|
||||
import org.matrix.olm.OlmInboundGroupSession;
|
||||
import org.matrix.olm.OlmMessage;
|
||||
import org.matrix.olm.OlmOutboundGroupSession;
|
||||
import org.matrix.olm.OlmSession;
|
||||
import org.matrix.olm.OlmUtility;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class MXOlmDevice {
|
||||
private static final String LOG_TAG = MXOlmDevice.class.getSimpleName();
|
||||
|
||||
// Curve25519 key for the account.
|
||||
private String mDeviceCurve25519Key;
|
||||
|
||||
// Ed25519 key for the account.
|
||||
private String mDeviceEd25519Key;
|
||||
|
||||
// The store where crypto data is saved.
|
||||
private final IMXCryptoStore mStore;
|
||||
|
||||
// The OLMKit account instance.
|
||||
private OlmAccount mOlmAccount;
|
||||
|
||||
// The OLMKit utility instance.
|
||||
private OlmUtility mOlmUtility;
|
||||
|
||||
// The outbound group session.
|
||||
// They are not stored in 'store' to avoid to remember to which devices we sent the session key.
|
||||
// Plus, in cryptography, it is good to refresh sessions from time to time.
|
||||
// The key is the session id, the value the outbound group session.
|
||||
private final Map<String, OlmOutboundGroupSession> mOutboundGroupSessionStore;
|
||||
|
||||
// Store a set of decrypted message indexes for each group session.
|
||||
// This partially mitigates a replay attack where a MITM resends a group
|
||||
// message into the room.
|
||||
//
|
||||
// The Matrix SDK exposes events through MXEventTimelines. A developer can open several
|
||||
// timelines from a same room so that a message can be decrypted several times but from
|
||||
// a different timeline.
|
||||
// So, store these message indexes per timeline id.
|
||||
//
|
||||
// The first level keys are timeline ids.
|
||||
// The second level keys are strings of form "<senderKey>|<session_id>|<message_index>"
|
||||
// Values are true.
|
||||
private final Map<String, Map<String, Boolean>> mInboundGroupSessionMessageIndexes;
|
||||
|
||||
/**
|
||||
* inboundGroupSessionWithId error
|
||||
*/
|
||||
private MXCryptoError mInboundGroupSessionWithIdError = null;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param store the used store
|
||||
*/
|
||||
public MXOlmDevice(IMXCryptoStore store) {
|
||||
mStore = store;
|
||||
|
||||
// Retrieve the account from the store
|
||||
mOlmAccount = mStore.getAccount();
|
||||
|
||||
if (null == mOlmAccount) {
|
||||
Log.d(LOG_TAG, "MXOlmDevice : create a new olm account");
|
||||
// Else, create it
|
||||
try {
|
||||
mOlmAccount = new OlmAccount();
|
||||
mStore.storeAccount(mOlmAccount);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "MXOlmDevice : cannot initialize mOlmAccount " + e.getMessage(), e);
|
||||
}
|
||||
} else {
|
||||
Log.d(LOG_TAG, "MXOlmDevice : use an existing account");
|
||||
}
|
||||
|
||||
try {
|
||||
mOlmUtility = new OlmUtility();
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## MXOlmDevice : OlmUtility failed with error " + e.getMessage(), e);
|
||||
mOlmUtility = null;
|
||||
}
|
||||
|
||||
mOutboundGroupSessionStore = new HashMap<>();
|
||||
|
||||
try {
|
||||
mDeviceCurve25519Key = mOlmAccount.identityKeys().get(OlmAccount.JSON_KEY_IDENTITY_KEY);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## MXOlmDevice : cannot find " + OlmAccount.JSON_KEY_IDENTITY_KEY + " with error " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
try {
|
||||
mDeviceEd25519Key = mOlmAccount.identityKeys().get(OlmAccount.JSON_KEY_FINGER_PRINT_KEY);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## MXOlmDevice : cannot find " + OlmAccount.JSON_KEY_FINGER_PRINT_KEY + " with error " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
mInboundGroupSessionMessageIndexes = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the instance
|
||||
*/
|
||||
public void release() {
|
||||
if (null != mOlmAccount) {
|
||||
mOlmAccount.releaseAccount();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the Curve25519 key for the account.
|
||||
*/
|
||||
public String getDeviceCurve25519Key() {
|
||||
return mDeviceCurve25519Key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the Ed25519 key for the account.
|
||||
*/
|
||||
public String getDeviceEd25519Key() {
|
||||
return mDeviceEd25519Key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a message with the ed25519 key for this account.
|
||||
*
|
||||
* @param message the message to be signed.
|
||||
* @return the base64-encoded signature.
|
||||
*/
|
||||
private String signMessage(String message) {
|
||||
try {
|
||||
return mOlmAccount.signMessage(message);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## signMessage() : failed " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a JSON dictionary with the ed25519 key for this account.
|
||||
* The signature is done on canonical version of the JSON.
|
||||
*
|
||||
* @param JSONDictionary the JSON to be signed.
|
||||
* @return the base64-encoded signature
|
||||
*/
|
||||
public String signJSON(Map<String, Object> JSONDictionary) {
|
||||
return signMessage(JsonUtils.getCanonicalizedJsonString(JSONDictionary));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The current (unused, unpublished) one-time keys for this account.
|
||||
*/
|
||||
public Map<String, Map<String, String>> getOneTimeKeys() {
|
||||
try {
|
||||
return mOlmAccount.oneTimeKeys();
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## getOneTimeKeys() : failed " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The maximum number of one-time keys the olm account can store.
|
||||
*/
|
||||
public long getMaxNumberOfOneTimeKeys() {
|
||||
if (null != mOlmAccount) {
|
||||
return mOlmAccount.maxOneTimeKeys();
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks all of the one-time keys as published.
|
||||
*/
|
||||
public void markKeysAsPublished() {
|
||||
try {
|
||||
mOlmAccount.markOneTimeKeysAsPublished();
|
||||
mStore.storeAccount(mOlmAccount);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## markKeysAsPublished() : failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate some new one-time keys
|
||||
*
|
||||
* @param numKeys number of keys to generate
|
||||
*/
|
||||
public void generateOneTimeKeys(int numKeys) {
|
||||
try {
|
||||
mOlmAccount.generateOneTimeKeys(numKeys);
|
||||
mStore.storeAccount(mOlmAccount);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## generateOneTimeKeys() : failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new outbound session.
|
||||
* The new session will be stored in the MXStore.
|
||||
*
|
||||
* @param theirIdentityKey the remote user's Curve25519 identity key
|
||||
* @param theirOneTimeKey the remote user's one-time Curve25519 key
|
||||
* @return the session id for the outbound session. @TODO OLMSession?
|
||||
*/
|
||||
public String createOutboundSession(String theirIdentityKey, String theirOneTimeKey) {
|
||||
Log.d(LOG_TAG, "## createOutboundSession() ; theirIdentityKey " + theirIdentityKey + " theirOneTimeKey " + theirOneTimeKey);
|
||||
OlmSession olmSession = null;
|
||||
|
||||
try {
|
||||
olmSession = new OlmSession();
|
||||
olmSession.initOutboundSession(mOlmAccount, theirIdentityKey, theirOneTimeKey);
|
||||
mStore.storeSession(olmSession, theirIdentityKey);
|
||||
|
||||
String sessionIdentifier = olmSession.sessionIdentifier();
|
||||
|
||||
Log.d(LOG_TAG, "## createOutboundSession() ; olmSession.sessionIdentifier: " + sessionIdentifier);
|
||||
return sessionIdentifier;
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## createOutboundSession() failed ; " + e.getMessage(), e);
|
||||
|
||||
if (null != olmSession) {
|
||||
olmSession.releaseSession();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new inbound session, given an incoming message.
|
||||
*
|
||||
* @param theirDeviceIdentityKey the remote user's Curve25519 identity key.
|
||||
* @param messageType the message_type field from the received message (must be 0).
|
||||
* @param ciphertext base64-encoded body from the received message.
|
||||
* @return {{payload: string, session_id: string}} decrypted payload, andsession id of new session.
|
||||
*/
|
||||
public Map<String, String> createInboundSession(String theirDeviceIdentityKey, int messageType, String ciphertext) {
|
||||
|
||||
Log.d(LOG_TAG, "## createInboundSession() : theirIdentityKey: " + theirDeviceIdentityKey);
|
||||
|
||||
OlmSession olmSession = null;
|
||||
|
||||
try {
|
||||
try {
|
||||
olmSession = new OlmSession();
|
||||
olmSession.initInboundSessionFrom(mOlmAccount, theirDeviceIdentityKey, ciphertext);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## createInboundSession() : the session creation failed " + e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
|
||||
Log.d(LOG_TAG, "## createInboundSession() : sessionId: " + olmSession.sessionIdentifier());
|
||||
|
||||
try {
|
||||
mOlmAccount.removeOneTimeKeys(olmSession);
|
||||
mStore.storeAccount(mOlmAccount);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## createInboundSession() : removeOneTimeKeys failed " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
Log.d(LOG_TAG, "## createInboundSession() : ciphertext: " + ciphertext);
|
||||
try {
|
||||
Log.d(LOG_TAG, "## createInboundSession() :ciphertext: SHA256:" + mOlmUtility.sha256(URLEncoder.encode(ciphertext, "utf-8")));
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## createInboundSession() :ciphertext: cannot encode ciphertext", e);
|
||||
}
|
||||
|
||||
OlmMessage olmMessage = new OlmMessage();
|
||||
olmMessage.mCipherText = ciphertext;
|
||||
olmMessage.mType = messageType;
|
||||
|
||||
String payloadString = null;
|
||||
|
||||
try {
|
||||
payloadString = olmSession.decryptMessage(olmMessage);
|
||||
mStore.storeSession(olmSession, theirDeviceIdentityKey);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## createInboundSession() : decryptMessage failed " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
Map<String, String> res = new HashMap<>();
|
||||
|
||||
if (!TextUtils.isEmpty(payloadString)) {
|
||||
res.put("payload", payloadString);
|
||||
}
|
||||
|
||||
String sessionIdentifier = olmSession.sessionIdentifier();
|
||||
|
||||
if (!TextUtils.isEmpty(sessionIdentifier)) {
|
||||
res.put("session_id", sessionIdentifier);
|
||||
}
|
||||
|
||||
return res;
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## createInboundSession() : OlmSession creation failed " + e.getMessage(), e);
|
||||
|
||||
if (null != olmSession) {
|
||||
olmSession.releaseSession();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of known session IDs for the given device.
|
||||
*
|
||||
* @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
|
||||
* @return a list of known session ids for the device.
|
||||
*/
|
||||
public Set<String> getSessionIds(String theirDeviceIdentityKey) {
|
||||
Map<String, OlmSession> map = mStore.getDeviceSessions(theirDeviceIdentityKey);
|
||||
|
||||
if (null != map) {
|
||||
return map.keySet();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the right olm session id for encrypting messages to the given identity key.
|
||||
*
|
||||
* @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
|
||||
* @return the session id, or nil if no established session.
|
||||
*/
|
||||
public String getSessionId(String theirDeviceIdentityKey) {
|
||||
String sessionId = null;
|
||||
Set<String> sessionIds = getSessionIds(theirDeviceIdentityKey);
|
||||
|
||||
if ((null != sessionIds) && (0 != sessionIds.size())) {
|
||||
List<String> sessionIdsList = new ArrayList<>(sessionIds);
|
||||
Collections.sort(sessionIdsList);
|
||||
sessionId = sessionIdsList.get(0);
|
||||
}
|
||||
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt an outgoing message using an existing session.
|
||||
*
|
||||
* @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
|
||||
* @param sessionId the id of the active session
|
||||
* @param payloadString the payload to be encrypted and sent
|
||||
* @return the cipher text
|
||||
*/
|
||||
public Map<String, Object> encryptMessage(String theirDeviceIdentityKey, String sessionId, String payloadString) {
|
||||
Map<String, Object> res = null;
|
||||
OlmMessage olmMessage;
|
||||
OlmSession olmSession = getSessionForDevice(theirDeviceIdentityKey, sessionId);
|
||||
|
||||
if (null != olmSession) {
|
||||
try {
|
||||
Log.d(LOG_TAG, "## encryptMessage() : olmSession.sessionIdentifier: " + olmSession.sessionIdentifier());
|
||||
//Log.d(LOG_TAG, "## encryptMessage() : payloadString: " + payloadString);
|
||||
|
||||
olmMessage = olmSession.encryptMessage(payloadString);
|
||||
mStore.storeSession(olmSession, theirDeviceIdentityKey);
|
||||
res = new HashMap<>();
|
||||
|
||||
res.put("body", olmMessage.mCipherText);
|
||||
res.put("type", olmMessage.mType);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## encryptMessage() : failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt an incoming message using an existing session.
|
||||
*
|
||||
* @param ciphertext the base64-encoded body from the received message.
|
||||
* @param messageType message_type field from the received message.
|
||||
* @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
|
||||
* @param sessionId the id of the active session.
|
||||
* @return the decrypted payload.
|
||||
*/
|
||||
public String decryptMessage(String ciphertext, int messageType, String sessionId, String theirDeviceIdentityKey) {
|
||||
String payloadString = null;
|
||||
|
||||
OlmSession olmSession = getSessionForDevice(theirDeviceIdentityKey, sessionId);
|
||||
|
||||
if (null != olmSession) {
|
||||
OlmMessage olmMessage = new OlmMessage();
|
||||
olmMessage.mCipherText = ciphertext;
|
||||
olmMessage.mType = messageType;
|
||||
|
||||
try {
|
||||
payloadString = olmSession.decryptMessage(olmMessage);
|
||||
mStore.storeSession(olmSession, theirDeviceIdentityKey);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## decryptMessage() : decryptMessage failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return payloadString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an incoming messages is a prekey message matching an existing session.
|
||||
*
|
||||
* @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
|
||||
* @param sessionId the id of the active session.
|
||||
* @param messageType message_type field from the received message.
|
||||
* @param ciphertext the base64-encoded body from the received message.
|
||||
* @return YES if the received message is a prekey message which matchesthe given session.
|
||||
*/
|
||||
public boolean matchesSession(String theirDeviceIdentityKey, String sessionId, int messageType, String ciphertext) {
|
||||
if (messageType != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
OlmSession olmSession = getSessionForDevice(theirDeviceIdentityKey, sessionId);
|
||||
return (null != olmSession) && olmSession.matchesInboundSession(ciphertext);
|
||||
}
|
||||
|
||||
|
||||
// Outbound group session
|
||||
|
||||
/**
|
||||
* Generate a new outbound group session.
|
||||
*
|
||||
* @return the session id for the outbound session.
|
||||
*/
|
||||
public String createOutboundGroupSession() {
|
||||
OlmOutboundGroupSession session = null;
|
||||
try {
|
||||
session = new OlmOutboundGroupSession();
|
||||
mOutboundGroupSessionStore.put(session.sessionIdentifier(), session);
|
||||
return session.sessionIdentifier();
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "createOutboundGroupSession " + e.getMessage(), e);
|
||||
|
||||
if (null != session) {
|
||||
session.releaseSession();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current session key of an outbound group session.
|
||||
*
|
||||
* @param sessionId the id of the outbound group session.
|
||||
* @return the base64-encoded secret key.
|
||||
*/
|
||||
public String getSessionKey(String sessionId) {
|
||||
if (!TextUtils.isEmpty(sessionId)) {
|
||||
try {
|
||||
return mOutboundGroupSessionStore.get(sessionId).sessionKey();
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## getSessionKey() : failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current message index of an outbound group session.
|
||||
*
|
||||
* @param sessionId the id of the outbound group session.
|
||||
* @return the current chain index.
|
||||
*/
|
||||
public int getMessageIndex(String sessionId) {
|
||||
if (!TextUtils.isEmpty(sessionId)) {
|
||||
return mOutboundGroupSessionStore.get(sessionId).messageIndex();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt an outgoing message with an outbound group session.
|
||||
*
|
||||
* @param sessionId the id of the outbound group session.
|
||||
* @param payloadString the payload to be encrypted and sent.
|
||||
* @return ciphertext
|
||||
*/
|
||||
public String encryptGroupMessage(String sessionId, String payloadString) {
|
||||
if (!TextUtils.isEmpty(sessionId) && !TextUtils.isEmpty(payloadString)) {
|
||||
try {
|
||||
return mOutboundGroupSessionStore.get(sessionId).encryptMessage(payloadString);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## encryptGroupMessage() : failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Inbound group session
|
||||
|
||||
/**
|
||||
* Add an inbound group session to the session store.
|
||||
*
|
||||
* @param sessionId the session identifier.
|
||||
* @param sessionKey base64-encoded secret key.
|
||||
* @param roomId the id of the room in which this session will be used.
|
||||
* @param senderKey the base64-encoded curve25519 key of the sender.
|
||||
* @param forwardingCurve25519KeyChain Devices involved in forwarding this session to us.
|
||||
* @param keysClaimed Other keys the sender claims.
|
||||
* @param exportFormat true if the megolm keys are in export format
|
||||
* @return true if the operation succeeds.
|
||||
*/
|
||||
public boolean addInboundGroupSession(String sessionId,
|
||||
String sessionKey,
|
||||
String roomId,
|
||||
String senderKey,
|
||||
List<String> forwardingCurve25519KeyChain,
|
||||
Map<String, String> keysClaimed,
|
||||
boolean exportFormat) {
|
||||
if (null != getInboundGroupSession(sessionId, senderKey, roomId)) {
|
||||
// If we already have this session, consider updating it
|
||||
Log.e(LOG_TAG, "## addInboundGroupSession() : Update for megolm session " + senderKey + "/" + sessionId);
|
||||
|
||||
// For now we just ignore updates. TODO: implement something here
|
||||
return false;
|
||||
}
|
||||
|
||||
MXOlmInboundGroupSession2 session = new MXOlmInboundGroupSession2(sessionKey, exportFormat);
|
||||
|
||||
// sanity check
|
||||
if (null == session.mSession) {
|
||||
Log.e(LOG_TAG, "## addInboundGroupSession : invalid session");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!TextUtils.equals(session.mSession.sessionIdentifier(), sessionId)) {
|
||||
Log.e(LOG_TAG, "## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: " + senderKey);
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## addInboundGroupSession : sessionIdentifier') failed " + e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
|
||||
session.mSenderKey = senderKey;
|
||||
session.mRoomId = roomId;
|
||||
session.mKeysClaimed = keysClaimed;
|
||||
session.mForwardingCurve25519KeyChain = forwardingCurve25519KeyChain;
|
||||
|
||||
mStore.storeInboundGroupSession(session);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import an inbound group session to the session store.
|
||||
*
|
||||
* @param exportedSessionMap the exported session map
|
||||
* @return the imported session if the operation succeeds.
|
||||
*/
|
||||
public MXOlmInboundGroupSession2 importInboundGroupSession(Map<String, Object> exportedSessionMap) {
|
||||
String sessionId = (String) exportedSessionMap.get("session_id");
|
||||
String senderKey = (String) exportedSessionMap.get("sender_key");
|
||||
String roomId = (String) exportedSessionMap.get("room_id");
|
||||
|
||||
if (null != getInboundGroupSession(sessionId, senderKey, roomId)) {
|
||||
// If we already have this session, consider updating it
|
||||
Log.e(LOG_TAG, "## importInboundGroupSession() : Update for megolm session " + senderKey + "/" + sessionId);
|
||||
|
||||
// For now we just ignore updates. TODO: implement something here
|
||||
return null;
|
||||
}
|
||||
|
||||
MXOlmInboundGroupSession2 session = null;
|
||||
|
||||
try {
|
||||
session = new MXOlmInboundGroupSession2(exportedSessionMap);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## importInboundGroupSession() : Update for megolm session " + senderKey + "/" + sessionId, e);
|
||||
}
|
||||
|
||||
// sanity check
|
||||
if ((null == session) || (null == session.mSession)) {
|
||||
Log.e(LOG_TAG, "## importInboundGroupSession : invalid session");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!TextUtils.equals(session.mSession.sessionIdentifier(), sessionId)) {
|
||||
Log.e(LOG_TAG, "## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: " + senderKey);
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## importInboundGroupSession : sessionIdentifier') failed " + e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
|
||||
mStore.storeInboundGroupSession(session);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an inbound group session
|
||||
*
|
||||
* @param sessionId the session identifier.
|
||||
* @param sessionKey base64-encoded secret key.
|
||||
*/
|
||||
public void removeInboundGroupSession(String sessionId, String sessionKey) {
|
||||
if ((null != sessionId) && (null != sessionKey)) {
|
||||
mStore.removeInboundGroupSession(sessionId, sessionKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt a received message with an inbound group session.
|
||||
*
|
||||
* @param body the base64-encoded body of the encrypted message.
|
||||
* @param roomId theroom in which the message was received.
|
||||
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
||||
* @param sessionId the session identifier.
|
||||
* @param senderKey the base64-encoded curve25519 key of the sender.
|
||||
* @return the decrypting result. Nil if the sessionId is unknown.
|
||||
*/
|
||||
public MXDecryptionResult decryptGroupMessage(String body,
|
||||
String roomId,
|
||||
String timeline,
|
||||
String sessionId,
|
||||
String senderKey) throws MXDecryptionException {
|
||||
MXDecryptionResult result = new MXDecryptionResult();
|
||||
MXOlmInboundGroupSession2 session = getInboundGroupSession(sessionId, senderKey, roomId);
|
||||
|
||||
if (null != session) {
|
||||
// Check that the room id matches the original one for the session. This stops
|
||||
// the HS pretending a message was targeting a different room.
|
||||
if (TextUtils.equals(roomId, session.mRoomId)) {
|
||||
String errorMessage = "";
|
||||
OlmInboundGroupSession.DecryptMessageResult decryptResult = null;
|
||||
try {
|
||||
decryptResult = session.mSession.decryptMessage(body);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## decryptGroupMessage () : decryptMessage failed " + e.getMessage(), e);
|
||||
errorMessage = e.getMessage();
|
||||
}
|
||||
|
||||
if (null != decryptResult) {
|
||||
if (null != timeline) {
|
||||
if (!mInboundGroupSessionMessageIndexes.containsKey(timeline)) {
|
||||
mInboundGroupSessionMessageIndexes.put(timeline, new HashMap<String, Boolean>());
|
||||
}
|
||||
|
||||
String messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex;
|
||||
|
||||
if (null != mInboundGroupSessionMessageIndexes.get(timeline).get(messageIndexKey)) {
|
||||
String reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex);
|
||||
Log.e(LOG_TAG, "## decryptGroupMessage() : " + reason);
|
||||
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.DUPLICATED_MESSAGE_INDEX_ERROR_CODE,
|
||||
MXCryptoError.UNABLE_TO_DECRYPT, reason));
|
||||
}
|
||||
|
||||
mInboundGroupSessionMessageIndexes.get(timeline).put(messageIndexKey, true);
|
||||
}
|
||||
|
||||
mStore.storeInboundGroupSession(session);
|
||||
try {
|
||||
JsonParser parser = new JsonParser();
|
||||
result.mPayload = parser.parse(JsonUtils.convertFromUTF8(decryptResult.mDecryptedMessage));
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## decryptGroupMessage() : RLEncoder.encode failed " + e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (null == result.mPayload) {
|
||||
Log.e(LOG_TAG, "## decryptGroupMessage() : fails to parse the payload");
|
||||
return null;
|
||||
}
|
||||
|
||||
result.mKeysClaimed = session.mKeysClaimed;
|
||||
result.mSenderKey = senderKey;
|
||||
result.mForwardingCurve25519KeyChain = session.mForwardingCurve25519KeyChain;
|
||||
} else {
|
||||
Log.e(LOG_TAG, "## decryptGroupMessage() : failed to decode the message");
|
||||
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.OLM_ERROR_CODE, errorMessage, null));
|
||||
}
|
||||
} else {
|
||||
String reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.mRoomId);
|
||||
Log.e(LOG_TAG, "## decryptGroupMessage() : " + reason);
|
||||
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_ERROR_CODE,
|
||||
MXCryptoError.UNABLE_TO_DECRYPT, reason));
|
||||
}
|
||||
} else {
|
||||
Log.e(LOG_TAG, "## decryptGroupMessage() : Cannot retrieve inbound group session " + sessionId);
|
||||
throw new MXDecryptionException(mInboundGroupSessionWithIdError);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset replay attack data for the given timeline.
|
||||
*
|
||||
* @param timeline the id of the timeline.
|
||||
*/
|
||||
public void resetReplayAttackCheckInTimeline(String timeline) {
|
||||
if (null != timeline) {
|
||||
mInboundGroupSessionMessageIndexes.remove(timeline);
|
||||
}
|
||||
}
|
||||
|
||||
// Utilities
|
||||
|
||||
/**
|
||||
* Verify an ed25519 signature on a JSON object.
|
||||
*
|
||||
* @param key the ed25519 key.
|
||||
* @param JSONDictinary the JSON object which was signed.
|
||||
* @param signature the base64-encoded signature to be checked.
|
||||
* @throws Exception the exception
|
||||
*/
|
||||
public void verifySignature(String key, Map<String, Object> JSONDictinary, String signature) throws Exception {
|
||||
// Check signature on the canonical version of the JSON
|
||||
mOlmUtility.verifyEd25519Signature(signature, key, JsonUtils.getCanonicalizedJsonString(JSONDictinary));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the SHA-256 hash of the input and encodes it as base64.
|
||||
*
|
||||
* @param message the message to hash.
|
||||
* @return the base64-encoded hash value.
|
||||
*/
|
||||
public String sha256(String message) {
|
||||
return mOlmUtility.sha256(JsonUtils.convertToUTF8(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Search an OlmSession
|
||||
*
|
||||
* @param theirDeviceIdentityKey the device key
|
||||
* @param sessionId the session Id
|
||||
* @return the olm session
|
||||
*/
|
||||
private OlmSession getSessionForDevice(String theirDeviceIdentityKey, String sessionId) {
|
||||
// sanity check
|
||||
if (!TextUtils.isEmpty(theirDeviceIdentityKey) && !TextUtils.isEmpty(sessionId)) {
|
||||
Map<String, OlmSession> map = mStore.getDeviceSessions(theirDeviceIdentityKey);
|
||||
|
||||
if (null != map) {
|
||||
return map.get(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract an InboundGroupSession from the session store and do some check.
|
||||
* mInboundGroupSessionWithIdError describes the failure reason.
|
||||
*
|
||||
* @param roomId the room where the sesion is used.
|
||||
* @param sessionId the session identifier.
|
||||
* @param senderKey the base64-encoded curve25519 key of the sender.
|
||||
* @return the inbound group session.
|
||||
*/
|
||||
public MXOlmInboundGroupSession2 getInboundGroupSession(String sessionId, String senderKey, String roomId) {
|
||||
mInboundGroupSessionWithIdError = null;
|
||||
|
||||
MXOlmInboundGroupSession2 session = mStore.getInboundGroupSession(sessionId, senderKey);
|
||||
|
||||
if (null != session) {
|
||||
// Check that the room id matches the original one for the session. This stops
|
||||
// the HS pretending a message was targeting a different room.
|
||||
if (!TextUtils.equals(roomId, session.mRoomId)) {
|
||||
String errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.mRoomId);
|
||||
Log.e(LOG_TAG, "## getInboundGroupSession() : " + errorDescription);
|
||||
mInboundGroupSessionWithIdError = new MXCryptoError(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_ERROR_CODE,
|
||||
MXCryptoError.UNABLE_TO_DECRYPT, errorDescription);
|
||||
}
|
||||
} else {
|
||||
Log.e(LOG_TAG, "## getInboundGroupSession() : Cannot retrieve inbound group session " + sessionId);
|
||||
mInboundGroupSessionWithIdError = new MXCryptoError(MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE,
|
||||
MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON, null);
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if we have the keys for a given megolm session.
|
||||
*
|
||||
* @param roomId room in which the message was received
|
||||
* @param senderKey base64-encoded curve25519 key of the sender
|
||||
* @param sessionId session identifier
|
||||
* @return true if the unbound session keys are known.
|
||||
*/
|
||||
public boolean hasInboundSessionKeys(String roomId, String senderKey, String sessionId) {
|
||||
return null != getInboundGroupSession(sessionId, senderKey, roomId);
|
||||
}
|
||||
}
|
@ -0,0 +1,371 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto;
|
||||
|
||||
import android.os.Handler;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.MXSession;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXUsersDevicesMap;
|
||||
import im.vector.matrix.android.internal.legacy.data.cryptostore.IMXCryptoStore;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.crypto.RoomKeyRequest;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class MXOutgoingRoomKeyRequestManager {
|
||||
private static final String LOG_TAG = MXOutgoingRoomKeyRequestManager.class.getSimpleName();
|
||||
|
||||
private static final int SEND_KEY_REQUESTS_DELAY_MS = 500;
|
||||
|
||||
// the linked session
|
||||
private MXSession mSession;
|
||||
|
||||
// working handler (should not be the UI thread)
|
||||
private Handler mWorkingHandler;
|
||||
|
||||
// store
|
||||
private IMXCryptoStore mCryptoStore;
|
||||
|
||||
// running
|
||||
public boolean mClientRunning;
|
||||
|
||||
// transaction counter
|
||||
private int mTxnCtr;
|
||||
|
||||
// sanity check to ensure that we don't end up with two concurrent runs
|
||||
// of mSendOutgoingRoomKeyRequestsTimer
|
||||
private boolean mSendOutgoingRoomKeyRequestsRunning;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param session the session
|
||||
* @param crypto the crypto engine
|
||||
*/
|
||||
public MXOutgoingRoomKeyRequestManager(MXSession session, MXCrypto crypto) {
|
||||
mSession = session;
|
||||
mWorkingHandler = crypto.getEncryptingThreadHandler();
|
||||
mCryptoStore = crypto.getCryptoStore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the client is started. Sets background processes running.
|
||||
*/
|
||||
public void start() {
|
||||
mClientRunning = true;
|
||||
startTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the client is stopped. Stops any running background processes.
|
||||
*/
|
||||
public void stop() {
|
||||
mClientRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make up a new transaction id
|
||||
*
|
||||
* @return {string} a new, unique, transaction id
|
||||
*/
|
||||
private String makeTxnId() {
|
||||
return "m" + System.currentTimeMillis() + "." + mTxnCtr++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send off a room key request, if we haven't already done so.
|
||||
* <p>
|
||||
* The `requestBody` is compared (with a deep-equality check) against
|
||||
* previous queued or sent requests and if it matches, no change is made.
|
||||
* Otherwise, a request is added to the pending list, and a job is started
|
||||
* in the background to send it.
|
||||
*
|
||||
* @param requestBody requestBody
|
||||
* @param recipients recipients
|
||||
*/
|
||||
public void sendRoomKeyRequest(final Map<String, String> requestBody, final List<Map<String, String>> recipients) {
|
||||
mWorkingHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
OutgoingRoomKeyRequest req = mCryptoStore.getOrAddOutgoingRoomKeyRequest(
|
||||
new OutgoingRoomKeyRequest(requestBody, recipients, makeTxnId(), OutgoingRoomKeyRequest.RequestState.UNSENT));
|
||||
|
||||
|
||||
if (req.mState == OutgoingRoomKeyRequest.RequestState.UNSENT) {
|
||||
startTimer();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel room key requests, if any match the given details
|
||||
*
|
||||
* @param requestBody requestBody
|
||||
*/
|
||||
public void cancelRoomKeyRequest(final Map<String, String> requestBody) {
|
||||
cancelRoomKeyRequest(requestBody, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel room key requests, if any match the given details, and resend
|
||||
*
|
||||
* @param requestBody requestBody
|
||||
*/
|
||||
public void resendRoomKeyRequest(final Map<String, String> requestBody) {
|
||||
cancelRoomKeyRequest(requestBody, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel room key requests, if any match the given details, and resend
|
||||
*
|
||||
* @param requestBody requestBody
|
||||
* @param andResend true to resend the key request
|
||||
*/
|
||||
private void cancelRoomKeyRequest(final Map<String, String> requestBody, boolean andResend) {
|
||||
OutgoingRoomKeyRequest req = mCryptoStore.getOutgoingRoomKeyRequest(requestBody);
|
||||
|
||||
if (null == req) {
|
||||
// no request was made for this key
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.mState == OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING
|
||||
|| req.mState == OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND) {
|
||||
// nothing to do here
|
||||
} else if ((req.mState == OutgoingRoomKeyRequest.RequestState.UNSENT)
|
||||
|| (req.mState == OutgoingRoomKeyRequest.RequestState.FAILED)) {
|
||||
Log.d(LOG_TAG, "## cancelRoomKeyRequest() : deleting unnecessary room key request for " + requestBody);
|
||||
mCryptoStore.deleteOutgoingRoomKeyRequest(req.mRequestId);
|
||||
} else if (req.mState == OutgoingRoomKeyRequest.RequestState.SENT) {
|
||||
if (andResend) {
|
||||
req.mState = OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND;
|
||||
} else {
|
||||
req.mState = OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING;
|
||||
}
|
||||
req.mCancellationTxnId = makeTxnId();
|
||||
mCryptoStore.updateOutgoingRoomKeyRequest(req);
|
||||
sendOutgoingRoomKeyRequestCancellation(req);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Start the background timer to send queued requests, if the timer isn't already running.
|
||||
*/
|
||||
private void startTimer() {
|
||||
mWorkingHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mSendOutgoingRoomKeyRequestsRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
mWorkingHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mSendOutgoingRoomKeyRequestsRunning) {
|
||||
Log.d(LOG_TAG, "## startTimer() : RoomKeyRequestSend already in progress!");
|
||||
return;
|
||||
}
|
||||
|
||||
mSendOutgoingRoomKeyRequestsRunning = true;
|
||||
sendOutgoingRoomKeyRequests();
|
||||
}
|
||||
}, SEND_KEY_REQUESTS_DELAY_MS);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// look for and send any queued requests. Runs itself recursively until
|
||||
// there are no more requests, or there is an error (in which case, the
|
||||
// timer will be restarted before the promise resolves).
|
||||
private void sendOutgoingRoomKeyRequests() {
|
||||
if (!mClientRunning) {
|
||||
mSendOutgoingRoomKeyRequestsRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(LOG_TAG, "## sendOutgoingRoomKeyRequests() : Looking for queued outgoing room key requests");
|
||||
OutgoingRoomKeyRequest outgoingRoomKeyRequest = mCryptoStore.getOutgoingRoomKeyRequestByState(
|
||||
new HashSet<>(Arrays.asList(OutgoingRoomKeyRequest.RequestState.UNSENT,
|
||||
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING,
|
||||
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND)));
|
||||
|
||||
if (null == outgoingRoomKeyRequest) {
|
||||
Log.e(LOG_TAG, "## sendOutgoingRoomKeyRequests() : No more outgoing room key requests");
|
||||
mSendOutgoingRoomKeyRequestsRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (OutgoingRoomKeyRequest.RequestState.UNSENT == outgoingRoomKeyRequest.mState) {
|
||||
sendOutgoingRoomKeyRequest(outgoingRoomKeyRequest);
|
||||
} else {
|
||||
sendOutgoingRoomKeyRequestCancellation(outgoingRoomKeyRequest);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the outgoing key request.
|
||||
*
|
||||
* @param request the request
|
||||
*/
|
||||
private void sendOutgoingRoomKeyRequest(final OutgoingRoomKeyRequest request) {
|
||||
Log.d(LOG_TAG, "## sendOutgoingRoomKeyRequest() : Requesting keys " + request.mRequestBody
|
||||
+ " from " + request.mRecipients + " id " + request.mRequestId);
|
||||
|
||||
Map<String, Object> requestMessage = new HashMap<>();
|
||||
requestMessage.put("action", "request");
|
||||
requestMessage.put("requesting_device_id", mCryptoStore.getDeviceId());
|
||||
requestMessage.put("request_id", request.mRequestId);
|
||||
requestMessage.put("body", request.mRequestBody);
|
||||
|
||||
sendMessageToDevices(requestMessage, request.mRecipients, request.mRequestId, new ApiCallback<Void>() {
|
||||
private void onDone(final OutgoingRoomKeyRequest.RequestState state) {
|
||||
mWorkingHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (request.mState != OutgoingRoomKeyRequest.RequestState.UNSENT) {
|
||||
Log.d(LOG_TAG, "## sendOutgoingRoomKeyRequest() : Cannot update room key request from UNSENT as it was already updated to "
|
||||
+ request.mState);
|
||||
} else {
|
||||
request.mState = state;
|
||||
mCryptoStore.updateOutgoingRoomKeyRequest(request);
|
||||
}
|
||||
|
||||
mSendOutgoingRoomKeyRequestsRunning = false;
|
||||
startTimer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(Void info) {
|
||||
Log.d(LOG_TAG, "## sendOutgoingRoomKeyRequest succeed");
|
||||
onDone(OutgoingRoomKeyRequest.RequestState.SENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
Log.e(LOG_TAG, "## sendOutgoingRoomKeyRequest failed " + e.getMessage(), e);
|
||||
onDone(OutgoingRoomKeyRequest.RequestState.FAILED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
Log.e(LOG_TAG, "## sendOutgoingRoomKeyRequest failed " + e.getMessage());
|
||||
onDone(OutgoingRoomKeyRequest.RequestState.FAILED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
Log.e(LOG_TAG, "## sendOutgoingRoomKeyRequest failed " + e.getMessage(), e);
|
||||
onDone(OutgoingRoomKeyRequest.RequestState.FAILED);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a RoomKeyRequest, cancel it and delete the request record
|
||||
*
|
||||
* @param request the request
|
||||
*/
|
||||
private void sendOutgoingRoomKeyRequestCancellation(final OutgoingRoomKeyRequest request) {
|
||||
Log.d(LOG_TAG, "## sendOutgoingRoomKeyRequestCancellation() : Sending cancellation for key request for " + request.mRequestBody
|
||||
+ " to " + request.mRecipients
|
||||
+ " cancellation id " + request.mCancellationTxnId);
|
||||
|
||||
Map<String, Object> requestMessageMap = new HashMap<>();
|
||||
requestMessageMap.put("action", RoomKeyRequest.ACTION_REQUEST_CANCELLATION);
|
||||
requestMessageMap.put("requesting_device_id", mCryptoStore.getDeviceId());
|
||||
requestMessageMap.put("request_id", request.mCancellationTxnId);
|
||||
|
||||
sendMessageToDevices(requestMessageMap, request.mRecipients, request.mCancellationTxnId, new ApiCallback<Void>() {
|
||||
private void onDone() {
|
||||
mWorkingHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mCryptoStore.deleteOutgoingRoomKeyRequest(request.mRequestId);
|
||||
mSendOutgoingRoomKeyRequestsRunning = false;
|
||||
startTimer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onSuccess(Void info) {
|
||||
Log.d(LOG_TAG, "## sendOutgoingRoomKeyRequestCancellation() : done");
|
||||
boolean resend = request.mState == OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND;
|
||||
|
||||
onDone();
|
||||
|
||||
// Resend the request with a new ID
|
||||
if (resend) {
|
||||
sendRoomKeyRequest(request.mRequestBody, request.mRecipients);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
Log.e(LOG_TAG, "## sendOutgoingRoomKeyRequestCancellation failed " + e.getMessage(), e);
|
||||
onDone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
Log.e(LOG_TAG, "## sendOutgoingRoomKeyRequestCancellation failed " + e.getMessage());
|
||||
onDone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
Log.e(LOG_TAG, "## sendOutgoingRoomKeyRequestCancellation failed " + e.getMessage(), e);
|
||||
onDone();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a RoomKeyRequest to a list of recipients
|
||||
*
|
||||
* @param message the message
|
||||
* @param recipients the recipients.
|
||||
* @param transactionId the transaction id
|
||||
* @param callback the asynchronous callback.
|
||||
*/
|
||||
private void sendMessageToDevices(final Map<String, Object> message,
|
||||
List<Map<String, String>> recipients,
|
||||
String transactionId,
|
||||
final ApiCallback<Void> callback) {
|
||||
MXUsersDevicesMap<Map<String, Object>> contentMap = new MXUsersDevicesMap<>();
|
||||
|
||||
for (Map<String, String> recipient : recipients) {
|
||||
contentMap.setObject(message, recipient.get("userId"), recipient.get("deviceId"));
|
||||
}
|
||||
|
||||
mSession.getCryptoRestClient().sendToDevice(Event.EVENT_TYPE_ROOM_KEY_REQUEST, contentMap, transactionId, callback);
|
||||
}
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Represents an outgoing room key request
|
||||
*/
|
||||
public class OutgoingRoomKeyRequest implements Serializable {
|
||||
|
||||
/**
|
||||
* possible states for a room key request
|
||||
*
|
||||
* The state machine looks like:
|
||||
*
|
||||
* |
|
||||
* V
|
||||
* UNSENT -----------------------------+
|
||||
* | |
|
||||
* | (send successful) | (cancellation requested)
|
||||
* V |
|
||||
* SENT |
|
||||
* |-------------------------------- | --------------+
|
||||
* | | |
|
||||
* | | | (cancellation requested with intent
|
||||
* | | | to resend a new request)
|
||||
* | (cancellation requested) | |
|
||||
* V | V
|
||||
* CANCELLATION_PENDING | CANCELLATION_PENDING_AND_WILL_RESEND
|
||||
* | | |
|
||||
* | (cancellation sent) | | (cancellation sent. Create new request
|
||||
* | | | in the UNSENT state)
|
||||
* V | |
|
||||
* (deleted) <---------------------------+----------------+
|
||||
*/
|
||||
|
||||
public enum RequestState {
|
||||
/**
|
||||
* request not yet sent
|
||||
*/
|
||||
UNSENT,
|
||||
/**
|
||||
* request sent, awaiting reply
|
||||
*/
|
||||
SENT,
|
||||
/**
|
||||
* reply received, cancellation not yet sent
|
||||
*/
|
||||
CANCELLATION_PENDING,
|
||||
/**
|
||||
* Cancellation not yet sent, once sent, a new request will be done
|
||||
*/
|
||||
CANCELLATION_PENDING_AND_WILL_RESEND,
|
||||
/**
|
||||
* sending failed
|
||||
*/
|
||||
FAILED
|
||||
}
|
||||
|
||||
// Unique id for this request. Used for both
|
||||
// an id within the request for later pairing with a cancellation, and for
|
||||
// the transaction id when sending the to_device messages to our local
|
||||
public String mRequestId;
|
||||
|
||||
// transaction id for the cancellation, if any
|
||||
public String mCancellationTxnId;
|
||||
|
||||
// list of recipients for the request
|
||||
public List<Map<String, String>> mRecipients;
|
||||
|
||||
// RequestBody
|
||||
public Map<String, String> mRequestBody;
|
||||
|
||||
// current state of this request
|
||||
public RequestState mState;
|
||||
|
||||
public OutgoingRoomKeyRequest(Map<String, String> requestBody, List<Map<String, String>> recipients, String requestId, RequestState state) {
|
||||
mRequestBody = requestBody;
|
||||
mRecipients = recipients;
|
||||
mRequestId = requestId;
|
||||
mState = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the room id
|
||||
*/
|
||||
public String getRoomId() {
|
||||
if (null != mRequestBody) {
|
||||
return mRequestBody.get("room_id");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the session id
|
||||
*/
|
||||
public String getSessionId() {
|
||||
if (null != mRequestBody) {
|
||||
return mRequestBody.get("session_id");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto.algorithms;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.MXSession;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.IncomingRoomKeyRequest;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.MXDecryptionException;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.MXEventDecryptionResult;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
|
||||
/**
|
||||
* An interface for decrypting data
|
||||
*/
|
||||
public interface IMXDecrypting {
|
||||
/**
|
||||
* Init the object fields
|
||||
*
|
||||
* @param matrixSession the session
|
||||
*/
|
||||
void initWithMatrixSession(MXSession matrixSession);
|
||||
|
||||
/**
|
||||
* Decrypt an event
|
||||
*
|
||||
* @param event the raw event.
|
||||
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
||||
* @return the decryption information, or null in case of error
|
||||
* @throws MXDecryptionException the decryption failure reason
|
||||
*/
|
||||
@Nullable
|
||||
MXEventDecryptionResult decryptEvent(Event event, String timeline) throws MXDecryptionException;
|
||||
|
||||
/**
|
||||
* Handle a key event.
|
||||
*
|
||||
* @param event the key event.
|
||||
*/
|
||||
void onRoomKeyEvent(Event event);
|
||||
|
||||
/**
|
||||
* Check if the some messages can be decrypted with a new session
|
||||
*
|
||||
* @param senderKey the session sender key
|
||||
* @param sessionId the session id
|
||||
*/
|
||||
void onNewSession(String senderKey, String sessionId);
|
||||
|
||||
/**
|
||||
* Determine if we have the keys necessary to respond to a room key request
|
||||
*
|
||||
* @param request keyRequest
|
||||
* @return true if we have the keys and could (theoretically) share
|
||||
*/
|
||||
boolean hasKeysForKeyRequest(IncomingRoomKeyRequest request);
|
||||
|
||||
/**
|
||||
* Send the response to a room key request.
|
||||
*
|
||||
* @param request keyRequest
|
||||
*/
|
||||
void shareKeysWithDevice(IncomingRoomKeyRequest request);
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto.algorithms;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.MXSession;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An interface for encrypting data
|
||||
*/
|
||||
public interface IMXEncrypting {
|
||||
|
||||
/**
|
||||
* Init
|
||||
*
|
||||
* @param matrixSession the related 'MXSession'.
|
||||
* @param roomId the id of the room we will be sending to.
|
||||
*/
|
||||
void initWithMatrixSession(MXSession matrixSession, String roomId);
|
||||
|
||||
/**
|
||||
* Encrypt an event content according to the configuration of the room.
|
||||
*
|
||||
* @param eventContent the content of the event.
|
||||
* @param eventType the type of the event.
|
||||
* @param userIds the room members the event will be sent to.
|
||||
* @param callback the asynchronous callback
|
||||
*/
|
||||
void encryptEventContent(JsonElement eventContent, String eventType, List<String> userIds, ApiCallback<JsonElement> callback);
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto.algorithms;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* This class represents the decryption result.
|
||||
*/
|
||||
public class MXDecryptionResult {
|
||||
/**
|
||||
* The decrypted payload (with properties 'type', 'content')
|
||||
*/
|
||||
public JsonElement mPayload;
|
||||
|
||||
/**
|
||||
* keys that the sender of the event claims ownership of:
|
||||
* map from key type to base64-encoded key.
|
||||
*/
|
||||
public Map<String, String> mKeysClaimed;
|
||||
|
||||
/**
|
||||
* The curve25519 key that the sender of the event is known to have ownership of.
|
||||
*/
|
||||
public String mSenderKey;
|
||||
|
||||
/**
|
||||
* Devices which forwarded this session to us (normally empty).
|
||||
*/
|
||||
public List<String> mForwardingCurve25519KeyChain;
|
||||
}
|
@ -0,0 +1,468 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto.algorithms.megolm;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.MXSession;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.IncomingRoomKeyRequest;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.MXCryptoError;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.MXDecryptionException;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.MXEventDecryptionResult;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.MXOlmDevice;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.algorithms.IMXDecrypting;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.algorithms.MXDecryptionResult;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXDeviceInfo;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXOlmInboundGroupSession2;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXOlmSessionResult;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXUsersDevicesMap;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.crypto.EncryptedEventContent;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.crypto.ForwardedRoomKeyContent;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.crypto.RoomKeyContent;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.crypto.RoomKeyRequestBody;
|
||||
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class MXMegolmDecryption implements IMXDecrypting {
|
||||
private static final String LOG_TAG = MXMegolmDecryption.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* The olm device interface
|
||||
*/
|
||||
private MXOlmDevice mOlmDevice;
|
||||
|
||||
// the matrix session
|
||||
private MXSession mSession;
|
||||
|
||||
/**
|
||||
* Events which we couldn't decrypt due to unknown sessions / indexes: map from
|
||||
* senderKey|sessionId to timelines to list of MatrixEvents.
|
||||
*/
|
||||
private Map<String, /* senderKey|sessionId */
|
||||
Map<String /* timelineId */, List<Event>>> mPendingEvents;
|
||||
|
||||
/**
|
||||
* Init the object fields
|
||||
*
|
||||
* @param matrixSession the matrix session
|
||||
*/
|
||||
@Override
|
||||
public void initWithMatrixSession(MXSession matrixSession) {
|
||||
mSession = matrixSession;
|
||||
mOlmDevice = matrixSession.getCrypto().getOlmDevice();
|
||||
mPendingEvents = new HashMap<>();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public MXEventDecryptionResult decryptEvent(Event event, String timeline) throws MXDecryptionException {
|
||||
return decryptEvent(event, timeline, true);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private MXEventDecryptionResult decryptEvent(Event event, String timeline, boolean requestKeysOnFail) throws MXDecryptionException {
|
||||
// sanity check
|
||||
if (null == event) {
|
||||
Log.e(LOG_TAG, "## decryptEvent() : null event");
|
||||
return null;
|
||||
}
|
||||
|
||||
EncryptedEventContent encryptedEventContent = JsonUtils.toEncryptedEventContent(event.getWireContent().getAsJsonObject());
|
||||
|
||||
String senderKey = encryptedEventContent.sender_key;
|
||||
String ciphertext = encryptedEventContent.ciphertext;
|
||||
String sessionId = encryptedEventContent.session_id;
|
||||
|
||||
if (TextUtils.isEmpty(senderKey) || TextUtils.isEmpty(sessionId) || TextUtils.isEmpty(ciphertext)) {
|
||||
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.MISSING_FIELDS_ERROR_CODE,
|
||||
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_FIELDS_REASON));
|
||||
}
|
||||
|
||||
MXEventDecryptionResult eventDecryptionResult = null;
|
||||
MXCryptoError cryptoError = null;
|
||||
MXDecryptionResult decryptGroupMessageResult = null;
|
||||
|
||||
try {
|
||||
decryptGroupMessageResult = mOlmDevice.decryptGroupMessage(ciphertext, event.roomId, timeline, sessionId, senderKey);
|
||||
} catch (MXDecryptionException e) {
|
||||
cryptoError = e.getCryptoError();
|
||||
}
|
||||
|
||||
// the decryption succeeds
|
||||
if ((null != decryptGroupMessageResult) && (null != decryptGroupMessageResult.mPayload) && (null == cryptoError)) {
|
||||
eventDecryptionResult = new MXEventDecryptionResult();
|
||||
|
||||
eventDecryptionResult.mClearEvent = decryptGroupMessageResult.mPayload;
|
||||
eventDecryptionResult.mSenderCurve25519Key = decryptGroupMessageResult.mSenderKey;
|
||||
|
||||
if (null != decryptGroupMessageResult.mKeysClaimed) {
|
||||
eventDecryptionResult.mClaimedEd25519Key = decryptGroupMessageResult.mKeysClaimed.get("ed25519");
|
||||
}
|
||||
|
||||
eventDecryptionResult.mForwardingCurve25519KeyChain = decryptGroupMessageResult.mForwardingCurve25519KeyChain;
|
||||
} else if (null != cryptoError) {
|
||||
if (cryptoError.isOlmError()) {
|
||||
if (TextUtils.equals("UNKNOWN_MESSAGE_INDEX", cryptoError.error)) {
|
||||
addEventToPendingList(event, timeline);
|
||||
|
||||
if (requestKeysOnFail) {
|
||||
requestKeysForEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
String reason = String.format(MXCryptoError.OLM_REASON, cryptoError.error);
|
||||
String detailedReason = String.format(MXCryptoError.DETAILLED_OLM_REASON, ciphertext, cryptoError.error);
|
||||
|
||||
throw new MXDecryptionException(new MXCryptoError(
|
||||
MXCryptoError.OLM_ERROR_CODE,
|
||||
reason,
|
||||
detailedReason));
|
||||
} else if (TextUtils.equals(cryptoError.errcode, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE)) {
|
||||
addEventToPendingList(event, timeline);
|
||||
if (requestKeysOnFail) {
|
||||
requestKeysForEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
throw new MXDecryptionException(cryptoError);
|
||||
}
|
||||
|
||||
return eventDecryptionResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for the real decryptEvent and for _retryDecryption. If
|
||||
* requestKeysOnFail is true, we'll send an m.room_key_request when we fail
|
||||
* to decrypt the event due to missing megolm keys.
|
||||
*
|
||||
* @param event the event
|
||||
*/
|
||||
private void requestKeysForEvent(Event event) {
|
||||
String sender = event.getSender();
|
||||
EncryptedEventContent wireContent = JsonUtils.toEncryptedEventContent(event.getWireContent());
|
||||
|
||||
List<Map<String, String>> recipients = new ArrayList<>();
|
||||
|
||||
Map<String, String> selfMap = new HashMap<>();
|
||||
selfMap.put("userId", mSession.getMyUserId());
|
||||
selfMap.put("deviceId", "*");
|
||||
recipients.add(selfMap);
|
||||
|
||||
if (!TextUtils.equals(sender, mSession.getMyUserId())) {
|
||||
Map<String, String> senderMap = new HashMap<>();
|
||||
senderMap.put("userId", sender);
|
||||
senderMap.put("deviceId", wireContent.device_id);
|
||||
recipients.add(senderMap);
|
||||
}
|
||||
|
||||
Map<String, String> requestBody = new HashMap<>();
|
||||
requestBody.put("room_id", event.roomId);
|
||||
requestBody.put("algorithm", wireContent.algorithm);
|
||||
requestBody.put("sender_key", wireContent.sender_key);
|
||||
requestBody.put("session_id", wireContent.session_id);
|
||||
|
||||
mSession.getCrypto().requestRoomKey(requestBody, recipients);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event to the list of those we couldn't decrypt the first time we
|
||||
* saw them.
|
||||
*
|
||||
* @param event the event to try to decrypt later
|
||||
* @param timelineId the timeline identifier
|
||||
*/
|
||||
private void addEventToPendingList(Event event, String timelineId) {
|
||||
EncryptedEventContent encryptedEventContent = JsonUtils.toEncryptedEventContent(event.getWireContent().getAsJsonObject());
|
||||
|
||||
String senderKey = encryptedEventContent.sender_key;
|
||||
String sessionId = encryptedEventContent.session_id;
|
||||
|
||||
String k = senderKey + "|" + sessionId;
|
||||
|
||||
// avoid undefined timelineId
|
||||
if (TextUtils.isEmpty(timelineId)) {
|
||||
timelineId = "";
|
||||
}
|
||||
|
||||
if (!mPendingEvents.containsKey(k)) {
|
||||
mPendingEvents.put(k, new HashMap<String, List<Event>>());
|
||||
}
|
||||
|
||||
if (!mPendingEvents.get(k).containsKey(timelineId)) {
|
||||
mPendingEvents.get(k).put(timelineId, new ArrayList<Event>());
|
||||
}
|
||||
|
||||
if (mPendingEvents.get(k).get(timelineId).indexOf(event) < 0) {
|
||||
Log.d(LOG_TAG, "## addEventToPendingList() : add Event " + event.eventId + " in room id " + event.roomId);
|
||||
mPendingEvents.get(k).get(timelineId).add(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a key event.
|
||||
*
|
||||
* @param roomKeyEvent the key event.
|
||||
*/
|
||||
@Override
|
||||
public void onRoomKeyEvent(Event roomKeyEvent) {
|
||||
boolean exportFormat = false;
|
||||
RoomKeyContent roomKeyContent = JsonUtils.toRoomKeyContent(roomKeyEvent.getContentAsJsonObject());
|
||||
|
||||
String roomId = roomKeyContent.room_id;
|
||||
String sessionId = roomKeyContent.session_id;
|
||||
String sessionKey = roomKeyContent.session_key;
|
||||
String senderKey = roomKeyEvent.senderKey();
|
||||
Map<String, String> keysClaimed = new HashMap<>();
|
||||
List<String> forwarding_curve25519_key_chain = null;
|
||||
|
||||
if (TextUtils.isEmpty(roomId) || TextUtils.isEmpty(sessionId) || TextUtils.isEmpty(sessionKey)) {
|
||||
Log.e(LOG_TAG, "## onRoomKeyEvent() : Key event is missing fields");
|
||||
return;
|
||||
}
|
||||
|
||||
if (TextUtils.equals(roomKeyEvent.getType(), Event.EVENT_TYPE_FORWARDED_ROOM_KEY)) {
|
||||
Log.d(LOG_TAG, "## onRoomKeyEvent(), forward adding key : roomId " + roomId + " sessionId " + sessionId
|
||||
+ " sessionKey " + sessionKey); // from " + event);
|
||||
ForwardedRoomKeyContent forwardedRoomKeyContent = JsonUtils.toForwardedRoomKeyContent(roomKeyEvent.getContentAsJsonObject());
|
||||
|
||||
if (null == forwardedRoomKeyContent.forwarding_curve25519_key_chain) {
|
||||
forwarding_curve25519_key_chain = new ArrayList<>();
|
||||
} else {
|
||||
forwarding_curve25519_key_chain = new ArrayList<>(forwardedRoomKeyContent.forwarding_curve25519_key_chain);
|
||||
}
|
||||
|
||||
forwarding_curve25519_key_chain.add(senderKey);
|
||||
|
||||
exportFormat = true;
|
||||
senderKey = forwardedRoomKeyContent.sender_key;
|
||||
if (null == senderKey) {
|
||||
Log.e(LOG_TAG, "## onRoomKeyEvent() : forwarded_room_key event is missing sender_key field");
|
||||
return;
|
||||
}
|
||||
|
||||
String ed25519Key = forwardedRoomKeyContent.sender_claimed_ed25519_key;
|
||||
|
||||
if (null == ed25519Key) {
|
||||
Log.e(LOG_TAG, "## forwarded_room_key_event is missing sender_claimed_ed25519_key field");
|
||||
return;
|
||||
}
|
||||
|
||||
keysClaimed.put("ed25519", ed25519Key);
|
||||
} else {
|
||||
Log.d(LOG_TAG, "## onRoomKeyEvent(), Adding key : roomId " + roomId + " sessionId " + sessionId
|
||||
+ " sessionKey " + sessionKey); // from " + event);
|
||||
|
||||
if (null == senderKey) {
|
||||
Log.e(LOG_TAG, "## onRoomKeyEvent() : key event has no sender key (not encrypted?)");
|
||||
return;
|
||||
}
|
||||
|
||||
// inherit the claimed ed25519 key from the setup message
|
||||
keysClaimed = roomKeyEvent.getKeysClaimed();
|
||||
}
|
||||
|
||||
mOlmDevice.addInboundGroupSession(sessionId, sessionKey, roomId, senderKey, forwarding_curve25519_key_chain, keysClaimed, exportFormat);
|
||||
|
||||
Map<String, String> content = new HashMap<>();
|
||||
content.put("algorithm", roomKeyContent.algorithm);
|
||||
content.put("room_id", roomKeyContent.room_id);
|
||||
content.put("session_id", roomKeyContent.session_id);
|
||||
content.put("sender_key", senderKey);
|
||||
mSession.getCrypto().cancelRoomKeyRequest(content);
|
||||
|
||||
onNewSession(senderKey, sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the some messages can be decrypted with a new session
|
||||
*
|
||||
* @param senderKey the session sender key
|
||||
* @param sessionId the session id
|
||||
*/
|
||||
public void onNewSession(String senderKey, String sessionId) {
|
||||
String k = senderKey + "|" + sessionId;
|
||||
|
||||
Map<String, List<Event>> pending = mPendingEvents.get(k);
|
||||
|
||||
if (null != pending) {
|
||||
// Have another go at decrypting events sent with this session.
|
||||
mPendingEvents.remove(k);
|
||||
|
||||
Set<String> timelineIds = pending.keySet();
|
||||
|
||||
for (String timelineId : timelineIds) {
|
||||
List<Event> events = pending.get(timelineId);
|
||||
|
||||
for (Event event : events) {
|
||||
MXEventDecryptionResult result = null;
|
||||
|
||||
try {
|
||||
result = decryptEvent(event, TextUtils.isEmpty(timelineId) ? null : timelineId);
|
||||
} catch (MXDecryptionException e) {
|
||||
Log.e(LOG_TAG, "## onNewSession() : Still can't decrypt " + event.eventId + ". Error " + e.getMessage(), e);
|
||||
event.setCryptoError(e.getCryptoError());
|
||||
}
|
||||
|
||||
if (null != result) {
|
||||
final Event fEvent = event;
|
||||
final MXEventDecryptionResult fResut = result;
|
||||
mSession.getCrypto().getUIHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
fEvent.setClearData(fResut);
|
||||
mSession.getDataHandler().onEventDecrypted(fEvent);
|
||||
}
|
||||
});
|
||||
Log.d(LOG_TAG, "## onNewSession() : successful re-decryption of " + event.eventId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasKeysForKeyRequest(IncomingRoomKeyRequest request) {
|
||||
return (null != request)
|
||||
&& (null != request.mRequestBody)
|
||||
&& mOlmDevice.hasInboundSessionKeys(request.mRequestBody.room_id, request.mRequestBody.sender_key, request.mRequestBody.session_id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shareKeysWithDevice(final IncomingRoomKeyRequest request) {
|
||||
// sanity checks
|
||||
if ((null == request) || (null == request.mRequestBody)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String userId = request.mUserId;
|
||||
|
||||
mSession.getCrypto().getDeviceList().downloadKeys(Arrays.asList(userId), false, new ApiCallback<MXUsersDevicesMap<MXDeviceInfo>>() {
|
||||
@Override
|
||||
public void onSuccess(MXUsersDevicesMap<MXDeviceInfo> devicesMap) {
|
||||
final String deviceId = request.mDeviceId;
|
||||
final MXDeviceInfo deviceInfo = mSession.getCrypto().mCryptoStore.getUserDevice(deviceId, userId);
|
||||
|
||||
if (null != deviceInfo) {
|
||||
final RoomKeyRequestBody body = request.mRequestBody;
|
||||
|
||||
Map<String, List<MXDeviceInfo>> devicesByUser = new HashMap<>();
|
||||
devicesByUser.put(userId, new ArrayList<>(Arrays.asList(deviceInfo)));
|
||||
|
||||
mSession.getCrypto().ensureOlmSessionsForDevices(devicesByUser, new ApiCallback<MXUsersDevicesMap<MXOlmSessionResult>>() {
|
||||
@Override
|
||||
public void onSuccess(MXUsersDevicesMap<MXOlmSessionResult> map) {
|
||||
MXOlmSessionResult olmSessionResult = map.getObject(deviceId, userId);
|
||||
|
||||
if ((null == olmSessionResult) || (null == olmSessionResult.mSessionId)) {
|
||||
// no session with this device, probably because there
|
||||
// were no one-time keys.
|
||||
//
|
||||
// ensureOlmSessionsForUsers has already done the logging,
|
||||
// so just skip it.
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(LOG_TAG, "## shareKeysWithDevice() : sharing keys for session " + body.sender_key + "|" + body.session_id
|
||||
+ " with device " + userId + ":" + deviceId);
|
||||
|
||||
MXOlmInboundGroupSession2 inboundGroupSession = mSession.getCrypto()
|
||||
.getOlmDevice().getInboundGroupSession(body.session_id, body.sender_key, body.room_id);
|
||||
|
||||
Map<String, Object> payloadJson = new HashMap<>();
|
||||
payloadJson.put("type", Event.EVENT_TYPE_FORWARDED_ROOM_KEY);
|
||||
payloadJson.put("content", inboundGroupSession.exportKeys());
|
||||
|
||||
Map<String, Object> encodedPayload = mSession.getCrypto().encryptMessage(payloadJson, Arrays.asList(deviceInfo));
|
||||
MXUsersDevicesMap<Map<String, Object>> sendToDeviceMap = new MXUsersDevicesMap<>();
|
||||
sendToDeviceMap.setObject(encodedPayload, userId, deviceId);
|
||||
|
||||
Log.d(LOG_TAG, "## shareKeysWithDevice() : sending to " + userId + ":" + deviceId);
|
||||
mSession.getCryptoRestClient().sendToDevice(Event.EVENT_TYPE_MESSAGE_ENCRYPTED, sendToDeviceMap, new ApiCallback<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void info) {
|
||||
Log.d(LOG_TAG, "## shareKeysWithDevice() : sent to " + userId + ":" + deviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
Log.e(LOG_TAG, "## shareKeysWithDevice() : sendToDevice " + userId + ":" + deviceId + " failed " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
Log.e(LOG_TAG, "## shareKeysWithDevice() : sendToDevice " + userId + ":" + deviceId + " failed " + e.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
Log.e(LOG_TAG, "## shareKeysWithDevice() : sendToDevice " + userId + ":" + deviceId + " failed " + e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
Log.e(LOG_TAG, "## shareKeysWithDevice() : ensureOlmSessionsForDevices " + userId + ":" + deviceId + " failed "
|
||||
+ e.getMessage(), e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
Log.e(LOG_TAG, "## shareKeysWithDevice() : ensureOlmSessionsForDevices " + userId + ":" + deviceId + " failed " + e.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
Log.e(LOG_TAG, "## shareKeysWithDevice() : ensureOlmSessionsForDevices " + userId + ":" + deviceId + " failed "
|
||||
+ e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Log.e(LOG_TAG, "## shareKeysWithDevice() : ensureOlmSessionsForDevices " + userId + ":" + deviceId + " not found");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
Log.e(LOG_TAG, "## shareKeysWithDevice() : downloadKeys " + userId + " failed " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
Log.e(LOG_TAG, "## shareKeysWithDevice() : downloadKeys " + userId + " failed " + e.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
Log.e(LOG_TAG, "## shareKeysWithDevice() : downloadKeys " + userId + " failed " + e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,714 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto.algorithms.megolm;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.MXSession;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.MXCrypto;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.MXCryptoAlgorithms;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.MXCryptoError;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.MXOlmDevice;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.algorithms.IMXEncrypting;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXDeviceInfo;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXOlmSessionResult;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXQueuedEncryption;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXUsersDevicesMap;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.SimpleApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
|
||||
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class MXMegolmEncryption implements IMXEncrypting {
|
||||
private static final String LOG_TAG = MXMegolmEncryption.class.getSimpleName();
|
||||
|
||||
private MXSession mSession;
|
||||
private MXCrypto mCrypto;
|
||||
|
||||
// The id of the room we will be sending to.
|
||||
private String mRoomId;
|
||||
|
||||
private String mDeviceId;
|
||||
|
||||
// OutboundSessionInfo. Null if we haven't yet started setting one up. Note
|
||||
// that even if this is non-null, it may not be ready for use (in which
|
||||
// case outboundSession.shareOperation will be non-null.)
|
||||
private MXOutboundSessionInfo mOutboundSession;
|
||||
|
||||
// true when there is an HTTP operation in progress
|
||||
private boolean mShareOperationIsProgress;
|
||||
|
||||
private final List<MXQueuedEncryption> mPendingEncryptions = new ArrayList<>();
|
||||
|
||||
// Session rotation periods
|
||||
private int mSessionRotationPeriodMsgs;
|
||||
private int mSessionRotationPeriodMs;
|
||||
|
||||
@Override
|
||||
public void initWithMatrixSession(MXSession matrixSession, String roomId) {
|
||||
mSession = matrixSession;
|
||||
mCrypto = matrixSession.getCrypto();
|
||||
|
||||
mRoomId = roomId;
|
||||
mDeviceId = matrixSession.getCredentials().deviceId;
|
||||
|
||||
// Default rotation periods
|
||||
// TODO: Make it configurable via parameters
|
||||
mSessionRotationPeriodMsgs = 100;
|
||||
mSessionRotationPeriodMs = 7 * 24 * 3600 * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a snapshot of the pending encryptions
|
||||
*/
|
||||
private List<MXQueuedEncryption> getPendingEncryptions() {
|
||||
List<MXQueuedEncryption> list = new ArrayList<>();
|
||||
|
||||
synchronized (mPendingEncryptions) {
|
||||
list.addAll(mPendingEncryptions);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encryptEventContent(final JsonElement eventContent,
|
||||
final String eventType,
|
||||
final List<String> userIds,
|
||||
final ApiCallback<JsonElement> callback) {
|
||||
// Queue the encryption request
|
||||
// It will be processed when everything is set up
|
||||
MXQueuedEncryption queuedEncryption = new MXQueuedEncryption();
|
||||
|
||||
queuedEncryption.mEventContent = eventContent;
|
||||
queuedEncryption.mEventType = eventType;
|
||||
queuedEncryption.mApiCallback = callback;
|
||||
|
||||
synchronized (mPendingEncryptions) {
|
||||
mPendingEncryptions.add(queuedEncryption);
|
||||
}
|
||||
|
||||
final long t0 = System.currentTimeMillis();
|
||||
Log.d(LOG_TAG, "## encryptEventContent () starts");
|
||||
|
||||
getDevicesInRoom(userIds, new ApiCallback<MXUsersDevicesMap<MXDeviceInfo>>() {
|
||||
|
||||
/**
|
||||
* A network error has been received while encrypting
|
||||
* @param e the exception
|
||||
*/
|
||||
private void dispatchNetworkError(Exception e) {
|
||||
Log.e(LOG_TAG, "## encryptEventContent() : onNetworkError " + e.getMessage(), e);
|
||||
List<MXQueuedEncryption> queuedEncryptions = getPendingEncryptions();
|
||||
|
||||
for (MXQueuedEncryption queuedEncryption : queuedEncryptions) {
|
||||
queuedEncryption.mApiCallback.onNetworkError(e);
|
||||
}
|
||||
|
||||
synchronized (mPendingEncryptions) {
|
||||
mPendingEncryptions.removeAll(queuedEncryptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A matrix error has been received while encrypting
|
||||
* @param e the exception
|
||||
*/
|
||||
private void dispatchMatrixError(MatrixError e) {
|
||||
Log.e(LOG_TAG, "## encryptEventContent() : onMatrixError " + e.getMessage());
|
||||
|
||||
List<MXQueuedEncryption> queuedEncryptions = getPendingEncryptions();
|
||||
|
||||
for (MXQueuedEncryption queuedEncryption : queuedEncryptions) {
|
||||
queuedEncryption.mApiCallback.onMatrixError(e);
|
||||
}
|
||||
|
||||
synchronized (mPendingEncryptions) {
|
||||
mPendingEncryptions.removeAll(queuedEncryptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An unexpected error has been received while encrypting
|
||||
* @param e the exception
|
||||
*/
|
||||
private void dispatchUnexpectedError(Exception e) {
|
||||
Log.e(LOG_TAG, "## onUnexpectedError() : onMatrixError " + e.getMessage(), e);
|
||||
|
||||
List<MXQueuedEncryption> queuedEncryptions = getPendingEncryptions();
|
||||
|
||||
for (MXQueuedEncryption queuedEncryption : queuedEncryptions) {
|
||||
queuedEncryption.mApiCallback.onUnexpectedError(e);
|
||||
}
|
||||
|
||||
synchronized (mPendingEncryptions) {
|
||||
mPendingEncryptions.removeAll(queuedEncryptions);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(MXUsersDevicesMap<MXDeviceInfo> devicesInRoom) {
|
||||
ensureOutboundSession(devicesInRoom, new ApiCallback<MXOutboundSessionInfo>() {
|
||||
@Override
|
||||
public void onSuccess(final MXOutboundSessionInfo session) {
|
||||
mCrypto.getEncryptingThreadHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Log.d(LOG_TAG, "## encryptEventContent () processPendingEncryptions after " + (System.currentTimeMillis() - t0) + "ms");
|
||||
processPendingEncryptions(session);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
dispatchNetworkError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
dispatchMatrixError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
dispatchUnexpectedError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
dispatchNetworkError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
dispatchMatrixError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
dispatchUnexpectedError(e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a new session.
|
||||
*
|
||||
* @return the session description
|
||||
*/
|
||||
private MXOutboundSessionInfo prepareNewSessionInRoom() {
|
||||
MXOlmDevice olmDevice = mCrypto.getOlmDevice();
|
||||
final String sessionId = olmDevice.createOutboundGroupSession();
|
||||
|
||||
Map<String, String> keysClaimedMap = new HashMap<>();
|
||||
keysClaimedMap.put("ed25519", olmDevice.getDeviceEd25519Key());
|
||||
|
||||
olmDevice.addInboundGroupSession(sessionId, olmDevice.getSessionKey(sessionId), mRoomId, olmDevice.getDeviceCurve25519Key(),
|
||||
new ArrayList<String>(), keysClaimedMap, false);
|
||||
|
||||
return new MXOutboundSessionInfo(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the outbound session
|
||||
*
|
||||
* @param devicesInRoom the devices list
|
||||
* @param callback the asynchronous callback.
|
||||
*/
|
||||
private void ensureOutboundSession(MXUsersDevicesMap<MXDeviceInfo> devicesInRoom, final ApiCallback<MXOutboundSessionInfo> callback) {
|
||||
MXOutboundSessionInfo session = mOutboundSession;
|
||||
|
||||
if ((null == session)
|
||||
// Need to make a brand new session?
|
||||
|| session.needsRotation(mSessionRotationPeriodMsgs, mSessionRotationPeriodMs)
|
||||
// Determine if we have shared with anyone we shouldn't have
|
||||
|| session.sharedWithTooManyDevices(devicesInRoom)) {
|
||||
mOutboundSession = session = prepareNewSessionInRoom();
|
||||
}
|
||||
|
||||
if (mShareOperationIsProgress) {
|
||||
Log.d(LOG_TAG, "## ensureOutboundSessionInRoom() : already in progress");
|
||||
// Key share already in progress
|
||||
return;
|
||||
}
|
||||
|
||||
final MXOutboundSessionInfo fSession = session;
|
||||
|
||||
Map<String, /* userId */List<MXDeviceInfo>> shareMap = new HashMap<>();
|
||||
|
||||
List<String> userIds = devicesInRoom.getUserIds();
|
||||
|
||||
for (String userId : userIds) {
|
||||
List<String> deviceIds = devicesInRoom.getUserDeviceIds(userId);
|
||||
|
||||
for (String deviceId : deviceIds) {
|
||||
MXDeviceInfo deviceInfo = devicesInRoom.getObject(deviceId, userId);
|
||||
|
||||
if (null == fSession.mSharedWithDevices.getObject(deviceId, userId)) {
|
||||
if (!shareMap.containsKey(userId)) {
|
||||
shareMap.put(userId, new ArrayList<MXDeviceInfo>());
|
||||
}
|
||||
|
||||
shareMap.get(userId).add(deviceInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shareKey(fSession, shareMap, new ApiCallback<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void anything) {
|
||||
mShareOperationIsProgress = false;
|
||||
if (null != callback) {
|
||||
callback.onSuccess(fSession);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(final Exception e) {
|
||||
Log.e(LOG_TAG, "## ensureOutboundSessionInRoom() : shareKey onNetworkError " + e.getMessage(), e);
|
||||
|
||||
if (null != callback) {
|
||||
callback.onNetworkError(e);
|
||||
}
|
||||
mShareOperationIsProgress = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(final MatrixError e) {
|
||||
Log.e(LOG_TAG, "## ensureOutboundSessionInRoom() : shareKey onMatrixError " + e.getMessage());
|
||||
|
||||
if (null != callback) {
|
||||
callback.onMatrixError(e);
|
||||
}
|
||||
mShareOperationIsProgress = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(final Exception e) {
|
||||
Log.e(LOG_TAG, "## ensureOutboundSessionInRoom() : shareKey onUnexpectedError " + e.getMessage(), e);
|
||||
|
||||
if (null != callback) {
|
||||
callback.onUnexpectedError(e);
|
||||
}
|
||||
mShareOperationIsProgress = false;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Share the device key to a list of users
|
||||
*
|
||||
* @param session the session info
|
||||
* @param devicesByUsers the devices map
|
||||
* @param callback the asynchronous callback
|
||||
*/
|
||||
private void shareKey(final MXOutboundSessionInfo session,
|
||||
final Map<String, List<MXDeviceInfo>> devicesByUsers,
|
||||
final ApiCallback<Void> callback) {
|
||||
// nothing to send, the task is done
|
||||
if (0 == devicesByUsers.size()) {
|
||||
Log.d(LOG_TAG, "## shareKey() : nothing more to do");
|
||||
|
||||
if (null != callback) {
|
||||
mCrypto.getUIHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onSuccess(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// reduce the map size to avoid request timeout when there are too devices (Users size * devices per user)
|
||||
Map<String, List<MXDeviceInfo>> subMap = new HashMap<>();
|
||||
|
||||
final List<String> userIds = new ArrayList<>();
|
||||
int devicesCount = 0;
|
||||
|
||||
for (String userId : devicesByUsers.keySet()) {
|
||||
List<MXDeviceInfo> devicesList = devicesByUsers.get(userId);
|
||||
|
||||
userIds.add(userId);
|
||||
subMap.put(userId, devicesList);
|
||||
|
||||
devicesCount += devicesList.size();
|
||||
|
||||
if (devicesCount > 100) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(LOG_TAG, "## shareKey() ; userId " + userIds);
|
||||
shareUserDevicesKey(session, subMap, new ApiCallback<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void info) {
|
||||
mCrypto.getEncryptingThreadHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (String userId : userIds) {
|
||||
devicesByUsers.remove(userId);
|
||||
}
|
||||
shareKey(session, devicesByUsers, callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
Log.e(LOG_TAG, "## shareKey() ; userIds " + userIds + " failed " + e.getMessage(), e);
|
||||
if (null != callback) {
|
||||
callback.onNetworkError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
Log.e(LOG_TAG, "## shareKey() ; userIds " + userIds + " failed " + e.getMessage());
|
||||
if (null != callback) {
|
||||
callback.onMatrixError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
Log.e(LOG_TAG, "## shareKey() ; userIds " + userIds + " failed " + e.getMessage(), e);
|
||||
if (null != callback) {
|
||||
callback.onUnexpectedError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Share the device keys of a an user
|
||||
*
|
||||
* @param session the session info
|
||||
* @param devicesByUser the devices map
|
||||
* @param callback the asynchronous callback
|
||||
*/
|
||||
private void shareUserDevicesKey(final MXOutboundSessionInfo session,
|
||||
final Map<String, List<MXDeviceInfo>> devicesByUser,
|
||||
final ApiCallback<Void> callback) {
|
||||
final String sessionKey = mCrypto.getOlmDevice().getSessionKey(session.mSessionId);
|
||||
final int chainIndex = mCrypto.getOlmDevice().getMessageIndex(session.mSessionId);
|
||||
|
||||
Map<String, Object> submap = new HashMap<>();
|
||||
submap.put("algorithm", MXCryptoAlgorithms.MXCRYPTO_ALGORITHM_MEGOLM);
|
||||
submap.put("room_id", mRoomId);
|
||||
submap.put("session_id", session.mSessionId);
|
||||
submap.put("session_key", sessionKey);
|
||||
submap.put("chain_index", chainIndex);
|
||||
|
||||
final Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("type", Event.EVENT_TYPE_ROOM_KEY);
|
||||
payload.put("content", submap);
|
||||
|
||||
final long t0 = System.currentTimeMillis();
|
||||
Log.d(LOG_TAG, "## shareUserDevicesKey() : starts");
|
||||
|
||||
mCrypto.ensureOlmSessionsForDevices(devicesByUser, new ApiCallback<MXUsersDevicesMap<MXOlmSessionResult>>() {
|
||||
@Override
|
||||
public void onSuccess(final MXUsersDevicesMap<MXOlmSessionResult> results) {
|
||||
mCrypto.getEncryptingThreadHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Log.d(LOG_TAG, "## shareUserDevicesKey() : ensureOlmSessionsForDevices succeeds after " + (System.currentTimeMillis() - t0) + " ms");
|
||||
MXUsersDevicesMap<Map<String, Object>> contentMap = new MXUsersDevicesMap<>();
|
||||
|
||||
boolean haveTargets = false;
|
||||
List<String> userIds = results.getUserIds();
|
||||
|
||||
for (String userId : userIds) {
|
||||
List<MXDeviceInfo> devicesToShareWith = devicesByUser.get(userId);
|
||||
|
||||
for (MXDeviceInfo deviceInfo : devicesToShareWith) {
|
||||
String deviceID = deviceInfo.deviceId;
|
||||
|
||||
MXOlmSessionResult sessionResult = results.getObject(deviceID, userId);
|
||||
|
||||
if ((null == sessionResult) || (null == sessionResult.mSessionId)) {
|
||||
// no session with this device, probably because there
|
||||
// were no one-time keys.
|
||||
//
|
||||
// we could send them a to_device message anyway, as a
|
||||
// signal that they have missed out on the key sharing
|
||||
// message because of the lack of keys, but there's not
|
||||
// much point in that really; it will mostly serve to clog
|
||||
// up to_device inboxes.
|
||||
//
|
||||
// ensureOlmSessionsForUsers has already done the logging,
|
||||
// so just skip it.
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.d(LOG_TAG, "## shareUserDevicesKey() : Sharing keys with device " + userId + ":" + deviceID);
|
||||
//noinspection ArraysAsListWithZeroOrOneArgument,ArraysAsListWithZeroOrOneArgument
|
||||
contentMap.setObject(mCrypto.encryptMessage(payload, Arrays.asList(sessionResult.mDevice)), userId, deviceID);
|
||||
haveTargets = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (haveTargets && !mCrypto.hasBeenReleased()) {
|
||||
final long t0 = System.currentTimeMillis();
|
||||
Log.d(LOG_TAG, "## shareUserDevicesKey() : has target");
|
||||
|
||||
mSession.getCryptoRestClient().sendToDevice(Event.EVENT_TYPE_MESSAGE_ENCRYPTED, contentMap, new ApiCallback<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void info) {
|
||||
mCrypto.getEncryptingThreadHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Log.d(LOG_TAG, "## shareUserDevicesKey() : sendToDevice succeeds after "
|
||||
+ (System.currentTimeMillis() - t0) + " ms");
|
||||
|
||||
// Add the devices we have shared with to session.sharedWithDevices.
|
||||
// we deliberately iterate over devicesByUser (ie, the devices we
|
||||
// attempted to share with) rather than the contentMap (those we did
|
||||
// share with), because we don't want to try to claim a one-time-key
|
||||
// for dead devices on every message.
|
||||
for (String userId : devicesByUser.keySet()) {
|
||||
List<MXDeviceInfo> devicesToShareWith = devicesByUser.get(userId);
|
||||
|
||||
for (MXDeviceInfo deviceInfo : devicesToShareWith) {
|
||||
session.mSharedWithDevices.setObject(chainIndex, userId, deviceInfo.deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
mCrypto.getUIHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (null != callback) {
|
||||
callback.onSuccess(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
Log.e(LOG_TAG, "## shareUserDevicesKey() : sendToDevice onNetworkError " + e.getMessage(), e);
|
||||
|
||||
if (null != callback) {
|
||||
callback.onNetworkError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
Log.e(LOG_TAG, "## shareUserDevicesKey() : sendToDevice onMatrixError " + e.getMessage());
|
||||
|
||||
if (null != callback) {
|
||||
callback.onMatrixError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
Log.e(LOG_TAG, "## shareUserDevicesKey() : sendToDevice onUnexpectedError " + e.getMessage(), e);
|
||||
|
||||
if (null != callback) {
|
||||
callback.onUnexpectedError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Log.d(LOG_TAG, "## shareUserDevicesKey() : no need to sharekey");
|
||||
|
||||
if (null != callback) {
|
||||
mCrypto.getUIHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onSuccess(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
Log.e(LOG_TAG, "## shareUserDevicesKey() : ensureOlmSessionsForDevices failed " + e.getMessage(), e);
|
||||
|
||||
if (null != callback) {
|
||||
callback.onNetworkError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
Log.e(LOG_TAG, "## shareUserDevicesKey() : ensureOlmSessionsForDevices failed " + e.getMessage());
|
||||
|
||||
if (null != callback) {
|
||||
callback.onMatrixError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
Log.e(LOG_TAG, "## shareUserDevicesKey() : ensureOlmSessionsForDevices failed " + e.getMessage(), e);
|
||||
|
||||
if (null != callback) {
|
||||
callback.onUnexpectedError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* process the pending encryptions
|
||||
*/
|
||||
private void processPendingEncryptions(MXOutboundSessionInfo session) {
|
||||
if (null != session) {
|
||||
List<MXQueuedEncryption> queuedEncryptions = getPendingEncryptions();
|
||||
|
||||
// Everything is in place, encrypt all pending events
|
||||
for (MXQueuedEncryption queuedEncryption : queuedEncryptions) {
|
||||
Map<String, Object> payloadJson = new HashMap<>();
|
||||
|
||||
payloadJson.put("room_id", mRoomId);
|
||||
payloadJson.put("type", queuedEncryption.mEventType);
|
||||
payloadJson.put("content", queuedEncryption.mEventContent);
|
||||
|
||||
String payloadString = JsonUtils.convertToUTF8(JsonUtils.canonicalize(JsonUtils.getGson(false).toJsonTree(payloadJson)).toString());
|
||||
String ciphertext = mCrypto.getOlmDevice().encryptGroupMessage(session.mSessionId, payloadString);
|
||||
|
||||
final Map<String, Object> map = new HashMap<>();
|
||||
map.put("algorithm", MXCryptoAlgorithms.MXCRYPTO_ALGORITHM_MEGOLM);
|
||||
map.put("sender_key", mCrypto.getOlmDevice().getDeviceCurve25519Key());
|
||||
map.put("ciphertext", ciphertext);
|
||||
map.put("session_id", session.mSessionId);
|
||||
|
||||
// Include our device ID so that recipients can send us a
|
||||
// m.new_device message if they don't have our session key.
|
||||
map.put("device_id", mDeviceId);
|
||||
|
||||
final MXQueuedEncryption fQueuedEncryption = queuedEncryption;
|
||||
mCrypto.getUIHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
fQueuedEncryption.mApiCallback.onSuccess(JsonUtils.getGson(false).toJsonTree(map));
|
||||
}
|
||||
});
|
||||
|
||||
session.mUseCount++;
|
||||
}
|
||||
|
||||
synchronized (mPendingEncryptions) {
|
||||
mPendingEncryptions.removeAll(queuedEncryptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of devices which can encrypt data to.
|
||||
* This method must be called in getDecryptingThreadHandler() thread.
|
||||
*
|
||||
* @param userIds the user ids whose devices must be checked.
|
||||
* @param callback the asynchronous callback
|
||||
*/
|
||||
private void getDevicesInRoom(final List<String> userIds, final ApiCallback<MXUsersDevicesMap<MXDeviceInfo>> callback) {
|
||||
// We are happy to use a cached version here: we assume that if we already
|
||||
// have a list of the user's devices, then we already share an e2e room
|
||||
// with them, which means that they will have announced any new devices via
|
||||
// an m.new_device.
|
||||
mCrypto.getDeviceList().downloadKeys(userIds, false, new SimpleApiCallback<MXUsersDevicesMap<MXDeviceInfo>>(callback) {
|
||||
@Override
|
||||
public void onSuccess(final MXUsersDevicesMap<MXDeviceInfo> devices) {
|
||||
mCrypto.getEncryptingThreadHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
boolean encryptToVerifiedDevicesOnly = mCrypto.getGlobalBlacklistUnverifiedDevices()
|
||||
|| mCrypto.isRoomBlacklistUnverifiedDevices(mRoomId);
|
||||
|
||||
final MXUsersDevicesMap<MXDeviceInfo> devicesInRoom = new MXUsersDevicesMap<>();
|
||||
final MXUsersDevicesMap<MXDeviceInfo> unknownDevices = new MXUsersDevicesMap<>();
|
||||
|
||||
List<String> userIds = devices.getUserIds();
|
||||
|
||||
for (String userId : userIds) {
|
||||
List<String> deviceIds = devices.getUserDeviceIds(userId);
|
||||
|
||||
for (String deviceId : deviceIds) {
|
||||
MXDeviceInfo deviceInfo = devices.getObject(deviceId, userId);
|
||||
|
||||
if (mCrypto.warnOnUnknownDevices() && deviceInfo.isUnknown()) {
|
||||
// The device is not yet known by the user
|
||||
unknownDevices.setObject(deviceInfo, userId, deviceId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (deviceInfo.isBlocked()) {
|
||||
// Remove any blocked devices
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!deviceInfo.isVerified() && encryptToVerifiedDevicesOnly) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TextUtils.equals(deviceInfo.identityKey(), mCrypto.getOlmDevice().getDeviceCurve25519Key())) {
|
||||
// Don't bother sending to ourself
|
||||
continue;
|
||||
}
|
||||
|
||||
devicesInRoom.setObject(deviceInfo, userId, deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
mCrypto.getUIHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Check if any of these devices are not yet known to the user.
|
||||
// if so, warn the user so they can verify or ignore.
|
||||
if (0 != unknownDevices.getMap().size()) {
|
||||
callback.onMatrixError(new MXCryptoError(MXCryptoError.UNKNOWN_DEVICES_CODE,
|
||||
MXCryptoError.UNABLE_TO_ENCRYPT, MXCryptoError.UNKNOWN_DEVICES_REASON, unknownDevices));
|
||||
} else {
|
||||
callback.onSuccess(devicesInRoom);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto.algorithms.megolm;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXDeviceInfo;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXUsersDevicesMap;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MXOutboundSessionInfo {
|
||||
private static final String LOG_TAG = MXOutboundSessionInfo.class.getSimpleName();
|
||||
|
||||
// When the session was created
|
||||
private final long mCreationTime;
|
||||
|
||||
// The id of the session
|
||||
public final String mSessionId;
|
||||
|
||||
// Number of times this session has been used
|
||||
public int mUseCount;
|
||||
|
||||
// Devices with which we have shared the session key
|
||||
// userId -> {deviceId -> msgindex}
|
||||
public final MXUsersDevicesMap<Integer> mSharedWithDevices;
|
||||
|
||||
// constructor
|
||||
public MXOutboundSessionInfo(String sessionId) {
|
||||
mSessionId = sessionId;
|
||||
mSharedWithDevices = new MXUsersDevicesMap<>();
|
||||
mCreationTime = System.currentTimeMillis();
|
||||
mUseCount = 0;
|
||||
}
|
||||
|
||||
public boolean needsRotation(int rotationPeriodMsgs, int rotationPeriodMs) {
|
||||
boolean needsRotation = false;
|
||||
long sessionLifetime = System.currentTimeMillis() - mCreationTime;
|
||||
|
||||
if ((mUseCount >= rotationPeriodMsgs) || (sessionLifetime >= rotationPeriodMs)) {
|
||||
Log.d(LOG_TAG, "## needsRotation() : Rotating megolm session after " + mUseCount + ", " + sessionLifetime + "ms");
|
||||
needsRotation = true;
|
||||
}
|
||||
|
||||
return needsRotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this session has been shared with devices which it shouldn't have been.
|
||||
*
|
||||
* @param devicesInRoom the devices map
|
||||
* @return true if we have shared the session with devices which aren't in devicesInRoom.
|
||||
*/
|
||||
public boolean sharedWithTooManyDevices(MXUsersDevicesMap<MXDeviceInfo> devicesInRoom) {
|
||||
List<String> userIds = mSharedWithDevices.getUserIds();
|
||||
|
||||
for (String userId : userIds) {
|
||||
if (null == devicesInRoom.getUserDeviceIds(userId)) {
|
||||
Log.d(LOG_TAG, "## sharedWithTooManyDevices() : Starting new session because we shared with " + userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
List<String> deviceIds = mSharedWithDevices.getUserDeviceIds(userId);
|
||||
|
||||
for (String deviceId : deviceIds) {
|
||||
if (null == devicesInRoom.getObject(deviceId, userId)) {
|
||||
Log.d(LOG_TAG, "## sharedWithTooManyDevices() : Starting new session because we shared with " + userId + ":" + deviceId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,272 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto.algorithms.olm;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.MXSession;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.IncomingRoomKeyRequest;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.MXCryptoError;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.MXDecryptionException;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.MXEventDecryptionResult;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.MXOlmDevice;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.algorithms.IMXDecrypting;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.crypto.OlmEventContent;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.crypto.OlmPayloadContent;
|
||||
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* An interface for encrypting data
|
||||
*/
|
||||
public class MXOlmDecryption implements IMXDecrypting {
|
||||
private static final String LOG_TAG = "MXOlmDecryption";
|
||||
|
||||
// The olm device interface
|
||||
private MXOlmDevice mOlmDevice;
|
||||
|
||||
// the matrix session
|
||||
private MXSession mSession;
|
||||
|
||||
@Override
|
||||
public void initWithMatrixSession(MXSession matrixSession) {
|
||||
mSession = matrixSession;
|
||||
mOlmDevice = matrixSession.getCrypto().getOlmDevice();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MXEventDecryptionResult decryptEvent(Event event, String timeline) throws MXDecryptionException {
|
||||
// sanity check
|
||||
if (null == event) {
|
||||
Log.e(LOG_TAG, "## decryptEvent() : null event");
|
||||
return null;
|
||||
}
|
||||
|
||||
OlmEventContent olmEventContent = JsonUtils.toOlmEventContent(event.getWireContent().getAsJsonObject());
|
||||
String deviceKey = olmEventContent.sender_key;
|
||||
Map<String, Object> ciphertext = olmEventContent.ciphertext;
|
||||
|
||||
if (null == ciphertext) {
|
||||
Log.e(LOG_TAG, "## decryptEvent() : missing cipher text");
|
||||
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.MISSING_CIPHER_TEXT_ERROR_CODE,
|
||||
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON));
|
||||
}
|
||||
|
||||
if (!ciphertext.containsKey(mOlmDevice.getDeviceCurve25519Key())) {
|
||||
Log.e(LOG_TAG, "## decryptEvent() : our device " + mOlmDevice.getDeviceCurve25519Key()
|
||||
+ " is not included in recipients. Event " + event.getContentAsJsonObject());
|
||||
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.NOT_INCLUDE_IN_RECIPIENTS_ERROR_CODE,
|
||||
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON));
|
||||
}
|
||||
|
||||
// The message for myUser
|
||||
Map<String, Object> message = (Map<String, Object>) ciphertext.get(mOlmDevice.getDeviceCurve25519Key());
|
||||
String payloadString = decryptMessage(message, deviceKey);
|
||||
|
||||
if (null == payloadString) {
|
||||
Log.e(LOG_TAG, "## decryptEvent() Failed to decrypt Olm event (id= " + event.eventId + " ) from " + deviceKey);
|
||||
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.BAD_ENCRYPTED_MESSAGE_ERROR_CODE,
|
||||
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON));
|
||||
}
|
||||
|
||||
JsonElement payload = new JsonParser().parse(JsonUtils.convertFromUTF8(payloadString));
|
||||
|
||||
if (null == payload) {
|
||||
Log.e(LOG_TAG, "## decryptEvent failed : null payload");
|
||||
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.UNABLE_TO_DECRYPT_ERROR_CODE,
|
||||
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON));
|
||||
}
|
||||
|
||||
OlmPayloadContent olmPayloadContent = JsonUtils.toOlmPayloadContent(payload);
|
||||
|
||||
if (TextUtils.isEmpty(olmPayloadContent.recipient)) {
|
||||
String reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient");
|
||||
Log.e(LOG_TAG, "## decryptEvent() : " + reason);
|
||||
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.MISSING_PROPERTY_ERROR_CODE,
|
||||
MXCryptoError.UNABLE_TO_DECRYPT, reason));
|
||||
}
|
||||
|
||||
if (!TextUtils.equals(olmPayloadContent.recipient, mSession.getMyUserId())) {
|
||||
Log.e(LOG_TAG, "## decryptEvent() : Event " + event.eventId + ": Intended recipient " + olmPayloadContent.recipient
|
||||
+ " does not match our id " + mSession.getMyUserId());
|
||||
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.BAD_RECIPIENT_ERROR_CODE,
|
||||
MXCryptoError.UNABLE_TO_DECRYPT, String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient)));
|
||||
}
|
||||
|
||||
if (null == olmPayloadContent.recipient_keys) {
|
||||
Log.e(LOG_TAG, "## decryptEvent() : Olm event (id=" + event.eventId
|
||||
+ ") contains no " + "'recipient_keys' property; cannot prevent unknown-key attack");
|
||||
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.MISSING_PROPERTY_ERROR_CODE,
|
||||
MXCryptoError.UNABLE_TO_DECRYPT, String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys")));
|
||||
}
|
||||
|
||||
String ed25519 = olmPayloadContent.recipient_keys.get("ed25519");
|
||||
|
||||
if (!TextUtils.equals(ed25519, mOlmDevice.getDeviceEd25519Key())) {
|
||||
Log.e(LOG_TAG, "## decryptEvent() : Event " + event.eventId + ": Intended recipient ed25519 key " + ed25519 + " did not match ours");
|
||||
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.BAD_RECIPIENT_KEY_ERROR_CODE,
|
||||
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.BAD_RECIPIENT_KEY_REASON));
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(olmPayloadContent.sender)) {
|
||||
Log.e(LOG_TAG, "## decryptEvent() : Olm event (id=" + event.eventId
|
||||
+ ") contains no 'sender' property; cannot prevent unknown-key attack");
|
||||
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.MISSING_PROPERTY_ERROR_CODE,
|
||||
MXCryptoError.UNABLE_TO_DECRYPT, String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender")));
|
||||
}
|
||||
|
||||
if (!TextUtils.equals(olmPayloadContent.sender, event.getSender())) {
|
||||
Log.e(LOG_TAG, "Event " + event.eventId + ": original sender " + olmPayloadContent.sender
|
||||
+ " does not match reported sender " + event.getSender());
|
||||
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.FORWARDED_MESSAGE_ERROR_CODE,
|
||||
MXCryptoError.UNABLE_TO_DECRYPT, String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender)));
|
||||
}
|
||||
|
||||
if (!TextUtils.equals(olmPayloadContent.room_id, event.roomId)) {
|
||||
Log.e(LOG_TAG, "## decryptEvent() : Event " + event.eventId + ": original room " + olmPayloadContent.room_id
|
||||
+ " does not match reported room " + event.roomId);
|
||||
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.BAD_ROOM_ERROR_CODE,
|
||||
MXCryptoError.UNABLE_TO_DECRYPT, String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.room_id)));
|
||||
}
|
||||
|
||||
if (null == olmPayloadContent.keys) {
|
||||
Log.e(LOG_TAG, "## decryptEvent failed : null keys");
|
||||
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.UNABLE_TO_DECRYPT_ERROR_CODE,
|
||||
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON));
|
||||
}
|
||||
|
||||
MXEventDecryptionResult result = new MXEventDecryptionResult();
|
||||
result.mClearEvent = payload;
|
||||
result.mSenderCurve25519Key = deviceKey;
|
||||
result.mClaimedEd25519Key = olmPayloadContent.keys.get("ed25519");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRoomKeyEvent(Event event) {
|
||||
// No impact for olm
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewSession(String senderKey, String sessionId) {
|
||||
// No impact for olm
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasKeysForKeyRequest(IncomingRoomKeyRequest request) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shareKeysWithDevice(IncomingRoomKeyRequest request) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to decrypt an Olm message.
|
||||
*
|
||||
* @param theirDeviceIdentityKey the Curve25519 identity key of the sender.
|
||||
* @param message message object, with 'type' and 'body' fields.
|
||||
* @return payload, if decrypted successfully.
|
||||
*/
|
||||
private String decryptMessage(Map<String, Object> message, String theirDeviceIdentityKey) {
|
||||
Set<String> sessionIdsSet = mOlmDevice.getSessionIds(theirDeviceIdentityKey);
|
||||
|
||||
List<String> sessionIds;
|
||||
|
||||
if (null == sessionIdsSet) {
|
||||
sessionIds = new ArrayList<>();
|
||||
} else {
|
||||
sessionIds = new ArrayList<>(sessionIdsSet);
|
||||
}
|
||||
|
||||
String messageBody = (String) message.get("body");
|
||||
Integer messageType = null;
|
||||
|
||||
Object typeAsVoid = message.get("type");
|
||||
|
||||
if (null != typeAsVoid) {
|
||||
if (typeAsVoid instanceof Double) {
|
||||
messageType = new Integer(((Double) typeAsVoid).intValue());
|
||||
} else if (typeAsVoid instanceof Integer) {
|
||||
messageType = (Integer) typeAsVoid;
|
||||
} else if (typeAsVoid instanceof Long) {
|
||||
messageType = new Integer(((Long) typeAsVoid).intValue());
|
||||
}
|
||||
}
|
||||
|
||||
if ((null == messageBody) || (null == messageType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try each session in turn
|
||||
// decryptionErrors = {};
|
||||
for (String sessionId : sessionIds) {
|
||||
String payload = mOlmDevice.decryptMessage(messageBody, messageType, sessionId, theirDeviceIdentityKey);
|
||||
|
||||
if (null != payload) {
|
||||
Log.d(LOG_TAG, "## decryptMessage() : Decrypted Olm message from " + theirDeviceIdentityKey + " with session " + sessionId);
|
||||
return payload;
|
||||
} else {
|
||||
boolean foundSession = mOlmDevice.matchesSession(theirDeviceIdentityKey, sessionId, messageType, messageBody);
|
||||
|
||||
if (foundSession) {
|
||||
// Decryption failed, but it was a prekey message matching this
|
||||
// session, so it should have worked.
|
||||
Log.e(LOG_TAG, "## decryptMessage() : Error decrypting prekey message with existing session id " + sessionId + ":TODO");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (messageType != 0) {
|
||||
// not a prekey message, so it should have matched an existing session, but it
|
||||
// didn't work.
|
||||
|
||||
if (sessionIds.size() == 0) {
|
||||
Log.e(LOG_TAG, "## decryptMessage() : No existing sessions");
|
||||
} else {
|
||||
Log.e(LOG_TAG, "## decryptMessage() : Error decrypting non-prekey message with existing sessions");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// prekey message which doesn't match any existing sessions: make a new
|
||||
// session.
|
||||
Map<String, String> res = mOlmDevice.createInboundSession(theirDeviceIdentityKey, messageType, messageBody);
|
||||
|
||||
if (null == res) {
|
||||
Log.e(LOG_TAG, "## decryptMessage() : Error decrypting non-prekey message with existing sessions");
|
||||
return null;
|
||||
}
|
||||
|
||||
Log.d(LOG_TAG, "## decryptMessage() : Created new inbound Olm session get id " + res.get("session_id") + " with " + theirDeviceIdentityKey);
|
||||
|
||||
return res.get("payload");
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto.algorithms.olm;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.MXSession;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.MXCrypto;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.algorithms.IMXEncrypting;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXDeviceInfo;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXOlmSessionResult;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXUsersDevicesMap;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.SimpleApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class MXOlmEncryption implements IMXEncrypting {
|
||||
private MXCrypto mCrypto;
|
||||
private String mRoomId;
|
||||
|
||||
@Override
|
||||
public void initWithMatrixSession(MXSession matrixSession, String roomId) {
|
||||
mCrypto = matrixSession.getCrypto();
|
||||
mRoomId = roomId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the stored device keys for a user.
|
||||
*/
|
||||
private List<MXDeviceInfo> getUserDevices(final String userId) {
|
||||
Map<String, MXDeviceInfo> map = mCrypto.getCryptoStore().getUserDevices(userId);
|
||||
return (null != map) ? new ArrayList<>(map.values()) : new ArrayList<MXDeviceInfo>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encryptEventContent(final JsonElement eventContent,
|
||||
final String eventType,
|
||||
final List<String> userIds,
|
||||
final ApiCallback<JsonElement> callback) {
|
||||
// pick the list of recipients based on the membership list.
|
||||
//
|
||||
// TODO: there is a race condition here! What if a new user turns up
|
||||
ensureSession(userIds, new SimpleApiCallback<Void>(callback) {
|
||||
@Override
|
||||
public void onSuccess(Void info) {
|
||||
List<MXDeviceInfo> deviceInfos = new ArrayList<>();
|
||||
|
||||
for (String userId : userIds) {
|
||||
List<MXDeviceInfo> devices = getUserDevices(userId);
|
||||
|
||||
if (null != devices) {
|
||||
for (MXDeviceInfo device : devices) {
|
||||
String key = device.identityKey();
|
||||
|
||||
if (TextUtils.equals(key, mCrypto.getOlmDevice().getDeviceCurve25519Key())) {
|
||||
// Don't bother setting up session to ourself
|
||||
continue;
|
||||
}
|
||||
|
||||
if (device.isBlocked()) {
|
||||
// Don't bother setting up sessions with blocked users
|
||||
continue;
|
||||
}
|
||||
|
||||
deviceInfos.add(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> messageMap = new HashMap<>();
|
||||
messageMap.put("room_id", mRoomId);
|
||||
messageMap.put("type", eventType);
|
||||
messageMap.put("content", eventContent);
|
||||
|
||||
mCrypto.encryptMessage(messageMap, deviceInfos);
|
||||
callback.onSuccess(JsonUtils.getGson(false).toJsonTree(messageMap));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the session
|
||||
*
|
||||
* @param users the user ids list
|
||||
* @param callback the asynchronous callback
|
||||
*/
|
||||
private void ensureSession(final List<String> users, final ApiCallback<Void> callback) {
|
||||
mCrypto.getDeviceList().downloadKeys(users, false, new SimpleApiCallback<MXUsersDevicesMap<MXDeviceInfo>>(callback) {
|
||||
@Override
|
||||
public void onSuccess(MXUsersDevicesMap<MXDeviceInfo> info) {
|
||||
mCrypto.ensureOlmSessionsForUsers(users, new SimpleApiCallback<MXUsersDevicesMap<MXOlmSessionResult>>(callback) {
|
||||
@Override
|
||||
public void onSuccess(MXUsersDevicesMap<MXOlmSessionResult> result) {
|
||||
if (null != callback) {
|
||||
callback.onSuccess(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto.data;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class MXDeviceInfo implements Serializable {
|
||||
private static final long serialVersionUID = 20129670646382964L;
|
||||
//
|
||||
//private static final String LOG_TAG = "MXDeviceInfo";
|
||||
|
||||
// This device is a new device and the user was not warned it has been added.
|
||||
public static final int DEVICE_VERIFICATION_UNKNOWN = -1;
|
||||
|
||||
// The user has not yet verified this device.
|
||||
public static final int DEVICE_VERIFICATION_UNVERIFIED = 0;
|
||||
|
||||
// The user has verified this device.
|
||||
public static final int DEVICE_VERIFICATION_VERIFIED = 1;
|
||||
|
||||
// The user has blocked this device.
|
||||
public static final int DEVICE_VERIFICATION_BLOCKED = 2;
|
||||
|
||||
/**
|
||||
* The id of this device.
|
||||
*/
|
||||
public String deviceId;
|
||||
|
||||
/**
|
||||
* the user id
|
||||
*/
|
||||
public String userId;
|
||||
|
||||
/**
|
||||
* The list of algorithms supported by this device.
|
||||
*/
|
||||
public List<String> algorithms;
|
||||
|
||||
/**
|
||||
* A map from <key type>:<id> to <base64-encoded key>>.
|
||||
*/
|
||||
public Map<String, String> keys;
|
||||
|
||||
/**
|
||||
* The signature of this MXDeviceInfo.
|
||||
* A map from <key type>:<device_id> to <base64-encoded key>>.
|
||||
*/
|
||||
public Map<String, Map<String, String>> signatures;
|
||||
|
||||
/*
|
||||
* Additional data from the home server.
|
||||
*/
|
||||
public Map<String, Object> unsigned;
|
||||
|
||||
/**
|
||||
* Verification state of this device.
|
||||
*/
|
||||
public int mVerified;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public MXDeviceInfo() {
|
||||
mVerified = DEVICE_VERIFICATION_UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param aDeviceId the device id
|
||||
*/
|
||||
public MXDeviceInfo(String aDeviceId) {
|
||||
deviceId = aDeviceId;
|
||||
mVerified = DEVICE_VERIFICATION_UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the device is unknown
|
||||
*
|
||||
* @return true if the device is unknown
|
||||
*/
|
||||
public boolean isUnknown() {
|
||||
return mVerified == DEVICE_VERIFICATION_UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the device is verified.
|
||||
*
|
||||
* @return true if the device is verified
|
||||
*/
|
||||
public boolean isVerified() {
|
||||
return mVerified == DEVICE_VERIFICATION_VERIFIED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the device is unverified.
|
||||
*
|
||||
* @return true if the device is unverified
|
||||
*/
|
||||
public boolean isUnverified() {
|
||||
return mVerified == DEVICE_VERIFICATION_UNVERIFIED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the device is blocked.
|
||||
*
|
||||
* @return true if the device is blocked
|
||||
*/
|
||||
public boolean isBlocked() {
|
||||
return mVerified == DEVICE_VERIFICATION_BLOCKED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the fingerprint
|
||||
*/
|
||||
public String fingerprint() {
|
||||
if ((null != keys) && !TextUtils.isEmpty(deviceId)) {
|
||||
return keys.get("ed25519:" + deviceId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the identity key
|
||||
*/
|
||||
public String identityKey() {
|
||||
if ((null != keys) && !TextUtils.isEmpty(deviceId)) {
|
||||
return keys.get("curve25519:" + deviceId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the display name
|
||||
*/
|
||||
public String displayName() {
|
||||
if (null != unsigned) {
|
||||
return (String) unsigned.get("device_display_name");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the signed data map
|
||||
*/
|
||||
public Map<String, Object> signalableJSONDictionary() {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
|
||||
map.put("device_id", deviceId);
|
||||
|
||||
if (null != userId) {
|
||||
map.put("user_id", userId);
|
||||
}
|
||||
|
||||
if (null != algorithms) {
|
||||
map.put("algorithms", algorithms);
|
||||
}
|
||||
|
||||
if (null != keys) {
|
||||
map.put("keys", keys);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a dictionary of the parameters
|
||||
*/
|
||||
public Map<String, Object> JSONDictionary() {
|
||||
Map<String, Object> JSONDictionary = new HashMap<>();
|
||||
|
||||
JSONDictionary.put("device_id", deviceId);
|
||||
|
||||
if (null != userId) {
|
||||
JSONDictionary.put("user_id", userId);
|
||||
}
|
||||
|
||||
if (null != algorithms) {
|
||||
JSONDictionary.put("algorithms", algorithms);
|
||||
}
|
||||
|
||||
if (null != keys) {
|
||||
JSONDictionary.put("keys", keys);
|
||||
}
|
||||
|
||||
if (null != signatures) {
|
||||
JSONDictionary.put("signatures", signatures);
|
||||
}
|
||||
|
||||
if (null != unsigned) {
|
||||
JSONDictionary.put("unsigned", unsigned);
|
||||
}
|
||||
|
||||
return JSONDictionary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.lang.String toString() {
|
||||
return "MXDeviceInfo " + userId + ":" + deviceId;
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto.data;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class MXEncryptEventContentResult implements Serializable {
|
||||
//public static final String LOG_TAG = "MXEncryptEventContentResult";
|
||||
|
||||
/**
|
||||
* The event content
|
||||
*/
|
||||
public final JsonElement mEventContent;
|
||||
|
||||
/**
|
||||
* the event type
|
||||
*/
|
||||
public final String mEventType;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param eventContent the eventContent
|
||||
* @param eventType the eventType
|
||||
*/
|
||||
public MXEncryptEventContentResult(JsonElement eventContent, String eventType) {
|
||||
mEventContent = eventContent;
|
||||
mEventType = eventType;
|
||||
}
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto.data;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class MXKey implements Serializable {
|
||||
private static final String LOG_TAG = "MXKey";
|
||||
/**
|
||||
* Key types.
|
||||
*/
|
||||
public static final String KEY_CURVE_25519_TYPE = "curve25519";
|
||||
public static final String KEY_SIGNED_CURVE_25519_TYPE = "signed_curve25519";
|
||||
//public static final String KEY_ED_25519_TYPE = "ed25519";
|
||||
|
||||
/**
|
||||
* The type of the key.
|
||||
*/
|
||||
public String type;
|
||||
|
||||
/**
|
||||
* The id of the key.
|
||||
*/
|
||||
public String keyId;
|
||||
|
||||
/**
|
||||
* The key.
|
||||
*/
|
||||
public String value;
|
||||
|
||||
/**
|
||||
* signature user Id to [deviceid][signature]
|
||||
*/
|
||||
public Map<String, Map<String, String>> signatures;
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
*/
|
||||
public MXKey() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a map to a MXKey
|
||||
*
|
||||
* @param map the map to convert
|
||||
*/
|
||||
public MXKey(Map<String, Map<String, Object>> map) {
|
||||
if ((null != map) && (map.size() > 0)) {
|
||||
List<String> mapKeys = new ArrayList<>(map.keySet());
|
||||
|
||||
String firstEntry = mapKeys.get(0);
|
||||
setKeyFullId(firstEntry);
|
||||
|
||||
Map<String, Object> params = map.get(firstEntry);
|
||||
value = (String) params.get("key");
|
||||
signatures = (Map<String, Map<String, String>>) params.get("signatures");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the key full id
|
||||
*/
|
||||
public String getKeyFullId() {
|
||||
return type + ":" + keyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the key fields with a key full id
|
||||
*
|
||||
* @param keyFullId the key full id
|
||||
*/
|
||||
private void setKeyFullId(String keyFullId) {
|
||||
if (!TextUtils.isEmpty(keyFullId)) {
|
||||
try {
|
||||
String[] components = keyFullId.split(":");
|
||||
|
||||
if (components.length == 2) {
|
||||
type = components[0];
|
||||
keyId = components[1];
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## setKeyFullId() failed : " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the signed data map
|
||||
*/
|
||||
public Map<String, Object> signalableJSONDictionary() {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
|
||||
if (null != value) {
|
||||
map.put("key", value);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a signature for an user Id and a signkey
|
||||
*
|
||||
* @param userId the user id
|
||||
* @param signkey the sign key
|
||||
* @return the signature
|
||||
*/
|
||||
public String signatureForUserId(String userId, String signkey) {
|
||||
// sanity checks
|
||||
if (!TextUtils.isEmpty(userId) && !TextUtils.isEmpty(signkey)) {
|
||||
if ((null != signatures) && signatures.containsKey(userId)) {
|
||||
return signatures.get(userId).get(signkey);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto.data;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import org.matrix.olm.OlmInboundGroupSession;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* This class adds more context to a OLMInboundGroupSession object.
|
||||
* This allows additional checks. The class implements NSCoding so that the context can be stored.
|
||||
*/
|
||||
public class MXOlmInboundGroupSession implements Serializable {
|
||||
//
|
||||
private static final String LOG_TAG = "OlmInboundGroupSession";
|
||||
|
||||
// The associated olm inbound group session.
|
||||
public OlmInboundGroupSession mSession;
|
||||
|
||||
// The room in which this session is used.
|
||||
public String mRoomId;
|
||||
|
||||
// The base64-encoded curve25519 key of the sender.
|
||||
public String mSenderKey;
|
||||
|
||||
// Other keys the sender claims.
|
||||
public Map<String, String> mKeysClaimed;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param sessionKey the session key
|
||||
*/
|
||||
public MXOlmInboundGroupSession(String sessionKey) {
|
||||
try {
|
||||
mSession = new OlmInboundGroupSession(sessionKey);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "Cannot create : " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto.data;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.crypto.MXCryptoAlgorithms;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
import org.matrix.olm.OlmInboundGroupSession;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* This class adds more context to a OLMInboundGroupSession object.
|
||||
* This allows additional checks. The class implements NSCoding so that the context can be stored.
|
||||
*/
|
||||
public class MXOlmInboundGroupSession2 implements Serializable {
|
||||
//
|
||||
private static final String LOG_TAG = "OlmInboundGroupSession";
|
||||
|
||||
// define a serialVersionUID to avoid having to redefine the class after updates
|
||||
private static final long serialVersionUID = 201702011617L;
|
||||
|
||||
// The associated olm inbound group session.
|
||||
public OlmInboundGroupSession mSession;
|
||||
|
||||
// The room in which this session is used.
|
||||
public String mRoomId;
|
||||
|
||||
// The base64-encoded curve25519 key of the sender.
|
||||
public String mSenderKey;
|
||||
|
||||
// Other keys the sender claims.
|
||||
public Map<String, String> mKeysClaimed;
|
||||
|
||||
// Devices which forwarded this session to us (normally empty).
|
||||
public List<String> mForwardingCurve25519KeyChain = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param prevFormatSession the previous session format
|
||||
*/
|
||||
public MXOlmInboundGroupSession2(MXOlmInboundGroupSession prevFormatSession) {
|
||||
mSession = prevFormatSession.mSession;
|
||||
mRoomId = prevFormatSession.mRoomId;
|
||||
mSenderKey = prevFormatSession.mSenderKey;
|
||||
mKeysClaimed = prevFormatSession.mKeysClaimed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param sessionKey the session key
|
||||
* @param isImported true if it is an imported session key
|
||||
*/
|
||||
public MXOlmInboundGroupSession2(String sessionKey, boolean isImported) {
|
||||
try {
|
||||
if (!isImported) {
|
||||
mSession = new OlmInboundGroupSession(sessionKey);
|
||||
} else {
|
||||
mSession = OlmInboundGroupSession.importSession(sessionKey);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "Cannot create : " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance from the provided keys map.
|
||||
*
|
||||
* @param map the map
|
||||
* @throws Exception if the data are invalid
|
||||
*/
|
||||
public MXOlmInboundGroupSession2(Map<String, Object> map) throws Exception {
|
||||
try {
|
||||
mSession = OlmInboundGroupSession.importSession((String) map.get("session_key"));
|
||||
|
||||
if (!TextUtils.equals(mSession.sessionIdentifier(), (String) map.get("session_id"))) {
|
||||
throw new Exception("Mismatched group session Id");
|
||||
}
|
||||
|
||||
mSenderKey = (String) map.get("sender_key");
|
||||
mKeysClaimed = (Map<String, String>) map.get("sender_claimed_keys");
|
||||
mRoomId = (String) map.get("room_id");
|
||||
} catch (Exception e) {
|
||||
throw new Exception(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the inbound group session keys
|
||||
*
|
||||
* @return the inbound group session as map if the operation succeeds
|
||||
*/
|
||||
public Map<String, Object> exportKeys() {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
|
||||
try {
|
||||
if (null == mForwardingCurve25519KeyChain) {
|
||||
mForwardingCurve25519KeyChain = new ArrayList<>();
|
||||
}
|
||||
|
||||
map.put("sender_claimed_ed25519_key", mKeysClaimed.get("ed25519"));
|
||||
map.put("forwardingCurve25519KeyChain", mForwardingCurve25519KeyChain);
|
||||
map.put("sender_key", mSenderKey);
|
||||
map.put("sender_claimed_keys", mKeysClaimed);
|
||||
map.put("room_id", mRoomId);
|
||||
map.put("session_id", mSession.sessionIdentifier());
|
||||
map.put("session_key", mSession.export(mSession.getFirstKnownIndex()));
|
||||
map.put("algorithm", MXCryptoAlgorithms.MXCRYPTO_ALGORITHM_MEGOLM);
|
||||
} catch (Exception e) {
|
||||
map = null;
|
||||
Log.e(LOG_TAG, "## export() : senderKey " + mSenderKey + " failed " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the first known message index
|
||||
*/
|
||||
public Long getFirstKnownIndex() {
|
||||
if (null != mSession) {
|
||||
try {
|
||||
return mSession.getFirstKnownIndex();
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## getFirstKnownIndex() : getFirstKnownIndex failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the session for a message index.
|
||||
*
|
||||
* @param messageIndex the message index
|
||||
* @return the exported data
|
||||
*/
|
||||
public String exportSession(long messageIndex) {
|
||||
if (null != mSession) {
|
||||
try {
|
||||
return mSession.export(messageIndex);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## exportSession() : export failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class MXOlmSessionResult implements Serializable {
|
||||
/**
|
||||
* the device
|
||||
*/
|
||||
public final MXDeviceInfo mDevice;
|
||||
|
||||
/**
|
||||
* Base64 olm session id.
|
||||
* null if no session could be established.
|
||||
*/
|
||||
public String mSessionId;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param device the device
|
||||
* @param sessionId the olm session id
|
||||
*/
|
||||
public MXOlmSessionResult(MXDeviceInfo device, String sessionId) {
|
||||
mDevice = device;
|
||||
mSessionId = sessionId;
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto.data;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
|
||||
|
||||
public class MXQueuedEncryption {
|
||||
|
||||
/**
|
||||
* The data to encrypt.
|
||||
*/
|
||||
public JsonElement mEventContent;
|
||||
public String mEventType;
|
||||
|
||||
/**
|
||||
* the asynchronous callback
|
||||
*/
|
||||
public ApiCallback<JsonElement> mApiCallback;
|
||||
}
|
@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.crypto.data;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class MXUsersDevicesMap<E> implements Serializable {
|
||||
|
||||
// The device keys as returned by the homeserver: a map of a map (userId -> deviceId -> Object).
|
||||
private final Map<String, Map<String, E>> mMap = new HashMap<>();
|
||||
|
||||
/**
|
||||
* @return the inner map
|
||||
*/
|
||||
public Map<String, Map<String, E>> getMap() {
|
||||
return mMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default constructor constructor
|
||||
*/
|
||||
public MXUsersDevicesMap() {
|
||||
}
|
||||
|
||||
/**
|
||||
* The constructor
|
||||
*
|
||||
* @param map the map
|
||||
*/
|
||||
public MXUsersDevicesMap(Map<String, Map<String, E>> map) {
|
||||
if (null != map) {
|
||||
Set<String> keys = map.keySet();
|
||||
|
||||
for (String key : keys) {
|
||||
mMap.put(key, new HashMap<>(map.get(key)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a deep copy
|
||||
*/
|
||||
public MXUsersDevicesMap<E> deepCopy() {
|
||||
MXUsersDevicesMap<E> copy = new MXUsersDevicesMap<>();
|
||||
|
||||
Set<String> keys = mMap.keySet();
|
||||
|
||||
for (String key : keys) {
|
||||
copy.mMap.put(key, new HashMap<>(mMap.get(key)));
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the user Ids
|
||||
*/
|
||||
public List<String> getUserIds() {
|
||||
return new ArrayList<>(mMap.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the device ids list for an user id
|
||||
*
|
||||
* @param userId the user id
|
||||
* @return the device ids list
|
||||
*/
|
||||
public List<String> getUserDeviceIds(String userId) {
|
||||
if (!TextUtils.isEmpty(userId) && mMap.containsKey(userId)) {
|
||||
return new ArrayList<>(mMap.get(userId).keySet());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the object for a device id and an user Id
|
||||
*
|
||||
* @param deviceId the device id
|
||||
* @param userId the object id
|
||||
* @return the object
|
||||
*/
|
||||
public E getObject(String deviceId, String userId) {
|
||||
if (!TextUtils.isEmpty(userId) && mMap.containsKey(userId) && !TextUtils.isEmpty(deviceId)) {
|
||||
return mMap.get(userId).get(deviceId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an object for a dedicated user Id and device Id
|
||||
*
|
||||
* @param object the object to set
|
||||
* @param userId the user Id
|
||||
* @param deviceId the device id
|
||||
*/
|
||||
public void setObject(E object, String userId, String deviceId) {
|
||||
if ((null != object) && !TextUtils.isEmpty(userId) && !TextUtils.isEmpty(deviceId)) {
|
||||
Map<String, E> subMap = mMap.get(userId);
|
||||
|
||||
if (null == subMap) {
|
||||
subMap = new HashMap<>();
|
||||
mMap.put(userId, subMap);
|
||||
}
|
||||
|
||||
subMap.put(deviceId, object);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the objects map for an user Id
|
||||
*
|
||||
* @param objectsPerDevices the objects maps
|
||||
* @param userId the user id
|
||||
*/
|
||||
public void setObjects(Map<String, E> objectsPerDevices, String userId) {
|
||||
if (!TextUtils.isEmpty(userId)) {
|
||||
if (null == objectsPerDevices) {
|
||||
mMap.remove(userId);
|
||||
} else {
|
||||
mMap.put(userId, new HashMap<>(objectsPerDevices));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes objects for a dedicated user
|
||||
*
|
||||
* @param userId the user id.
|
||||
*/
|
||||
public void removeUserObjects(String userId) {
|
||||
if (!TextUtils.isEmpty(userId)) {
|
||||
mMap.remove(userId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the internal dictionary
|
||||
*/
|
||||
public void removeAllObjects() {
|
||||
mMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add entries from another MXUsersDevicesMap
|
||||
*
|
||||
* @param other the other one
|
||||
*/
|
||||
public void addEntriesFromMap(MXUsersDevicesMap<E> other) {
|
||||
if (null != other) {
|
||||
mMap.putAll(other.getMap());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.lang.String toString() {
|
||||
if (null != mMap) {
|
||||
return "MXUsersDevicesMap " + mMap.toString();
|
||||
} else {
|
||||
return "MXDeviceInfo : null map";
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,423 @@
|
||||
/*
|
||||
* Copyright 2014 OpenMarket Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.internal.legacy.data;
|
||||
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.data.store.IMXStore;
|
||||
import im.vector.matrix.android.internal.legacy.data.timeline.EventTimeline;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.SimpleApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.rest.client.RoomsRestClient;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.TokensChunkEvents;
|
||||
import im.vector.matrix.android.internal.legacy.util.FilterUtil;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Layer for retrieving data either from the storage implementation, or from the server if the information is not available.
|
||||
*/
|
||||
public class DataRetriever {
|
||||
private static final String LOG_TAG = DataRetriever.class.getSimpleName();
|
||||
|
||||
private RoomsRestClient mRestClient;
|
||||
|
||||
private final Map<String, String> mPendingForwardRequestTokenByRoomId = new HashMap<>();
|
||||
private final Map<String, String> mPendingBackwardRequestTokenByRoomId = new HashMap<>();
|
||||
private final Map<String, String> mPendingRemoteRequestTokenByRoomId = new HashMap<>();
|
||||
|
||||
public RoomsRestClient getRoomsRestClient() {
|
||||
return mRestClient;
|
||||
}
|
||||
|
||||
public void setRoomsRestClient(final RoomsRestClient client) {
|
||||
mRestClient = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the cached messages for a dedicated roomId
|
||||
*
|
||||
* @param store the store.
|
||||
* @param roomId the roomId
|
||||
* @return the events list, null if the room does not exist
|
||||
*/
|
||||
public Collection<Event> getCachedRoomMessages(final IMXStore store, final String roomId) {
|
||||
return store.getRoomMessages(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel any history requests for a dedicated room
|
||||
*
|
||||
* @param roomId the room id.
|
||||
*/
|
||||
public void cancelHistoryRequests(final String roomId) {
|
||||
Log.d(LOG_TAG, "## cancelHistoryRequests() : roomId " + roomId);
|
||||
|
||||
clearPendingToken(mPendingForwardRequestTokenByRoomId, roomId);
|
||||
clearPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel any request history requests for a dedicated room
|
||||
*
|
||||
* @param roomId the room id.
|
||||
*/
|
||||
public void cancelRemoteHistoryRequest(final String roomId) {
|
||||
Log.d(LOG_TAG, "## cancelRemoteHistoryRequest() : roomId " + roomId);
|
||||
|
||||
clearPendingToken(mPendingRemoteRequestTokenByRoomId, roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the event associated with the eventId and roomId
|
||||
* Look in the store before hitting the rest client.
|
||||
*
|
||||
* @param store the store to look in
|
||||
* @param roomId the room Id
|
||||
* @param eventId the eventId
|
||||
* @param callback the callback
|
||||
*/
|
||||
public void getEvent(final IMXStore store, final String roomId, final String eventId, final ApiCallback<Event> callback) {
|
||||
final Event event = store.getEvent(eventId, roomId);
|
||||
if (event == null) {
|
||||
mRestClient.getEvent(roomId, eventId, callback);
|
||||
} else {
|
||||
callback.onSuccess(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a back pagination for a dedicated room from Token.
|
||||
*
|
||||
* @param store the store to use
|
||||
* @param roomId the room Id
|
||||
* @param token the start token.
|
||||
* @param limit the maximum number of messages to retrieve
|
||||
* @param withLazyLoading true when lazy loading is enabled
|
||||
* @param callback the callback
|
||||
*/
|
||||
public void backPaginate(final IMXStore store,
|
||||
final String roomId,
|
||||
final String token,
|
||||
final int limit,
|
||||
final boolean withLazyLoading,
|
||||
final ApiCallback<TokensChunkEvents> callback) {
|
||||
// reach the marker end
|
||||
if (TextUtils.equals(token, Event.PAGINATE_BACK_TOKEN_END)) {
|
||||
// nothing more to provide
|
||||
final android.os.Handler handler = new android.os.Handler(Looper.getMainLooper());
|
||||
|
||||
// call the callback with a delay
|
||||
// to reproduce the same behaviour as a network request.
|
||||
// except for the initial request.
|
||||
Runnable r = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
handler.postDelayed(new Runnable() {
|
||||
public void run() {
|
||||
callback.onSuccess(new TokensChunkEvents());
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
handler.post(r);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(LOG_TAG, "## backPaginate() : starts for roomId " + roomId);
|
||||
|
||||
TokensChunkEvents storageResponse = store.getEarlierMessages(roomId, token, limit);
|
||||
|
||||
putPendingToken(mPendingBackwardRequestTokenByRoomId, roomId, token);
|
||||
|
||||
if (storageResponse != null) {
|
||||
final android.os.Handler handler = new android.os.Handler(Looper.getMainLooper());
|
||||
final TokensChunkEvents fStorageResponse = storageResponse;
|
||||
|
||||
Log.d(LOG_TAG, "## backPaginate() : some data has been retrieved into the local storage (" + fStorageResponse.chunk.size() + " events)");
|
||||
|
||||
// call the callback with a delay
|
||||
// to reproduce the same behaviour as a network request.
|
||||
// except for the initial request.
|
||||
Runnable r = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
handler.postDelayed(new Runnable() {
|
||||
public void run() {
|
||||
String expectedToken = getPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
|
||||
Log.d(LOG_TAG, "## backPaginate() : local store roomId " + roomId + " token " + token + " vs " + expectedToken);
|
||||
|
||||
if (TextUtils.equals(expectedToken, token)) {
|
||||
clearPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
|
||||
callback.onSuccess(fStorageResponse);
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
Thread t = new Thread(r);
|
||||
t.start();
|
||||
} else {
|
||||
Log.d(LOG_TAG, "## backPaginate() : trigger a remote request");
|
||||
|
||||
mRestClient.getRoomMessagesFrom(roomId, token, EventTimeline.Direction.BACKWARDS, limit, FilterUtil.createRoomEventFilter(withLazyLoading),
|
||||
new SimpleApiCallback<TokensChunkEvents>(callback) {
|
||||
@Override
|
||||
public void onSuccess(TokensChunkEvents tokensChunkEvents) {
|
||||
String expectedToken = getPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
|
||||
|
||||
Log.d(LOG_TAG, "## backPaginate() succeeds : roomId " + roomId + " token " + token + " vs " + expectedToken);
|
||||
|
||||
if (TextUtils.equals(expectedToken, token)) {
|
||||
clearPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
|
||||
|
||||
// Watch for the one event overlap
|
||||
Event oldestEvent = store.getOldestEvent(roomId);
|
||||
|
||||
if (tokensChunkEvents.chunk.size() != 0) {
|
||||
tokensChunkEvents.chunk.get(0).mToken = tokensChunkEvents.start;
|
||||
|
||||
// there is no more data on server side
|
||||
if (null == tokensChunkEvents.end) {
|
||||
tokensChunkEvents.end = Event.PAGINATE_BACK_TOKEN_END;
|
||||
}
|
||||
|
||||
tokensChunkEvents.chunk.get(tokensChunkEvents.chunk.size() - 1).mToken = tokensChunkEvents.end;
|
||||
|
||||
Event firstReturnedEvent = tokensChunkEvents.chunk.get(0);
|
||||
if ((oldestEvent != null) && (firstReturnedEvent != null)
|
||||
&& TextUtils.equals(oldestEvent.eventId, firstReturnedEvent.eventId)) {
|
||||
tokensChunkEvents.chunk.remove(0);
|
||||
}
|
||||
|
||||
store.storeRoomEvents(roomId, tokensChunkEvents, EventTimeline.Direction.BACKWARDS);
|
||||
}
|
||||
|
||||
Log.d(LOG_TAG, "## backPaginate() succeed : roomId " + roomId
|
||||
+ " token " + token
|
||||
+ " got " + tokensChunkEvents.chunk.size());
|
||||
callback.onSuccess(tokensChunkEvents);
|
||||
}
|
||||
}
|
||||
|
||||
private void logErrorMessage(String expectedToken, String errorMessage) {
|
||||
Log.e(LOG_TAG, "## backPaginate() failed : roomId " + roomId
|
||||
+ " token " + token
|
||||
+ " expected " + expectedToken
|
||||
+ " with " + errorMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
String expectedToken = getPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
|
||||
logErrorMessage(expectedToken, e.getMessage());
|
||||
|
||||
// dispatch only if it is expected
|
||||
if (TextUtils.equals(token, expectedToken)) {
|
||||
clearPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
|
||||
callback.onNetworkError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
String expectedToken = getPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
|
||||
logErrorMessage(expectedToken, e.getMessage());
|
||||
|
||||
// dispatch only if it is expected
|
||||
if (TextUtils.equals(token, expectedToken)) {
|
||||
clearPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
|
||||
callback.onMatrixError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
String expectedToken = getPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
|
||||
logErrorMessage(expectedToken, e.getMessage());
|
||||
|
||||
// dispatch only if it is expected
|
||||
if (TextUtils.equals(token, expectedToken)) {
|
||||
clearPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
|
||||
callback.onUnexpectedError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a forward pagination for a dedicated room from Token.
|
||||
*
|
||||
* @param store the store to use
|
||||
* @param roomId the room Id
|
||||
* @param token the start token.
|
||||
* @param withLazyLoading true when lazy loading is enabled
|
||||
* @param callback the callback
|
||||
*/
|
||||
private void forwardPaginate(final IMXStore store,
|
||||
final String roomId,
|
||||
final String token,
|
||||
final boolean withLazyLoading,
|
||||
final ApiCallback<TokensChunkEvents> callback) {
|
||||
putPendingToken(mPendingForwardRequestTokenByRoomId, roomId, token);
|
||||
|
||||
mRestClient.getRoomMessagesFrom(roomId, token, EventTimeline.Direction.FORWARDS, RoomsRestClient.DEFAULT_MESSAGES_PAGINATION_LIMIT,
|
||||
FilterUtil.createRoomEventFilter(withLazyLoading),
|
||||
new SimpleApiCallback<TokensChunkEvents>(callback) {
|
||||
@Override
|
||||
public void onSuccess(TokensChunkEvents tokensChunkEvents) {
|
||||
if (TextUtils.equals(getPendingToken(mPendingForwardRequestTokenByRoomId, roomId), token)) {
|
||||
clearPendingToken(mPendingForwardRequestTokenByRoomId, roomId);
|
||||
store.storeRoomEvents(roomId, tokensChunkEvents, EventTimeline.Direction.FORWARDS);
|
||||
callback.onSuccess(tokensChunkEvents);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Request messages than the given token. These will come from storage if available, from the server otherwise.
|
||||
*
|
||||
* @param store the store to use
|
||||
* @param roomId the room id
|
||||
* @param token the token to go back from. Null to start from live.
|
||||
* @param direction the pagination direction
|
||||
* @param withLazyLoading true when lazy loading is enabled
|
||||
* @param callback the onComplete callback
|
||||
*/
|
||||
public void paginate(final IMXStore store,
|
||||
final String roomId,
|
||||
final String token,
|
||||
final EventTimeline.Direction direction,
|
||||
final boolean withLazyLoading,
|
||||
final ApiCallback<TokensChunkEvents> callback) {
|
||||
if (direction == EventTimeline.Direction.BACKWARDS) {
|
||||
backPaginate(store, roomId, token, RoomsRestClient.DEFAULT_MESSAGES_PAGINATION_LIMIT, withLazyLoading, callback);
|
||||
} else {
|
||||
forwardPaginate(store, roomId, token, withLazyLoading, callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request events to the server. The local cache is not used.
|
||||
* The events will not be saved in the local storage.
|
||||
*
|
||||
* @param roomId the room id
|
||||
* @param token the token to go back from.
|
||||
* @param paginationCount the number of events to retrieve.
|
||||
* @param withLazyLoading true when lazy loading is enabled
|
||||
* @param callback the onComplete callback
|
||||
*/
|
||||
public void requestServerRoomHistory(final String roomId,
|
||||
final String token,
|
||||
final int paginationCount,
|
||||
final boolean withLazyLoading,
|
||||
final ApiCallback<TokensChunkEvents> callback) {
|
||||
putPendingToken(mPendingRemoteRequestTokenByRoomId, roomId, token);
|
||||
|
||||
mRestClient.getRoomMessagesFrom(roomId, token, EventTimeline.Direction.BACKWARDS, paginationCount, FilterUtil.createRoomEventFilter(withLazyLoading),
|
||||
new SimpleApiCallback<TokensChunkEvents>(callback) {
|
||||
@Override
|
||||
public void onSuccess(TokensChunkEvents info) {
|
||||
if (TextUtils.equals(getPendingToken(mPendingRemoteRequestTokenByRoomId, roomId), token)) {
|
||||
if (info.chunk.size() != 0) {
|
||||
info.chunk.get(0).mToken = info.start;
|
||||
info.chunk.get(info.chunk.size() - 1).mToken = info.end;
|
||||
}
|
||||
|
||||
clearPendingToken(mPendingRemoteRequestTokenByRoomId, roomId);
|
||||
callback.onSuccess(info);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// Pending token management
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* Clear token for a dedicated room
|
||||
*
|
||||
* @param dict the token cache
|
||||
* @param roomId the room id
|
||||
*/
|
||||
private void clearPendingToken(final Map<String, String> dict, final String roomId) {
|
||||
Log.d(LOG_TAG, "## clearPendingToken() : roomId " + roomId);
|
||||
|
||||
if (null != roomId) {
|
||||
synchronized (dict) {
|
||||
dict.remove(roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pending token for a dedicated room
|
||||
*
|
||||
* @param dict the token cache
|
||||
* @param roomId the room Id
|
||||
* @return the token
|
||||
*/
|
||||
private String getPendingToken(final Map<String, String> dict, final String roomId) {
|
||||
String expectedToken = "Not a valid token";
|
||||
|
||||
synchronized (dict) {
|
||||
// token == null is a valid value
|
||||
if (dict.containsKey(roomId)) {
|
||||
expectedToken = dict.get(roomId);
|
||||
|
||||
if (TextUtils.isEmpty(expectedToken)) {
|
||||
expectedToken = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.d(LOG_TAG, "## getPendingToken() : roomId " + roomId + " token " + expectedToken);
|
||||
|
||||
return expectedToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a token for a dedicated room
|
||||
*
|
||||
* @param dict the token cache
|
||||
* @param roomId the room id
|
||||
* @param token the token
|
||||
*/
|
||||
private void putPendingToken(final Map<String, String> dict, final String roomId, final String token) {
|
||||
Log.d(LOG_TAG, "## putPendingToken() : roomId " + roomId + " token " + token);
|
||||
|
||||
synchronized (dict) {
|
||||
// null is allowed for a request
|
||||
if (null == token) {
|
||||
dict.put(roomId, "");
|
||||
} else {
|
||||
dict.put(roomId, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,446 @@
|
||||
/*
|
||||
* Copyright 2014 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.SimpleApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.User;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.pid.ThirdPartyIdentifier;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.pid.ThreePid;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Class representing the logged-in user.
|
||||
*/
|
||||
public class MyUser extends User {
|
||||
|
||||
private static final String LOG_TAG = MyUser.class.getSimpleName();
|
||||
|
||||
// refresh status
|
||||
private boolean mIsAvatarRefreshed = false;
|
||||
private boolean mIsDisplayNameRefreshed = false;
|
||||
private boolean mAre3PIdsLoaded = false;
|
||||
|
||||
// the account info is refreshed in one row
|
||||
// so, if there is a pending refresh the listeners are added to this list.
|
||||
private transient List<ApiCallback<Void>> mRefreshListeners;
|
||||
|
||||
private transient final Handler mUiHandler;
|
||||
|
||||
// linked emails to the account
|
||||
private transient List<ThirdPartyIdentifier> mEmailIdentifiers = new ArrayList<>();
|
||||
// linked phone number to the account
|
||||
private transient List<ThirdPartyIdentifier> mPhoneNumberIdentifiers = new ArrayList<>();
|
||||
|
||||
public MyUser(User user) {
|
||||
clone(user);
|
||||
|
||||
mUiHandler = new Handler(Looper.getMainLooper());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's display name.
|
||||
*
|
||||
* @param displayName the new name
|
||||
* @param callback the async callback
|
||||
*/
|
||||
public void updateDisplayName(final String displayName, final ApiCallback<Void> callback) {
|
||||
mDataHandler.getProfileRestClient().updateDisplayname(displayName, new SimpleApiCallback<Void>(callback) {
|
||||
@Override
|
||||
public void onSuccess(Void info) {
|
||||
// Update the object member before calling the given callback
|
||||
MyUser.this.displayname = displayName;
|
||||
mDataHandler.getStore().setDisplayName(displayName, System.currentTimeMillis());
|
||||
|
||||
callback.onSuccess(info);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's avatar URL.
|
||||
*
|
||||
* @param avatarUrl the new avatar URL
|
||||
* @param callback the async callback
|
||||
*/
|
||||
public void updateAvatarUrl(final String avatarUrl, final ApiCallback<Void> callback) {
|
||||
mDataHandler.getProfileRestClient().updateAvatarUrl(avatarUrl, new SimpleApiCallback<Void>(callback) {
|
||||
@Override
|
||||
public void onSuccess(Void info) {
|
||||
// Update the object member before calling the given callback
|
||||
setAvatarUrl(avatarUrl);
|
||||
mDataHandler.getStore().setAvatarURL(avatarUrl, System.currentTimeMillis());
|
||||
|
||||
callback.onSuccess(info);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a validation token for an email address 3Pid
|
||||
*
|
||||
* @param pid the pid to retrieve a token
|
||||
* @param callback the callback when the operation is done
|
||||
*/
|
||||
public void requestEmailValidationToken(ThreePid pid, ApiCallback<Void> callback) {
|
||||
if (null != pid) {
|
||||
pid.requestEmailValidationToken(mDataHandler.getProfileRestClient(), null, false, callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a validation token for a phone number 3Pid
|
||||
*
|
||||
* @param pid the pid to retrieve a token
|
||||
* @param callback the callback when the operation is done
|
||||
*/
|
||||
public void requestPhoneNumberValidationToken(ThreePid pid, ApiCallback<Void> callback) {
|
||||
if (null != pid) {
|
||||
pid.requestPhoneNumberValidationToken(mDataHandler.getProfileRestClient(), false, callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new pid to the account.
|
||||
*
|
||||
* @param pid the pid to add.
|
||||
* @param bind true to add it.
|
||||
* @param callback the async callback
|
||||
*/
|
||||
public void add3Pid(final ThreePid pid, final boolean bind, final ApiCallback<Void> callback) {
|
||||
if (null != pid) {
|
||||
mDataHandler.getProfileRestClient().add3PID(pid, bind, new SimpleApiCallback<Void>(callback) {
|
||||
@Override
|
||||
public void onSuccess(Void info) {
|
||||
// refresh the third party identifiers lists
|
||||
refreshThirdPartyIdentifiers(callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a 3pid from an account
|
||||
*
|
||||
* @param pid the pid to delete
|
||||
* @param callback the async callback
|
||||
*/
|
||||
public void delete3Pid(final ThirdPartyIdentifier pid, final ApiCallback<Void> callback) {
|
||||
if (null != pid) {
|
||||
mDataHandler.getProfileRestClient().delete3PID(pid, new SimpleApiCallback<Void>(callback) {
|
||||
@Override
|
||||
public void onSuccess(Void info) {
|
||||
// refresh the third party identifiers lists
|
||||
refreshThirdPartyIdentifiers(callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the lists of identifiers
|
||||
*/
|
||||
private void buildIdentifiersLists() {
|
||||
List<ThirdPartyIdentifier> identifiers = mDataHandler.getStore().thirdPartyIdentifiers();
|
||||
mEmailIdentifiers = new ArrayList<>();
|
||||
mPhoneNumberIdentifiers = new ArrayList<>();
|
||||
for (ThirdPartyIdentifier identifier : identifiers) {
|
||||
switch (identifier.medium) {
|
||||
case ThreePid.MEDIUM_EMAIL:
|
||||
mEmailIdentifiers.add(identifier);
|
||||
break;
|
||||
case ThreePid.MEDIUM_MSISDN:
|
||||
mPhoneNumberIdentifiers.add(identifier);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the list of linked emails
|
||||
*/
|
||||
public List<ThirdPartyIdentifier> getlinkedEmails() {
|
||||
if (mEmailIdentifiers == null) {
|
||||
buildIdentifiersLists();
|
||||
}
|
||||
|
||||
return mEmailIdentifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the list of linked emails
|
||||
*/
|
||||
public List<ThirdPartyIdentifier> getlinkedPhoneNumbers() {
|
||||
if (mPhoneNumberIdentifiers == null) {
|
||||
buildIdentifiersLists();
|
||||
}
|
||||
|
||||
return mPhoneNumberIdentifiers;
|
||||
}
|
||||
|
||||
//================================================================================
|
||||
// Refresh
|
||||
//================================================================================
|
||||
|
||||
/**
|
||||
* Refresh the user data if it is required
|
||||
*
|
||||
* @param callback callback when the job is done.
|
||||
*/
|
||||
public void refreshUserInfos(final ApiCallback<Void> callback) {
|
||||
refreshUserInfos(false, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the user data if it is required
|
||||
*
|
||||
* @param callback callback when the job is done.
|
||||
*/
|
||||
public void refreshThirdPartyIdentifiers(final ApiCallback<Void> callback) {
|
||||
mAre3PIdsLoaded = false;
|
||||
refreshUserInfos(false, callback);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Refresh the user data if it is required
|
||||
*
|
||||
* @param skipPendingTest true to do not check if the refreshes started (private use)
|
||||
* @param callback callback when the job is done.
|
||||
*/
|
||||
public void refreshUserInfos(boolean skipPendingTest, final ApiCallback<Void> callback) {
|
||||
if (!skipPendingTest) {
|
||||
boolean isPending;
|
||||
|
||||
synchronized (this) {
|
||||
// mRefreshListeners == null => no refresh in progress
|
||||
// mRefreshListeners != null -> a refresh is in progress
|
||||
isPending = (null != mRefreshListeners);
|
||||
|
||||
if (null == mRefreshListeners) {
|
||||
mRefreshListeners = new ArrayList<>();
|
||||
}
|
||||
|
||||
if (null != callback) {
|
||||
mRefreshListeners.add(callback);
|
||||
}
|
||||
}
|
||||
|
||||
if (isPending) {
|
||||
// please wait
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mIsDisplayNameRefreshed) {
|
||||
refreshUserDisplayname();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mIsAvatarRefreshed) {
|
||||
refreshUserAvatarUrl();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mAre3PIdsLoaded) {
|
||||
refreshThirdPartyIdentifiers();
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
if (null != mRefreshListeners) {
|
||||
for (ApiCallback<Void> listener : mRefreshListeners) {
|
||||
try {
|
||||
listener.onSuccess(null);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## refreshUserInfos() : listener.onSuccess failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no more pending refreshes
|
||||
mRefreshListeners = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the avatar url
|
||||
*/
|
||||
private void refreshUserAvatarUrl() {
|
||||
mDataHandler.getProfileRestClient().avatarUrl(user_id, new SimpleApiCallback<String>() {
|
||||
@Override
|
||||
public void onSuccess(String anAvatarUrl) {
|
||||
if (mDataHandler.isAlive()) {
|
||||
// local value
|
||||
setAvatarUrl(anAvatarUrl);
|
||||
// metadata file
|
||||
mDataHandler.getStore().setAvatarURL(anAvatarUrl, System.currentTimeMillis());
|
||||
// user
|
||||
mDataHandler.getStore().storeUser(MyUser.this);
|
||||
|
||||
mIsAvatarRefreshed = true;
|
||||
|
||||
// jump to the next items
|
||||
refreshUserInfos(true, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void onError() {
|
||||
if (mDataHandler.isAlive()) {
|
||||
mUiHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
refreshUserAvatarUrl();
|
||||
}
|
||||
}, 1 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
onError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(final MatrixError e) {
|
||||
// cannot retrieve this value, jump to the next items
|
||||
mIsAvatarRefreshed = true;
|
||||
refreshUserInfos(true, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(final Exception e) {
|
||||
// cannot retrieve this value, jump to the next items
|
||||
mIsAvatarRefreshed = true;
|
||||
refreshUserInfos(true, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the displayname.
|
||||
*/
|
||||
private void refreshUserDisplayname() {
|
||||
mDataHandler.getProfileRestClient().displayname(user_id, new SimpleApiCallback<String>() {
|
||||
@Override
|
||||
public void onSuccess(String aDisplayname) {
|
||||
if (mDataHandler.isAlive()) {
|
||||
// local value
|
||||
displayname = aDisplayname;
|
||||
// store metadata
|
||||
mDataHandler.getStore().setDisplayName(aDisplayname, System.currentTimeMillis());
|
||||
|
||||
mIsDisplayNameRefreshed = true;
|
||||
|
||||
// jump to the next items
|
||||
refreshUserInfos(true, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void onError() {
|
||||
if (mDataHandler.isAlive()) {
|
||||
mUiHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
refreshUserDisplayname();
|
||||
}
|
||||
}, 1 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
onError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(final MatrixError e) {
|
||||
// cannot retrieve this value, jump to the next items
|
||||
mIsDisplayNameRefreshed = true;
|
||||
refreshUserInfos(true, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(final Exception e) {
|
||||
// cannot retrieve this value, jump to the next items
|
||||
mIsDisplayNameRefreshed = true;
|
||||
refreshUserInfos(true, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the Third party identifiers i.e. the linked email to this account
|
||||
*/
|
||||
public void refreshThirdPartyIdentifiers() {
|
||||
mDataHandler.getProfileRestClient().threePIDs(new SimpleApiCallback<List<ThirdPartyIdentifier>>() {
|
||||
@Override
|
||||
public void onSuccess(List<ThirdPartyIdentifier> identifiers) {
|
||||
if (mDataHandler.isAlive()) {
|
||||
// store
|
||||
mDataHandler.getStore().setThirdPartyIdentifiers(identifiers);
|
||||
|
||||
buildIdentifiersLists();
|
||||
|
||||
mAre3PIdsLoaded = true;
|
||||
|
||||
// jump to the next items
|
||||
refreshUserInfos(true, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void onError() {
|
||||
if (mDataHandler.isAlive()) {
|
||||
mUiHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
refreshThirdPartyIdentifiers();
|
||||
}
|
||||
}, 1 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
onError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(final MatrixError e) {
|
||||
// cannot retrieve this value, jump to the next items
|
||||
mAre3PIdsLoaded = true;
|
||||
refreshUserInfos(true, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(final Exception e) {
|
||||
// cannot retrieve this value, jump to the next items
|
||||
mAre3PIdsLoaded = true;
|
||||
refreshUserInfos(true, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class Pusher {
|
||||
public String pushkey;
|
||||
public Object kind;
|
||||
public String profileTag;
|
||||
public String appId;
|
||||
public String appDisplayName;
|
||||
public String deviceDisplayName;
|
||||
public String lang;
|
||||
public Map<String, String> data;
|
||||
public Boolean append;
|
||||
|
||||
|
||||
@Override
|
||||
public java.lang.String toString() {
|
||||
return "Pusher : \n\tappDisplayName " + appDisplayName + "\n\tdeviceDisplayName " + deviceDisplayName + "\n\tpushkey " + pushkey;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2014 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Class representing private data that the user has defined for a room.
|
||||
*/
|
||||
public class RoomAccountData implements java.io.Serializable {
|
||||
|
||||
private static final long serialVersionUID = -8406116277864521120L;
|
||||
|
||||
// The tags the user defined for this room.
|
||||
// The key is the tag name. The value, the associated MXRoomTag object.
|
||||
private Map<String, RoomTag> tags = null;
|
||||
|
||||
/**
|
||||
* Process an event that modifies room account data (like m.tag event).
|
||||
*
|
||||
* @param event an event
|
||||
*/
|
||||
public void handleTagEvent(Event event) {
|
||||
if (event.getType().equals(Event.EVENT_TYPE_TAGS)) {
|
||||
tags = RoomTag.roomTagsWithTagEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a RoomTag for a key.
|
||||
*
|
||||
* @param key the key.
|
||||
* @return the roomTag if it is found else null
|
||||
*/
|
||||
@Nullable
|
||||
public RoomTag roomTag(String key) {
|
||||
if ((null != tags) && tags.containsKey(key)) {
|
||||
return tags.get(key);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if some tags are defined
|
||||
*/
|
||||
public boolean hasTags() {
|
||||
return (null != tags) && (tags.size() > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the list of keys, or null if no tag
|
||||
*/
|
||||
@Nullable
|
||||
public Set<String> getKeys() {
|
||||
if (hasTags()) {
|
||||
return tags.keySet();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2014 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Class representing the email invitation parameters
|
||||
*/
|
||||
public class RoomEmailInvitation {
|
||||
|
||||
// the email invitation parameters
|
||||
// earch parameter can be null
|
||||
public String email;
|
||||
public String signUrl;
|
||||
public String roomName;
|
||||
public String roomAvatarUrl;
|
||||
public String inviterName;
|
||||
public String guestAccessToken;
|
||||
public String guestUserId;
|
||||
|
||||
// the constructor
|
||||
public RoomEmailInvitation(Map<String, String> parameters) {
|
||||
|
||||
if (null != parameters) {
|
||||
email = parameters.get("email");
|
||||
signUrl = parameters.get("signurl");
|
||||
roomName = parameters.get("room_name");
|
||||
roomAvatarUrl = parameters.get("room_avatar_url");
|
||||
inviterName = parameters.get("inviter_name");
|
||||
guestAccessToken = parameters.get("guestAccessToken");
|
||||
guestUserId = parameters.get("guest_user_id");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,901 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipDescription;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.listeners.IMXMediaUploadListener;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.message.Message;
|
||||
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
import im.vector.matrix.android.internal.legacy.util.ResourceUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* RoomMediaMessage encapsulates the media information to be sent.
|
||||
*/
|
||||
public class RoomMediaMessage implements Parcelable {
|
||||
private static final String LOG_TAG = RoomMediaMessage.class.getSimpleName();
|
||||
|
||||
private static final Uri mDummyUri = Uri.parse("http://www.matrixdummy.org");
|
||||
|
||||
/**
|
||||
* Interface to monitor event creation.
|
||||
*/
|
||||
public interface EventCreationListener {
|
||||
/**
|
||||
* The dedicated event has been created and added to the events list.
|
||||
*
|
||||
* @param roomMediaMessage the room media message.
|
||||
*/
|
||||
void onEventCreated(RoomMediaMessage roomMediaMessage);
|
||||
|
||||
/**
|
||||
* The event creation failed.
|
||||
*
|
||||
* @param roomMediaMessage the room media message.
|
||||
* @param errorMessage the failure reason
|
||||
*/
|
||||
void onEventCreationFailed(RoomMediaMessage roomMediaMessage, String errorMessage);
|
||||
|
||||
/**
|
||||
* The media encryption failed.
|
||||
*
|
||||
* @param roomMediaMessage the room media message.
|
||||
*/
|
||||
void onEncryptionFailed(RoomMediaMessage roomMediaMessage);
|
||||
}
|
||||
|
||||
// the item is defined either from an uri
|
||||
private Uri mUri;
|
||||
private String mMimeType;
|
||||
|
||||
// the message to send
|
||||
private Event mEvent;
|
||||
|
||||
// or a clipData Item
|
||||
private ClipData.Item mClipDataItem;
|
||||
|
||||
// the filename
|
||||
private String mFileName;
|
||||
|
||||
// Message.MSGTYPE_XX value
|
||||
private String mMessageType;
|
||||
|
||||
// The replyTo event
|
||||
@Nullable
|
||||
private Event mReplyToEvent;
|
||||
|
||||
// thumbnail size
|
||||
private Pair<Integer, Integer> mThumbnailSize = new Pair<>(100, 100);
|
||||
|
||||
// upload media upload listener
|
||||
private transient IMXMediaUploadListener mMediaUploadListener;
|
||||
|
||||
// event sending callback
|
||||
private transient ApiCallback<Void> mEventSendingCallback;
|
||||
|
||||
// event creation listener
|
||||
private transient EventCreationListener mEventCreationListener;
|
||||
|
||||
/**
|
||||
* Constructor from a ClipData.Item.
|
||||
* It might be used by a third party medias selection.
|
||||
*
|
||||
* @param clipDataItem the data item
|
||||
* @param mimeType the mime type
|
||||
*/
|
||||
public RoomMediaMessage(ClipData.Item clipDataItem, String mimeType) {
|
||||
mClipDataItem = clipDataItem;
|
||||
mMimeType = mimeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for a text message.
|
||||
*
|
||||
* @param text the text
|
||||
* @param htmlText the HTML text
|
||||
* @param format the formatted text format
|
||||
*/
|
||||
public RoomMediaMessage(CharSequence text, String htmlText, String format) {
|
||||
mClipDataItem = new ClipData.Item(text, htmlText);
|
||||
mMimeType = (null == htmlText) ? ClipDescription.MIMETYPE_TEXT_PLAIN : format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor from a media Uri/
|
||||
*
|
||||
* @param uri the media uri
|
||||
*/
|
||||
public RoomMediaMessage(Uri uri) {
|
||||
this(uri, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor from a media Uri/
|
||||
*
|
||||
* @param uri the media uri
|
||||
* @param filename the media file name
|
||||
*/
|
||||
public RoomMediaMessage(Uri uri, String filename) {
|
||||
mUri = uri;
|
||||
mFileName = filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor from an event.
|
||||
*
|
||||
* @param event the event
|
||||
*/
|
||||
public RoomMediaMessage(Event event) {
|
||||
setEvent(event);
|
||||
|
||||
Message message = JsonUtils.toMessage(event.getContent());
|
||||
if (null != message) {
|
||||
setMessageType(message.msgtype);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor from a parcel
|
||||
*
|
||||
* @param source the parcel
|
||||
*/
|
||||
private RoomMediaMessage(Parcel source) {
|
||||
mUri = unformatNullUri((Uri) source.readParcelable(Uri.class.getClassLoader()));
|
||||
mMimeType = unformatNullString(source.readString());
|
||||
|
||||
CharSequence clipDataItemText = unformatNullString(source.readString());
|
||||
String clipDataItemHtml = unformatNullString(source.readString());
|
||||
Uri clipDataItemUri = unformatNullUri((Uri) source.readParcelable(Uri.class.getClassLoader()));
|
||||
|
||||
if (!TextUtils.isEmpty(clipDataItemText) || !TextUtils.isEmpty(clipDataItemHtml) || (null != clipDataItemUri)) {
|
||||
mClipDataItem = new ClipData.Item(clipDataItemText, clipDataItemHtml, null, clipDataItemUri);
|
||||
}
|
||||
|
||||
mFileName = unformatNullString(source.readString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.lang.String toString() {
|
||||
String description = "";
|
||||
|
||||
description += "mUri " + mUri;
|
||||
description += " -- mMimeType " + mMimeType;
|
||||
description += " -- mEvent " + mEvent;
|
||||
description += " -- mClipDataItem " + mClipDataItem;
|
||||
description += " -- mFileName " + mFileName;
|
||||
description += " -- mMessageType " + mMessageType;
|
||||
description += " -- mThumbnailSize " + mThumbnailSize;
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// Parcelable
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* Unformat parcelled String
|
||||
*
|
||||
* @param string the string to unformat
|
||||
* @return the unformatted string
|
||||
*/
|
||||
private static String unformatNullString(final String string) {
|
||||
if (TextUtils.isEmpty(string)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert null uri to a dummy one
|
||||
*
|
||||
* @param uri the uri to unformat
|
||||
* @return the unformatted
|
||||
*/
|
||||
private static Uri unformatNullUri(final Uri uri) {
|
||||
if ((null == uri) || mDummyUri.equals(uri)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert null string to ""
|
||||
*
|
||||
* @param string the string to format
|
||||
* @return the formatted string
|
||||
*/
|
||||
private static String formatNullString(final String string) {
|
||||
if (TextUtils.isEmpty(string)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
private static String formatNullString(final CharSequence charSequence) {
|
||||
if (TextUtils.isEmpty(charSequence)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return charSequence.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert null uri to a dummy one
|
||||
*
|
||||
* @param uri the uri to format
|
||||
* @return the formatted
|
||||
*/
|
||||
private static Uri formatNullUri(final Uri uri) {
|
||||
if (null == uri) {
|
||||
return mDummyUri;
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeParcelable(formatNullUri(mUri), 0);
|
||||
dest.writeString(formatNullString(mMimeType));
|
||||
|
||||
if (null == mClipDataItem) {
|
||||
dest.writeString("");
|
||||
dest.writeString("");
|
||||
dest.writeParcelable(formatNullUri(null), 0);
|
||||
} else {
|
||||
dest.writeString(formatNullString(mClipDataItem.getText()));
|
||||
dest.writeString(formatNullString(mClipDataItem.getHtmlText()));
|
||||
dest.writeParcelable(formatNullUri(mClipDataItem.getUri()), 0);
|
||||
}
|
||||
|
||||
dest.writeString(formatNullString(mFileName));
|
||||
}
|
||||
|
||||
// Creator
|
||||
public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
|
||||
public RoomMediaMessage createFromParcel(Parcel in) {
|
||||
return new RoomMediaMessage(in);
|
||||
}
|
||||
|
||||
public RoomMediaMessage[] newArray(int size) {
|
||||
return new RoomMediaMessage[size];
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================================================
|
||||
// Setters / getters
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* Set the message type.
|
||||
*
|
||||
* @param messageType the message type.
|
||||
*/
|
||||
public void setMessageType(String messageType) {
|
||||
mMessageType = messageType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the message type.
|
||||
*/
|
||||
public String getMessageType() {
|
||||
return mMessageType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the replyTo event.
|
||||
*
|
||||
* @param replyToEvent the event to reply to
|
||||
*/
|
||||
public void setReplyToEvent(@Nullable Event replyToEvent) {
|
||||
mReplyToEvent = replyToEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the replyTo event.
|
||||
*/
|
||||
@Nullable
|
||||
public Event getReplyToEvent() {
|
||||
return mReplyToEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the inner event.
|
||||
*
|
||||
* @param event the new event.
|
||||
*/
|
||||
public void setEvent(Event event) {
|
||||
mEvent = event;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the inner event objects
|
||||
*/
|
||||
public Event getEvent() {
|
||||
return mEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the thumbnail size.
|
||||
*
|
||||
* @param size the new thumbnail size.
|
||||
*/
|
||||
public void setThumbnailSize(Pair<Integer, Integer> size) {
|
||||
mThumbnailSize = size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the thumbnail size.
|
||||
*/
|
||||
public Pair<Integer, Integer> getThumbnailSize() {
|
||||
return mThumbnailSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the media upload listener.
|
||||
*
|
||||
* @param mediaUploadListener the media upload listener.
|
||||
*/
|
||||
public void setMediaUploadListener(IMXMediaUploadListener mediaUploadListener) {
|
||||
mMediaUploadListener = mediaUploadListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the media upload listener.
|
||||
*/
|
||||
public IMXMediaUploadListener getMediaUploadListener() {
|
||||
return mMediaUploadListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the event sending callback.
|
||||
*
|
||||
* @param callback the callback
|
||||
*/
|
||||
public void setEventSendingCallback(ApiCallback<Void> callback) {
|
||||
mEventSendingCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the event sending callback.
|
||||
*/
|
||||
public ApiCallback<Void> getSendingCallback() {
|
||||
return mEventSendingCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the listener
|
||||
*
|
||||
* @param eventCreationListener the new listener
|
||||
*/
|
||||
public void setEventCreationListener(EventCreationListener eventCreationListener) {
|
||||
mEventCreationListener = eventCreationListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the listener.
|
||||
*/
|
||||
public EventCreationListener getEventCreationListener() {
|
||||
return mEventCreationListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the raw text contained in this Item.
|
||||
*
|
||||
* @return the raw text
|
||||
*/
|
||||
public CharSequence getText() {
|
||||
if (null != mClipDataItem) {
|
||||
return mClipDataItem.getText();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the raw HTML text contained in this Item.
|
||||
*
|
||||
* @return the raw HTML text
|
||||
*/
|
||||
public String getHtmlText() {
|
||||
if (null != mClipDataItem) {
|
||||
return mClipDataItem.getHtmlText();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the Intent contained in this Item.
|
||||
*
|
||||
* @return the intent
|
||||
*/
|
||||
public Intent getIntent() {
|
||||
if (null != mClipDataItem) {
|
||||
return mClipDataItem.getIntent();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the URI contained in this Item.
|
||||
*
|
||||
* @return the Uri
|
||||
*/
|
||||
public Uri getUri() {
|
||||
if (null != mUri) {
|
||||
return mUri;
|
||||
} else if (null != mClipDataItem) {
|
||||
return mClipDataItem.getUri();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mimetype.
|
||||
*
|
||||
* @param context the context
|
||||
* @return the mimetype
|
||||
*/
|
||||
public String getMimeType(Context context) {
|
||||
if ((null == mMimeType) && (null != getUri())) {
|
||||
try {
|
||||
Uri uri = getUri();
|
||||
mMimeType = context.getContentResolver().getType(uri);
|
||||
|
||||
// try to find the mimetype from the filename
|
||||
if (null == mMimeType) {
|
||||
String extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString().toLowerCase());
|
||||
if (extension != null) {
|
||||
mMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
}
|
||||
}
|
||||
|
||||
if (null != mMimeType) {
|
||||
// the mimetype is sometimes in uppercase.
|
||||
mMimeType = mMimeType.toLowerCase();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "Failed to open resource input stream", e);
|
||||
}
|
||||
}
|
||||
|
||||
return mMimeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the MINI_KIND image thumbnail.
|
||||
*
|
||||
* @param context the context
|
||||
* @return the MINI_KIND thumbnail it it exists
|
||||
*/
|
||||
public Bitmap getMiniKindImageThumbnail(Context context) {
|
||||
return getImageThumbnail(context, MediaStore.Images.Thumbnails.MINI_KIND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the FULL_SCREEN image thumbnail.
|
||||
*
|
||||
* @param context the context
|
||||
* @return the FULL_SCREEN thumbnail it it exists
|
||||
*/
|
||||
public Bitmap getFullScreenImageKindThumbnail(Context context) {
|
||||
return getImageThumbnail(context, MediaStore.Images.Thumbnails.FULL_SCREEN_KIND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the image thumbnail.
|
||||
*
|
||||
* @param context the context.
|
||||
* @param kind the thumbnail kind.
|
||||
* @return the thumbnail.
|
||||
*/
|
||||
private Bitmap getImageThumbnail(Context context, int kind) {
|
||||
// sanity check
|
||||
if ((null == getMimeType(context)) || !getMimeType(context).startsWith("image/")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Bitmap thumbnailBitmap = null;
|
||||
|
||||
try {
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
|
||||
List uriPath = getUri().getPathSegments();
|
||||
Long imageId;
|
||||
String lastSegment = (String) uriPath.get(uriPath.size() - 1);
|
||||
|
||||
// > Kitkat
|
||||
if (lastSegment.startsWith("image:")) {
|
||||
lastSegment = lastSegment.substring("image:".length());
|
||||
}
|
||||
|
||||
try {
|
||||
imageId = Long.parseLong(lastSegment);
|
||||
} catch (Exception e) {
|
||||
imageId = null;
|
||||
}
|
||||
|
||||
if (null != imageId) {
|
||||
thumbnailBitmap = MediaStore.Images.Thumbnails.getThumbnail(resolver, imageId, kind, null);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "MediaStore.Images.Thumbnails.getThumbnail " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
return thumbnailBitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context the context
|
||||
* @return the filename
|
||||
*/
|
||||
public String getFileName(Context context) {
|
||||
if ((null == mFileName) && (null != getUri())) {
|
||||
Uri mediaUri = getUri();
|
||||
|
||||
if (null != mediaUri) {
|
||||
try {
|
||||
if (mediaUri.toString().startsWith("content://")) {
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = context.getContentResolver().query(mediaUri, null, null, null, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
mFileName = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "cursor.getString " + e.getMessage(), e);
|
||||
} finally {
|
||||
if (null != cursor) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(mFileName)) {
|
||||
List uriPath = mediaUri.getPathSegments();
|
||||
mFileName = (String) uriPath.get(uriPath.size() - 1);
|
||||
}
|
||||
} else if (mediaUri.toString().startsWith("file://")) {
|
||||
mFileName = mediaUri.getLastPathSegment();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
mFileName = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a media into a dedicated folder
|
||||
*
|
||||
* @param context the context
|
||||
* @param folder the folder.
|
||||
*/
|
||||
public void saveMedia(Context context, File folder) {
|
||||
mFileName = null;
|
||||
Uri mediaUri = getUri();
|
||||
|
||||
if (null != mediaUri) {
|
||||
try {
|
||||
ResourceUtils.Resource resource = ResourceUtils.openResource(context, mediaUri, getMimeType(context));
|
||||
|
||||
if (null == resource) {
|
||||
Log.e(LOG_TAG, "## saveMedia : Fail to retrieve the resource " + mediaUri);
|
||||
} else {
|
||||
mUri = saveFile(folder, resource.mContentStream, getFileName(context), resource.mMimeType);
|
||||
resource.mContentStream.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## saveMedia : failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a file in a dedicated directory.
|
||||
* The filename is optional.
|
||||
*
|
||||
* @param folder the destination folder
|
||||
* @param stream the file stream
|
||||
* @param defaultFileName the filename, null to generate a new one
|
||||
* @param mimeType the file mimetype.
|
||||
* @return the file uri
|
||||
*/
|
||||
private static Uri saveFile(File folder, InputStream stream, String defaultFileName, String mimeType) {
|
||||
String filename = defaultFileName;
|
||||
|
||||
if (null == filename) {
|
||||
filename = "file" + System.currentTimeMillis();
|
||||
|
||||
if (null != mimeType) {
|
||||
String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
|
||||
|
||||
if (null != extension) {
|
||||
filename += "." + extension;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Uri fileUri = null;
|
||||
|
||||
try {
|
||||
File file = new File(folder, filename);
|
||||
|
||||
// if the file exits, delete it
|
||||
if (file.exists()) {
|
||||
file.delete();
|
||||
}
|
||||
|
||||
FileOutputStream fos = new FileOutputStream(file.getPath());
|
||||
|
||||
try {
|
||||
byte[] buf = new byte[1024 * 32];
|
||||
|
||||
int len;
|
||||
while ((len = stream.read(buf)) != -1) {
|
||||
fos.write(buf, 0, len);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## saveFile failed " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
fos.flush();
|
||||
fos.close();
|
||||
stream.close();
|
||||
|
||||
fileUri = Uri.fromFile(file);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## saveFile failed " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
return fileUri;
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// Dispatchers
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* Dispatch onEventCreated.
|
||||
*/
|
||||
void onEventCreated() {
|
||||
if (null != getEventCreationListener()) {
|
||||
try {
|
||||
getEventCreationListener().onEventCreated(this);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## onEventCreated() failed : " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// clear the listener
|
||||
mEventCreationListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch onEventCreationFailed.
|
||||
*/
|
||||
void onEventCreationFailed(String errorMessage) {
|
||||
if (null != getEventCreationListener()) {
|
||||
try {
|
||||
getEventCreationListener().onEventCreationFailed(this, errorMessage);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## onEventCreationFailed() failed : " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// clear the listeners
|
||||
mMediaUploadListener = null;
|
||||
mEventSendingCallback = null;
|
||||
mEventCreationListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch onEncryptionFailed.
|
||||
*/
|
||||
void onEncryptionFailed() {
|
||||
if (null != getEventCreationListener()) {
|
||||
try {
|
||||
getEventCreationListener().onEncryptionFailed(this);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## onEncryptionFailed() failed : " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// clear the listeners
|
||||
mMediaUploadListener = null;
|
||||
mEventSendingCallback = null;
|
||||
mEventCreationListener = null;
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// Retrieve RoomMediaMessages from intents.
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* List the item provided in an intent.
|
||||
*
|
||||
* @param intent the intent.
|
||||
* @return the RoomMediaMessages list
|
||||
*/
|
||||
public static List<RoomMediaMessage> listRoomMediaMessages(Intent intent) {
|
||||
return listRoomMediaMessages(intent, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* List the item provided in an intent.
|
||||
*
|
||||
* @param intent the intent.
|
||||
* @param loader the class loader.
|
||||
* @return the room list
|
||||
*/
|
||||
public static List<RoomMediaMessage> listRoomMediaMessages(Intent intent, ClassLoader loader) {
|
||||
List<RoomMediaMessage> roomMediaMessages = new ArrayList<>();
|
||||
|
||||
|
||||
if (null != intent) {
|
||||
// chrome adds many items when sharing an web page link
|
||||
// so, test first the type
|
||||
if (TextUtils.equals(intent.getType(), ClipDescription.MIMETYPE_TEXT_PLAIN)) {
|
||||
String message = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
|
||||
if (null == message) {
|
||||
CharSequence sequence = intent.getCharSequenceExtra(Intent.EXTRA_TEXT);
|
||||
if (null != sequence) {
|
||||
message = sequence.toString();
|
||||
}
|
||||
}
|
||||
|
||||
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
|
||||
|
||||
if (!TextUtils.isEmpty(subject)) {
|
||||
if (TextUtils.isEmpty(message)) {
|
||||
message = subject;
|
||||
} else if (android.util.Patterns.WEB_URL.matcher(message).matches()) {
|
||||
message = subject + "\n" + message;
|
||||
}
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(message)) {
|
||||
roomMediaMessages.add(new RoomMediaMessage(message, null, intent.getType()));
|
||||
return roomMediaMessages;
|
||||
}
|
||||
}
|
||||
|
||||
ClipData clipData = null;
|
||||
List<String> mimetypes = null;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
clipData = intent.getClipData();
|
||||
}
|
||||
|
||||
// multiple data
|
||||
if (null != clipData) {
|
||||
if (null != clipData.getDescription()) {
|
||||
if (0 != clipData.getDescription().getMimeTypeCount()) {
|
||||
mimetypes = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < clipData.getDescription().getMimeTypeCount(); i++) {
|
||||
mimetypes.add(clipData.getDescription().getMimeType(i));
|
||||
}
|
||||
|
||||
// if the filter is "accept anything" the mimetype does not make sense
|
||||
if (1 == mimetypes.size()) {
|
||||
if (mimetypes.get(0).endsWith("/*")) {
|
||||
mimetypes = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int count = clipData.getItemCount();
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
ClipData.Item item = clipData.getItemAt(i);
|
||||
String mimetype = null;
|
||||
|
||||
if (null != mimetypes) {
|
||||
if (i < mimetypes.size()) {
|
||||
mimetype = mimetypes.get(i);
|
||||
} else {
|
||||
mimetype = mimetypes.get(0);
|
||||
}
|
||||
|
||||
// uris list is not a valid mimetype
|
||||
if (TextUtils.equals(mimetype, ClipDescription.MIMETYPE_TEXT_URILIST)) {
|
||||
mimetype = null;
|
||||
}
|
||||
}
|
||||
|
||||
roomMediaMessages.add(new RoomMediaMessage(item, mimetype));
|
||||
}
|
||||
} else if (null != intent.getData()) {
|
||||
roomMediaMessages.add(new RoomMediaMessage(intent.getData()));
|
||||
} else {
|
||||
Bundle bundle = intent.getExtras();
|
||||
|
||||
if (null != bundle) {
|
||||
// provide a custom loader
|
||||
bundle.setClassLoader(RoomMediaMessage.class.getClassLoader());
|
||||
// list the Uris list
|
||||
if (bundle.containsKey(Intent.EXTRA_STREAM)) {
|
||||
try {
|
||||
Object streamUri = bundle.get(Intent.EXTRA_STREAM);
|
||||
|
||||
if (streamUri instanceof Uri) {
|
||||
roomMediaMessages.add(new RoomMediaMessage((Uri) streamUri));
|
||||
} else if (streamUri instanceof List) {
|
||||
List<Object> streams = (List<Object>) streamUri;
|
||||
|
||||
for (Object object : streams) {
|
||||
if (object instanceof Uri) {
|
||||
roomMediaMessages.add(new RoomMediaMessage((Uri) object));
|
||||
} else if (object instanceof RoomMediaMessage) {
|
||||
roomMediaMessages.add((RoomMediaMessage) object);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "fail to extract the extra stream", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return roomMediaMessages;
|
||||
}
|
||||
}
|
@ -0,0 +1,983 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.media.ThumbnailUtils;
|
||||
import android.net.Uri;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import android.provider.MediaStore;
|
||||
import android.text.Html;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
|
||||
import im.vector.matrix.android.R;
|
||||
import im.vector.matrix.android.internal.legacy.MXDataHandler;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.MXEncryptedAttachments;
|
||||
import im.vector.matrix.android.internal.legacy.db.MXMediasCache;
|
||||
import im.vector.matrix.android.internal.legacy.listeners.MXMediaUploadListener;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.message.AudioMessage;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.message.FileMessage;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.message.ImageMessage;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.message.MediaMessage;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.message.Message;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.message.RelatesTo;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.message.VideoMessage;
|
||||
import im.vector.matrix.android.internal.legacy.util.ImageUtils;
|
||||
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
import im.vector.matrix.android.internal.legacy.util.PermalinkUtils;
|
||||
import im.vector.matrix.android.internal.legacy.util.ResourceUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Room helper to send media messages in the right order.
|
||||
*/
|
||||
class RoomMediaMessagesSender {
|
||||
private static final String LOG_TAG = RoomMediaMessagesSender.class.getSimpleName();
|
||||
|
||||
// pending events list
|
||||
private final List<RoomMediaMessage> mPendingRoomMediaMessages = new ArrayList<>();
|
||||
|
||||
// linked room
|
||||
private final Room mRoom;
|
||||
|
||||
// data handler
|
||||
private final MXDataHandler mDataHandler;
|
||||
|
||||
// linked context
|
||||
private final Context mContext;
|
||||
|
||||
// the sending item
|
||||
private RoomMediaMessage mSendingRoomMediaMessage;
|
||||
|
||||
// UI thread
|
||||
private static android.os.Handler mUiHandler = null;
|
||||
|
||||
// events creation threads
|
||||
private static android.os.Handler mEventHandler = null;
|
||||
|
||||
// encoding creation threads
|
||||
private static android.os.Handler mEncodingHandler = null;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param context the context
|
||||
* @param dataHandler the dataHanlder
|
||||
* @param room the room
|
||||
*/
|
||||
RoomMediaMessagesSender(Context context, MXDataHandler dataHandler, Room room) {
|
||||
mRoom = room;
|
||||
mContext = context.getApplicationContext();
|
||||
mDataHandler = dataHandler;
|
||||
|
||||
if (null == mUiHandler) {
|
||||
mUiHandler = new android.os.Handler(Looper.getMainLooper());
|
||||
|
||||
HandlerThread eventHandlerThread = new HandlerThread("RoomDataItemsSender_event", Thread.MIN_PRIORITY);
|
||||
eventHandlerThread.start();
|
||||
mEventHandler = new android.os.Handler(eventHandlerThread.getLooper());
|
||||
|
||||
HandlerThread encodingHandlerThread = new HandlerThread("RoomDataItemsSender_encoding", Thread.MIN_PRIORITY);
|
||||
encodingHandlerThread.start();
|
||||
mEncodingHandler = new android.os.Handler(encodingHandlerThread.getLooper());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a new media message to the room
|
||||
*
|
||||
* @param roomMediaMessage the message to send
|
||||
*/
|
||||
void send(final RoomMediaMessage roomMediaMessage) {
|
||||
mEventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (null == roomMediaMessage.getEvent()) {
|
||||
Message message;
|
||||
String mimeType = roomMediaMessage.getMimeType(mContext);
|
||||
|
||||
// avoid null case
|
||||
if (null == mimeType) {
|
||||
mimeType = "";
|
||||
}
|
||||
|
||||
if (null == roomMediaMessage.getUri()) {
|
||||
message = buildTextMessage(roomMediaMessage);
|
||||
} else if (mimeType.startsWith("image/")) {
|
||||
message = buildImageMessage(roomMediaMessage);
|
||||
} else if (mimeType.startsWith("video/")) {
|
||||
message = buildVideoMessage(roomMediaMessage);
|
||||
} else {
|
||||
message = buildFileMessage(roomMediaMessage);
|
||||
}
|
||||
|
||||
if (null == message) {
|
||||
Log.e(LOG_TAG, "## send " + roomMediaMessage + " not supported");
|
||||
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
roomMediaMessage.onEventCreationFailed("not supported " + roomMediaMessage);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
roomMediaMessage.setMessageType(message.msgtype);
|
||||
|
||||
if (roomMediaMessage.getReplyToEvent() != null) {
|
||||
// Note: it is placed here, but may be moved to the outer event during the encryption of the content
|
||||
message.relatesTo = new RelatesTo();
|
||||
message.relatesTo.dict = new HashMap<>();
|
||||
message.relatesTo.dict.put("event_id", roomMediaMessage.getReplyToEvent().eventId);
|
||||
}
|
||||
|
||||
Event event = new Event(message, mDataHandler.getUserId(), mRoom.getRoomId());
|
||||
|
||||
roomMediaMessage.setEvent(event);
|
||||
}
|
||||
|
||||
mDataHandler.updateEventState(roomMediaMessage.getEvent(), Event.SentState.UNSENT);
|
||||
mRoom.storeOutgoingEvent(roomMediaMessage.getEvent());
|
||||
mDataHandler.getStore().commit();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
roomMediaMessage.onEventCreated();
|
||||
}
|
||||
});
|
||||
|
||||
synchronized (LOG_TAG) {
|
||||
if (!mPendingRoomMediaMessages.contains(roomMediaMessage)) {
|
||||
mPendingRoomMediaMessages.add(roomMediaMessage);
|
||||
}
|
||||
}
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// send the item
|
||||
sendNext();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip the sending media item.
|
||||
*/
|
||||
private void skip() {
|
||||
synchronized (LOG_TAG) {
|
||||
mSendingRoomMediaMessage = null;
|
||||
}
|
||||
|
||||
sendNext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the next pending item
|
||||
*/
|
||||
private void sendNext() {
|
||||
RoomMediaMessage roomMediaMessage;
|
||||
|
||||
synchronized (LOG_TAG) {
|
||||
// please wait
|
||||
if (null != mSendingRoomMediaMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mPendingRoomMediaMessages.isEmpty()) {
|
||||
mSendingRoomMediaMessage = mPendingRoomMediaMessages.get(0);
|
||||
mPendingRoomMediaMessages.remove(0);
|
||||
} else {
|
||||
// nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
roomMediaMessage = mSendingRoomMediaMessage;
|
||||
}
|
||||
|
||||
// upload the medias first
|
||||
if (uploadMedias(roomMediaMessage)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// send the event
|
||||
sendEvent(roomMediaMessage.getEvent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the event after uploading the medias
|
||||
*
|
||||
* @param event the event to send
|
||||
*/
|
||||
private void sendEvent(final Event event) {
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// nothing more to upload
|
||||
mRoom.sendEvent(event, new ApiCallback<Void>() {
|
||||
private ApiCallback<Void> getCallback() {
|
||||
ApiCallback<Void> callback;
|
||||
|
||||
synchronized (LOG_TAG) {
|
||||
callback = mSendingRoomMediaMessage.getSendingCallback();
|
||||
mSendingRoomMediaMessage.setEventSendingCallback(null);
|
||||
mSendingRoomMediaMessage = null;
|
||||
}
|
||||
|
||||
return callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(Void info) {
|
||||
ApiCallback<Void> callback = getCallback();
|
||||
|
||||
if (null != callback) {
|
||||
try {
|
||||
callback.onSuccess(null);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## sendNext() failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
sendNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
ApiCallback<Void> callback = getCallback();
|
||||
|
||||
if (null != callback) {
|
||||
try {
|
||||
callback.onNetworkError(e);
|
||||
} catch (Exception e2) {
|
||||
Log.e(LOG_TAG, "## sendNext() failed " + e2.getMessage(), e2);
|
||||
}
|
||||
}
|
||||
|
||||
sendNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
ApiCallback<Void> callback = getCallback();
|
||||
|
||||
if (null != callback) {
|
||||
try {
|
||||
callback.onMatrixError(e);
|
||||
} catch (Exception e2) {
|
||||
Log.e(LOG_TAG, "## sendNext() failed " + e2.getMessage(), e2);
|
||||
}
|
||||
}
|
||||
|
||||
sendNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
ApiCallback<Void> callback = getCallback();
|
||||
|
||||
if (null != callback) {
|
||||
try {
|
||||
callback.onUnexpectedError(e);
|
||||
} catch (Exception e2) {
|
||||
Log.e(LOG_TAG, "## sendNext() failed " + e2.getMessage(), e2);
|
||||
}
|
||||
}
|
||||
|
||||
sendNext();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// Messages builder methods.
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* Build a text message from a RoomMediaMessage.
|
||||
*
|
||||
* @param roomMediaMessage the RoomMediaMessage.
|
||||
* @return the message
|
||||
*/
|
||||
private Message buildTextMessage(RoomMediaMessage roomMediaMessage) {
|
||||
CharSequence sequence = roomMediaMessage.getText();
|
||||
String htmlText = roomMediaMessage.getHtmlText();
|
||||
String text = null;
|
||||
|
||||
if (null == sequence) {
|
||||
if (null != htmlText) {
|
||||
text = Html.fromHtml(htmlText).toString();
|
||||
}
|
||||
} else {
|
||||
text = sequence.toString();
|
||||
}
|
||||
|
||||
// a text message cannot be null
|
||||
if (TextUtils.isEmpty(text) && !TextUtils.equals(roomMediaMessage.getMessageType(), Message.MSGTYPE_EMOTE)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Message message = new Message();
|
||||
message.msgtype = (null == roomMediaMessage.getMessageType()) ? Message.MSGTYPE_TEXT : roomMediaMessage.getMessageType();
|
||||
message.body = text;
|
||||
|
||||
// an emote can have an empty body
|
||||
if (null == message.body) {
|
||||
message.body = "";
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(htmlText)) {
|
||||
message.formatted_body = htmlText;
|
||||
message.format = Message.FORMAT_MATRIX_HTML;
|
||||
}
|
||||
|
||||
// Deals with in reply to event
|
||||
Event replyToEvent = roomMediaMessage.getReplyToEvent();
|
||||
if (replyToEvent != null) {
|
||||
// Cf. https://docs.google.com/document/d/1BPd4lBrooZrWe_3s_lHw_e-Dydvc7bXbm02_sV2k6Sc
|
||||
String msgType = JsonUtils.getMessageMsgType(replyToEvent.getContentAsJsonObject());
|
||||
|
||||
// Build body and formatted body, depending of the `msgtype` of the event the user is replying to
|
||||
if (msgType != null) {
|
||||
// Compute the content of the event user is replying to
|
||||
String replyToBody;
|
||||
String replyToFormattedBody;
|
||||
boolean replyToEventIsAlreadyAReply = false;
|
||||
|
||||
switch (msgType) {
|
||||
case Message.MSGTYPE_TEXT:
|
||||
case Message.MSGTYPE_NOTICE:
|
||||
case Message.MSGTYPE_EMOTE:
|
||||
Message messageToReplyTo = JsonUtils.toMessage(replyToEvent.getContentAsJsonObject());
|
||||
|
||||
replyToBody = messageToReplyTo.body;
|
||||
|
||||
if (TextUtils.isEmpty(messageToReplyTo.formatted_body)) {
|
||||
replyToFormattedBody = messageToReplyTo.body;
|
||||
} else {
|
||||
replyToFormattedBody = messageToReplyTo.formatted_body;
|
||||
}
|
||||
|
||||
replyToEventIsAlreadyAReply = messageToReplyTo.relatesTo != null
|
||||
&& messageToReplyTo.relatesTo.dict != null
|
||||
&& !TextUtils.isEmpty(messageToReplyTo.relatesTo.dict.get("event_id"));
|
||||
|
||||
break;
|
||||
case Message.MSGTYPE_IMAGE:
|
||||
replyToBody = mContext.getString(R.string.reply_to_an_image);
|
||||
replyToFormattedBody = replyToBody;
|
||||
break;
|
||||
case Message.MSGTYPE_VIDEO:
|
||||
replyToBody = mContext.getString(R.string.reply_to_a_video);
|
||||
replyToFormattedBody = replyToBody;
|
||||
break;
|
||||
case Message.MSGTYPE_AUDIO:
|
||||
replyToBody = mContext.getString(R.string.reply_to_an_audio_file);
|
||||
replyToFormattedBody = replyToBody;
|
||||
break;
|
||||
case Message.MSGTYPE_FILE:
|
||||
replyToBody = mContext.getString(R.string.reply_to_a_file);
|
||||
replyToFormattedBody = replyToBody;
|
||||
break;
|
||||
default:
|
||||
// Other msg types are not supported yet
|
||||
Log.w(LOG_TAG, "Reply to: unsupported msgtype: " + msgType);
|
||||
replyToBody = null;
|
||||
replyToFormattedBody = null;
|
||||
break;
|
||||
}
|
||||
|
||||
if (replyToBody != null) {
|
||||
String replyContent;
|
||||
if (TextUtils.isEmpty(message.formatted_body)) {
|
||||
replyContent = message.body;
|
||||
} else {
|
||||
replyContent = message.formatted_body;
|
||||
}
|
||||
|
||||
message.body = includeReplyToToBody(replyToEvent,
|
||||
replyToBody,
|
||||
replyToEventIsAlreadyAReply,
|
||||
message.body,
|
||||
msgType.equals(Message.MSGTYPE_EMOTE));
|
||||
message.formatted_body = includeReplyToToFormattedBody(replyToEvent,
|
||||
replyToFormattedBody,
|
||||
replyToEventIsAlreadyAReply,
|
||||
replyContent,
|
||||
msgType.equals(Message.MSGTYPE_EMOTE));
|
||||
|
||||
// Note: we need to force the format to Message.FORMAT_MATRIX_HTML
|
||||
message.format = Message.FORMAT_MATRIX_HTML;
|
||||
} else {
|
||||
Log.e(LOG_TAG, "Unsupported 'msgtype': " + msgType + ". Consider calling Room.canReplyTo(Event)");
|
||||
|
||||
// Ensure there will not be "m.relates_to" data in the sent event
|
||||
roomMediaMessage.setReplyToEvent(null);
|
||||
}
|
||||
} else {
|
||||
Log.e(LOG_TAG, "Null 'msgtype'. Consider calling Room.canReplyTo(Event)");
|
||||
|
||||
// Ensure there will not be "m.relates_to" data in the sent event
|
||||
roomMediaMessage.setReplyToEvent(null);
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private String includeReplyToToBody(Event replyToEvent,
|
||||
String replyToBody,
|
||||
boolean stripPreviousReplyTo,
|
||||
String messageBody,
|
||||
boolean isEmote) {
|
||||
int firstLineIndex = 0;
|
||||
|
||||
String[] lines = replyToBody.split("\n");
|
||||
|
||||
if (stripPreviousReplyTo) {
|
||||
// Strip replyToBody from previous reply to
|
||||
|
||||
// Strip line starting with "> "
|
||||
while (firstLineIndex < lines.length && lines[firstLineIndex].startsWith("> ")) {
|
||||
firstLineIndex++;
|
||||
}
|
||||
|
||||
// Strip empty line after
|
||||
if (firstLineIndex < lines.length && lines[firstLineIndex].isEmpty()) {
|
||||
firstLineIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder ret = new StringBuilder();
|
||||
|
||||
if (firstLineIndex < lines.length) {
|
||||
// Add <${mxid}> to the first line
|
||||
if (isEmote) {
|
||||
lines[firstLineIndex] = "* <" + replyToEvent.sender + "> " + lines[firstLineIndex];
|
||||
} else {
|
||||
lines[firstLineIndex] = "<" + replyToEvent.sender + "> " + lines[firstLineIndex];
|
||||
}
|
||||
|
||||
for (int i = firstLineIndex; i < lines.length; i++) {
|
||||
ret.append("> ")
|
||||
.append(lines[i])
|
||||
.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
ret.append("\n")
|
||||
.append(messageBody);
|
||||
|
||||
return ret.toString();
|
||||
}
|
||||
|
||||
private String includeReplyToToFormattedBody(Event replyToEvent,
|
||||
String replyToFormattedBody,
|
||||
boolean stripPreviousReplyTo,
|
||||
String messageFormattedBody,
|
||||
boolean isEmote) {
|
||||
if (stripPreviousReplyTo) {
|
||||
// Strip replyToFormattedBody from previous reply to
|
||||
replyToFormattedBody = replyToFormattedBody.replaceAll("^<mx-reply>.*</mx-reply>", "");
|
||||
}
|
||||
|
||||
StringBuilder ret = new StringBuilder("<mx-reply><blockquote><a href=\"")
|
||||
// ${evLink}
|
||||
.append(PermalinkUtils.createPermalink(replyToEvent))
|
||||
.append("\">")
|
||||
// "In reply to"
|
||||
.append(mContext.getString(R.string.message_reply_to_prefix))
|
||||
.append("</a> ");
|
||||
|
||||
if (isEmote) {
|
||||
ret.append("* ");
|
||||
}
|
||||
|
||||
ret.append("<a href=\"")
|
||||
// ${userLink}
|
||||
.append(PermalinkUtils.createPermalink(replyToEvent.sender))
|
||||
.append("\">")
|
||||
// ${mxid}
|
||||
.append(replyToEvent.sender)
|
||||
.append("</a><br>")
|
||||
.append(replyToFormattedBody)
|
||||
.append("</blockquote></mx-reply>")
|
||||
.append(messageFormattedBody);
|
||||
|
||||
return ret.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the thumbnail path of shot image.
|
||||
*
|
||||
* @param picturePath the image path
|
||||
* @return the thumbnail image path.
|
||||
*/
|
||||
private static String getThumbnailPath(String picturePath) {
|
||||
if (!TextUtils.isEmpty(picturePath) && picturePath.endsWith(".jpg")) {
|
||||
return picturePath.replace(".jpg", "_thumb.jpg");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the image thumbnail saved by the medias picker.
|
||||
*
|
||||
* @param sharedDataItem the sharedItem
|
||||
* @return the thumbnail if it exits.
|
||||
*/
|
||||
private Bitmap getMediasPickerThumbnail(RoomMediaMessage sharedDataItem) {
|
||||
Bitmap thumbnailBitmap = null;
|
||||
|
||||
try {
|
||||
String thumbPath = getThumbnailPath(sharedDataItem.getUri().getPath());
|
||||
|
||||
if (null != thumbPath) {
|
||||
File thumbFile = new File(thumbPath);
|
||||
|
||||
if (thumbFile.exists()) {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
|
||||
thumbnailBitmap = BitmapFactory.decodeFile(thumbPath, options);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "cannot restore the medias picker thumbnail " + e.getMessage(), e);
|
||||
} catch (OutOfMemoryError oom) {
|
||||
Log.e(LOG_TAG, "cannot restore the medias picker thumbnail oom", oom);
|
||||
}
|
||||
|
||||
return thumbnailBitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the media Url.
|
||||
*
|
||||
* @param roomMediaMessage the room media message
|
||||
* @return the media URL
|
||||
*/
|
||||
private String getMediaUrl(RoomMediaMessage roomMediaMessage) {
|
||||
String mediaUrl = roomMediaMessage.getUri().toString();
|
||||
|
||||
if (!mediaUrl.startsWith("file:")) {
|
||||
// save the content:// file in to the medias cache
|
||||
String mimeType = roomMediaMessage.getMimeType(mContext);
|
||||
ResourceUtils.Resource resource = ResourceUtils.openResource(mContext, roomMediaMessage.getUri(), mimeType);
|
||||
|
||||
// save the file in the filesystem
|
||||
mediaUrl = mDataHandler.getMediasCache().saveMedia(resource.mContentStream, null, mimeType);
|
||||
resource.close();
|
||||
}
|
||||
|
||||
return mediaUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an image message from a RoomMediaMessage.
|
||||
*
|
||||
* @param roomMediaMessage the roomMediaMessage
|
||||
* @return the image message
|
||||
*/
|
||||
private Message buildImageMessage(RoomMediaMessage roomMediaMessage) {
|
||||
try {
|
||||
String mimeType = roomMediaMessage.getMimeType(mContext);
|
||||
final MXMediasCache mediasCache = mDataHandler.getMediasCache();
|
||||
|
||||
String mediaUrl = getMediaUrl(roomMediaMessage);
|
||||
|
||||
// compute the thumbnail
|
||||
Bitmap thumbnailBitmap = roomMediaMessage.getFullScreenImageKindThumbnail(mContext);
|
||||
|
||||
if (null == thumbnailBitmap) {
|
||||
thumbnailBitmap = getMediasPickerThumbnail(roomMediaMessage);
|
||||
}
|
||||
|
||||
if (null == thumbnailBitmap) {
|
||||
Pair<Integer, Integer> thumbnailSize = roomMediaMessage.getThumbnailSize();
|
||||
thumbnailBitmap = ResourceUtils.createThumbnailBitmap(mContext, roomMediaMessage.getUri(), thumbnailSize.first, thumbnailSize.second);
|
||||
}
|
||||
|
||||
if (null == thumbnailBitmap) {
|
||||
thumbnailBitmap = roomMediaMessage.getMiniKindImageThumbnail(mContext);
|
||||
}
|
||||
|
||||
String thumbnailURL = null;
|
||||
|
||||
if (null != thumbnailBitmap) {
|
||||
thumbnailURL = mediasCache.saveBitmap(thumbnailBitmap, null);
|
||||
}
|
||||
|
||||
// get the exif rotation angle
|
||||
final int rotationAngle = ImageUtils.getRotationAngleForBitmap(mContext, Uri.parse(mediaUrl));
|
||||
|
||||
if (0 != rotationAngle) {
|
||||
// always apply the rotation to the image
|
||||
ImageUtils.rotateImage(mContext, thumbnailURL, rotationAngle, mediasCache);
|
||||
}
|
||||
|
||||
ImageMessage imageMessage = new ImageMessage();
|
||||
imageMessage.url = mediaUrl;
|
||||
imageMessage.body = roomMediaMessage.getFileName(mContext);
|
||||
|
||||
if (TextUtils.isEmpty(imageMessage.body)) {
|
||||
imageMessage.body = "Image";
|
||||
}
|
||||
|
||||
Uri imageUri = Uri.parse(mediaUrl);
|
||||
|
||||
if (null == imageMessage.info) {
|
||||
Room.fillImageInfo(mContext, imageMessage, imageUri, mimeType);
|
||||
}
|
||||
|
||||
if ((null != thumbnailURL) && (null != imageMessage.info) && (null == imageMessage.info.thumbnailInfo)) {
|
||||
Uri thumbUri = Uri.parse(thumbnailURL);
|
||||
Room.fillThumbnailInfo(mContext, imageMessage, thumbUri, "image/jpeg");
|
||||
imageMessage.info.thumbnailUrl = thumbnailURL;
|
||||
}
|
||||
|
||||
return imageMessage;
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## buildImageMessage() failed " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the video thumbnail
|
||||
*
|
||||
* @param videoUrl the video url
|
||||
* @return the video thumbnail
|
||||
*/
|
||||
public String getVideoThumbnailUrl(final String videoUrl) {
|
||||
String thumbUrl = null;
|
||||
try {
|
||||
Uri uri = Uri.parse(videoUrl);
|
||||
Bitmap thumb = ThumbnailUtils.createVideoThumbnail(uri.getPath(), MediaStore.Images.Thumbnails.MINI_KIND);
|
||||
thumbUrl = mDataHandler.getMediasCache().saveBitmap(thumb, null);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## getVideoThumbnailUrl() failed with " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
return thumbUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an video message from a RoomMediaMessage.
|
||||
*
|
||||
* @param roomMediaMessage the roomMediaMessage
|
||||
* @return the video message
|
||||
*/
|
||||
private Message buildVideoMessage(RoomMediaMessage roomMediaMessage) {
|
||||
try {
|
||||
String mediaUrl = getMediaUrl(roomMediaMessage);
|
||||
String thumbnailUrl = getVideoThumbnailUrl(mediaUrl);
|
||||
|
||||
if (null == thumbnailUrl) {
|
||||
return buildFileMessage(roomMediaMessage);
|
||||
}
|
||||
|
||||
VideoMessage videoMessage = new VideoMessage();
|
||||
videoMessage.url = mediaUrl;
|
||||
videoMessage.body = roomMediaMessage.getFileName(mContext);
|
||||
|
||||
Uri videoUri = Uri.parse(mediaUrl);
|
||||
Uri thumbnailUri = (null != thumbnailUrl) ? Uri.parse(thumbnailUrl) : null;
|
||||
Room.fillVideoInfo(mContext, videoMessage, videoUri, roomMediaMessage.getMimeType(mContext), thumbnailUri, "image/jpeg");
|
||||
|
||||
if (null == videoMessage.body) {
|
||||
videoMessage.body = videoUri.getLastPathSegment();
|
||||
}
|
||||
|
||||
return videoMessage;
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## buildVideoMessage() failed " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an file message from a RoomMediaMessage.
|
||||
*
|
||||
* @param roomMediaMessage the roomMediaMessage
|
||||
* @return the video message
|
||||
*/
|
||||
private Message buildFileMessage(RoomMediaMessage roomMediaMessage) {
|
||||
try {
|
||||
String mimeType = roomMediaMessage.getMimeType(mContext);
|
||||
|
||||
String mediaUrl = getMediaUrl(roomMediaMessage);
|
||||
FileMessage fileMessage;
|
||||
|
||||
if (mimeType.startsWith("audio/")) {
|
||||
fileMessage = new AudioMessage();
|
||||
} else {
|
||||
fileMessage = new FileMessage();
|
||||
}
|
||||
|
||||
fileMessage.url = mediaUrl;
|
||||
fileMessage.body = roomMediaMessage.getFileName(mContext);
|
||||
Uri uri = Uri.parse(mediaUrl);
|
||||
Room.fillFileInfo(mContext, fileMessage, uri, mimeType);
|
||||
|
||||
if (null == fileMessage.body) {
|
||||
fileMessage.body = uri.getLastPathSegment();
|
||||
}
|
||||
|
||||
return fileMessage;
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## buildFileMessage() failed " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// Upload medias management
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* Upload the medias.
|
||||
*
|
||||
* @param roomMediaMessage the roomMediaMessage
|
||||
* @return true if a media is uploaded
|
||||
*/
|
||||
private boolean uploadMedias(final RoomMediaMessage roomMediaMessage) {
|
||||
final Event event = roomMediaMessage.getEvent();
|
||||
final Message message = JsonUtils.toMessage(event.getContent());
|
||||
|
||||
if (!(message instanceof MediaMessage)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final MediaMessage mediaMessage = (MediaMessage) message;
|
||||
final String url;
|
||||
final String fMimeType;
|
||||
|
||||
if (mediaMessage.isThumbnailLocalContent()) {
|
||||
url = mediaMessage.getThumbnailUrl();
|
||||
fMimeType = "image/jpeg";
|
||||
} else if (mediaMessage.isLocalContent()) {
|
||||
url = mediaMessage.getUrl();
|
||||
fMimeType = mediaMessage.getMimeType();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
mEncodingHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final MXMediasCache mediasCache = mDataHandler.getMediasCache();
|
||||
|
||||
Uri uri = Uri.parse(url);
|
||||
String mimeType = fMimeType;
|
||||
final MXEncryptedAttachments.EncryptionResult encryptionResult;
|
||||
final Uri encryptedUri;
|
||||
InputStream stream;
|
||||
|
||||
String filename = null;
|
||||
|
||||
try {
|
||||
stream = new FileInputStream(new File(uri.getPath()));
|
||||
if (mRoom.isEncrypted() && mDataHandler.isCryptoEnabled() && (null != stream)) {
|
||||
encryptionResult = MXEncryptedAttachments.encryptAttachment(stream, mimeType);
|
||||
stream.close();
|
||||
|
||||
if (null != encryptionResult) {
|
||||
mimeType = "application/octet-stream";
|
||||
encryptedUri = Uri.parse(mediasCache.saveMedia(encryptionResult.mEncryptedStream, null, fMimeType));
|
||||
File file = new File(encryptedUri.getPath());
|
||||
stream = new FileInputStream(file);
|
||||
} else {
|
||||
skip();
|
||||
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mDataHandler.updateEventState(roomMediaMessage.getEvent(), Event.SentState.UNDELIVERED);
|
||||
mRoom.storeOutgoingEvent(roomMediaMessage.getEvent());
|
||||
mDataHandler.getStore().commit();
|
||||
|
||||
roomMediaMessage.onEncryptionFailed();
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Only pass filename string to server in non-encrypted rooms to prevent leaking filename
|
||||
filename = mediaMessage.isThumbnailLocalContent() ? ("thumb" + message.body) : message.body;
|
||||
encryptionResult = null;
|
||||
encryptedUri = null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
skip();
|
||||
return;
|
||||
}
|
||||
|
||||
mDataHandler.updateEventState(roomMediaMessage.getEvent(), Event.SentState.SENDING);
|
||||
|
||||
mediasCache.uploadContent(stream, filename, mimeType, url,
|
||||
new MXMediaUploadListener() {
|
||||
@Override
|
||||
public void onUploadStart(final String uploadId) {
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (null != roomMediaMessage.getMediaUploadListener()) {
|
||||
roomMediaMessage.getMediaUploadListener().onUploadStart(uploadId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUploadCancel(final String uploadId) {
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mDataHandler.updateEventState(roomMediaMessage.getEvent(), Event.SentState.UNDELIVERED);
|
||||
|
||||
if (null != roomMediaMessage.getMediaUploadListener()) {
|
||||
roomMediaMessage.getMediaUploadListener().onUploadCancel(uploadId);
|
||||
roomMediaMessage.setMediaUploadListener(null);
|
||||
roomMediaMessage.setEventSendingCallback(null);
|
||||
}
|
||||
|
||||
skip();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUploadError(final String uploadId, final int serverResponseCode, final String serverErrorMessage) {
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mDataHandler.updateEventState(roomMediaMessage.getEvent(), Event.SentState.UNDELIVERED);
|
||||
|
||||
if (null != roomMediaMessage.getMediaUploadListener()) {
|
||||
roomMediaMessage.getMediaUploadListener().onUploadError(uploadId, serverResponseCode, serverErrorMessage);
|
||||
roomMediaMessage.setMediaUploadListener(null);
|
||||
roomMediaMessage.setEventSendingCallback(null);
|
||||
}
|
||||
|
||||
skip();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUploadComplete(final String uploadId, final String contentUri) {
|
||||
mUiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
boolean isThumbnailUpload = mediaMessage.isThumbnailLocalContent();
|
||||
|
||||
if (isThumbnailUpload) {
|
||||
mediaMessage.setThumbnailUrl(encryptionResult, contentUri);
|
||||
|
||||
if (null != encryptionResult) {
|
||||
mediasCache.saveFileMediaForUrl(contentUri, encryptedUri.toString(), -1, -1, "image/jpeg");
|
||||
try {
|
||||
new File(Uri.parse(url).getPath()).delete();
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## cannot delete the uncompress media", e);
|
||||
}
|
||||
} else {
|
||||
Pair<Integer, Integer> thumbnailSize = roomMediaMessage.getThumbnailSize();
|
||||
mediasCache.saveFileMediaForUrl(contentUri, url, thumbnailSize.first, thumbnailSize.second, "image/jpeg");
|
||||
}
|
||||
|
||||
// update the event content with the new message info
|
||||
event.updateContent(JsonUtils.toJson(message));
|
||||
|
||||
// force to save the room events list
|
||||
// https://github.com/vector-im/riot-android/issues/1390
|
||||
mDataHandler.getStore().flushRoomEvents(mRoom.getRoomId());
|
||||
|
||||
// upload the media
|
||||
uploadMedias(roomMediaMessage);
|
||||
} else {
|
||||
if (null != encryptedUri) {
|
||||
// replace the thumbnail and the media contents by the computed one
|
||||
mediasCache.saveFileMediaForUrl(contentUri, encryptedUri.toString(), mediaMessage.getMimeType());
|
||||
try {
|
||||
new File(Uri.parse(url).getPath()).delete();
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## cannot delete the uncompress media", e);
|
||||
}
|
||||
} else {
|
||||
// replace the thumbnail and the media contents by the computed one
|
||||
mediasCache.saveFileMediaForUrl(contentUri, url, mediaMessage.getMimeType());
|
||||
}
|
||||
mediaMessage.setUrl(encryptionResult, contentUri);
|
||||
|
||||
// update the event content with the new message info
|
||||
event.updateContent(JsonUtils.toJson(message));
|
||||
|
||||
// force to save the room events list
|
||||
// https://github.com/vector-im/riot-android/issues/1390
|
||||
mDataHandler.getStore().flushRoomEvents(mRoom.getRoomId());
|
||||
|
||||
Log.d(LOG_TAG, "Uploaded to " + contentUri);
|
||||
|
||||
// send
|
||||
sendEvent(event);
|
||||
}
|
||||
|
||||
if (null != roomMediaMessage.getMediaUploadListener()) {
|
||||
roomMediaMessage.getMediaUploadListener().onUploadComplete(uploadId, contentUri);
|
||||
|
||||
if (!isThumbnailUpload) {
|
||||
roomMediaMessage.setMediaUploadListener(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,275 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Looper;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.MXSession;
|
||||
import im.vector.matrix.android.internal.legacy.data.timeline.EventTimeline;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.publicroom.PublicRoom;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.sync.RoomResponse;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* The `RoomEmailInvitation` gathers information for displaying the preview of a room that is unknown for the user.
|
||||
* Such room can come from an email invitation link or a link to a room.
|
||||
*/
|
||||
public class RoomPreviewData {
|
||||
|
||||
private static final String LOG_TAG = RoomPreviewData.class.getSimpleName();
|
||||
|
||||
// The id of the room to preview.
|
||||
private String mRoomId;
|
||||
|
||||
// the room Alias
|
||||
private String mRoomAlias;
|
||||
|
||||
// the id of the event to preview
|
||||
private String mEventId;
|
||||
|
||||
// In case of email invitation, the information extracted from the email invitation link.
|
||||
private RoomEmailInvitation mRoomEmailInvitation;
|
||||
|
||||
// preview information
|
||||
// comes from the email invitation or retrieve from an initialSync
|
||||
private String mRoomName;
|
||||
private String mRoomAvatarUrl;
|
||||
|
||||
// the room state
|
||||
private RoomState mRoomState;
|
||||
|
||||
// If the RoomState cannot be retrieved, this may contains some data
|
||||
private PublicRoom mPublicRoom;
|
||||
|
||||
// the initial sync data
|
||||
private RoomResponse mRoomResponse;
|
||||
|
||||
// the session
|
||||
private MXSession mSession;
|
||||
|
||||
/**
|
||||
* Create an RoomPreviewData instance
|
||||
*
|
||||
* @param session the session.
|
||||
* @param roomId the room Id to preview
|
||||
* @param eventId the event Id to preview (optional)
|
||||
* @param roomAlias the room alias (optional)
|
||||
* @param emailInvitationParams the email invitation parameters (optional)
|
||||
*/
|
||||
public RoomPreviewData(MXSession session, String roomId, String eventId, String roomAlias, Map<String, String> emailInvitationParams) {
|
||||
mSession = session;
|
||||
mRoomId = roomId;
|
||||
mRoomAlias = roomAlias;
|
||||
mEventId = eventId;
|
||||
|
||||
if (null != emailInvitationParams) {
|
||||
mRoomEmailInvitation = new RoomEmailInvitation(emailInvitationParams);
|
||||
mRoomName = mRoomEmailInvitation.roomName;
|
||||
mRoomAvatarUrl = mRoomEmailInvitation.roomAvatarUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the room state
|
||||
*/
|
||||
@Nullable
|
||||
public RoomState getRoomState() {
|
||||
return mRoomState;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the public room data
|
||||
*/
|
||||
@Nullable
|
||||
public PublicRoom getPublicRoom() {
|
||||
return mPublicRoom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the room state.
|
||||
*
|
||||
* @param roomState the new roomstate
|
||||
*/
|
||||
public void setRoomState(RoomState roomState) {
|
||||
mRoomState = roomState;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the room name
|
||||
*/
|
||||
public String getRoomName() {
|
||||
String roomName = mRoomName;
|
||||
|
||||
if (TextUtils.isEmpty(roomName)) {
|
||||
roomName = getRoomIdOrAlias();
|
||||
}
|
||||
|
||||
return roomName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the room name.
|
||||
*
|
||||
* @param aRoomName the new room name
|
||||
*/
|
||||
public void setRoomName(String aRoomName) {
|
||||
mRoomName = aRoomName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the room avatar URL
|
||||
*/
|
||||
public String getRoomAvatarUrl() {
|
||||
return mRoomAvatarUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the room id
|
||||
*/
|
||||
public String getRoomId() {
|
||||
return mRoomId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the room id or the alias (alias is preferred)
|
||||
*/
|
||||
public String getRoomIdOrAlias() {
|
||||
if (!TextUtils.isEmpty(mRoomAlias)) {
|
||||
return mRoomAlias;
|
||||
} else {
|
||||
return mRoomId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the event id.
|
||||
*/
|
||||
public String getEventId() {
|
||||
return mEventId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the session
|
||||
*/
|
||||
public MXSession getSession() {
|
||||
return mSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the initial sync response
|
||||
*/
|
||||
public RoomResponse getRoomResponse() {
|
||||
return mRoomResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the room invitation
|
||||
*/
|
||||
public RoomEmailInvitation getRoomEmailInvitation() {
|
||||
return mRoomEmailInvitation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to get more information from the homeserver about the room.
|
||||
*
|
||||
* @param apiCallback the callback when the operation is done.
|
||||
*/
|
||||
public void fetchPreviewData(final ApiCallback<Void> apiCallback) {
|
||||
mSession.getRoomsApiClient().initialSync(mRoomId, new ApiCallback<RoomResponse>() {
|
||||
@Override
|
||||
public void onSuccess(final RoomResponse roomResponse) {
|
||||
AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
// save the initial sync response
|
||||
mRoomResponse = roomResponse;
|
||||
|
||||
mRoomState = new RoomState();
|
||||
mRoomState.roomId = mRoomId;
|
||||
|
||||
for (Event event : roomResponse.state) {
|
||||
mRoomState.applyState(null, event, EventTimeline.Direction.FORWARDS);
|
||||
}
|
||||
|
||||
// TODO LazyLoading handle case where room has no name
|
||||
mRoomName = mRoomState.name;
|
||||
mRoomAvatarUrl = mRoomState.getAvatarUrl();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void args) {
|
||||
apiCallback.onSuccess(null);
|
||||
}
|
||||
};
|
||||
try {
|
||||
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
} catch (final Exception e) {
|
||||
Log.e(LOG_TAG, "## fetchPreviewData() failed " + e.getMessage(), e);
|
||||
task.cancel(true);
|
||||
|
||||
(new android.os.Handler(Looper.getMainLooper())).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (null != apiCallback) {
|
||||
apiCallback.onUnexpectedError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
mRoomState = new RoomState();
|
||||
mRoomState.roomId = mRoomId;
|
||||
apiCallback.onNetworkError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
mRoomState = new RoomState();
|
||||
mRoomState.roomId = mRoomId;
|
||||
apiCallback.onMatrixError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
mRoomState = new RoomState();
|
||||
mRoomState.roomId = mRoomId;
|
||||
apiCallback.onUnexpectedError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Public RoomData, In case RoomState cannot be retrieved
|
||||
*
|
||||
* @param publicRoom
|
||||
*/
|
||||
public void setPublicRoom(PublicRoom publicRoom) {
|
||||
mPublicRoom = publicRoom;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,593 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.call.MXCallsManager;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.EventContent;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.RoomMember;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.message.Message;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.sync.RoomSyncSummary;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Stores summarised information about the room.
|
||||
*/
|
||||
public class RoomSummary implements java.io.Serializable {
|
||||
private static final String LOG_TAG = RoomSummary.class.getSimpleName();
|
||||
|
||||
private static final long serialVersionUID = -3683013938626566489L;
|
||||
|
||||
// list of supported types
|
||||
private static final List<String> sSupportedType = Arrays.asList(
|
||||
Event.EVENT_TYPE_STATE_ROOM_TOPIC,
|
||||
Event.EVENT_TYPE_MESSAGE_ENCRYPTED,
|
||||
Event.EVENT_TYPE_MESSAGE_ENCRYPTION,
|
||||
Event.EVENT_TYPE_STATE_ROOM_NAME,
|
||||
Event.EVENT_TYPE_STATE_ROOM_MEMBER,
|
||||
Event.EVENT_TYPE_STATE_ROOM_CREATE,
|
||||
Event.EVENT_TYPE_STATE_HISTORY_VISIBILITY,
|
||||
Event.EVENT_TYPE_STATE_ROOM_THIRD_PARTY_INVITE,
|
||||
Event.EVENT_TYPE_STICKER);
|
||||
|
||||
// List of known unsupported types
|
||||
private static final List<String> sKnownUnsupportedType = Arrays.asList(
|
||||
Event.EVENT_TYPE_TYPING,
|
||||
Event.EVENT_TYPE_STATE_ROOM_POWER_LEVELS,
|
||||
Event.EVENT_TYPE_STATE_ROOM_JOIN_RULES,
|
||||
Event.EVENT_TYPE_STATE_CANONICAL_ALIAS,
|
||||
Event.EVENT_TYPE_STATE_ROOM_ALIASES,
|
||||
Event.EVENT_TYPE_URL_PREVIEW,
|
||||
Event.EVENT_TYPE_STATE_RELATED_GROUPS,
|
||||
Event.EVENT_TYPE_STATE_ROOM_GUEST_ACCESS,
|
||||
Event.EVENT_TYPE_REDACTION);
|
||||
|
||||
private String mRoomId = null;
|
||||
private String mTopic = null;
|
||||
private Event mLatestReceivedEvent = null;
|
||||
|
||||
// the room state is only used to check
|
||||
// 1- the invitation status
|
||||
// 2- the members display name
|
||||
private transient RoomState mLatestRoomState = null;
|
||||
|
||||
// defines the latest read message
|
||||
private String mReadReceiptEventId;
|
||||
|
||||
// the read marker event id
|
||||
private String mReadMarkerEventId;
|
||||
|
||||
private Set<String> mRoomTags;
|
||||
|
||||
// counters
|
||||
public int mUnreadEventsCount;
|
||||
public int mNotificationCount;
|
||||
public int mHighlightsCount;
|
||||
|
||||
// invitation status
|
||||
// retrieved at initial sync
|
||||
// the roomstate is not always known
|
||||
private String mInviterUserId = null;
|
||||
|
||||
// retrieved from the roomState
|
||||
private String mInviterName = null;
|
||||
|
||||
private String mUserId = null;
|
||||
|
||||
// Info from sync, depending on the room position in the sync
|
||||
private String mUserMembership;
|
||||
|
||||
/**
|
||||
* Tell if the room is a user conference user one
|
||||
*/
|
||||
private Boolean mIsConferenceUserRoom = null;
|
||||
|
||||
/**
|
||||
* Data from RoomSyncSummary
|
||||
*/
|
||||
private List<String> mHeroes = new ArrayList<>();
|
||||
|
||||
private int mJoinedMembersCountFromSyncRoomSummary;
|
||||
|
||||
private int mInvitedMembersCountFromSyncRoomSummary;
|
||||
|
||||
public RoomSummary() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a room summary
|
||||
*
|
||||
* @param fromSummary the summary source
|
||||
* @param event the latest event of the room
|
||||
* @param roomState the room state - used to display the event
|
||||
* @param userId our own user id - used to display the room name
|
||||
*/
|
||||
public RoomSummary(@Nullable RoomSummary fromSummary,
|
||||
Event event,
|
||||
RoomState roomState,
|
||||
String userId) {
|
||||
mUserId = userId;
|
||||
|
||||
if (null != roomState) {
|
||||
setRoomId(roomState.roomId);
|
||||
}
|
||||
|
||||
if ((null == getRoomId()) && (null != event)) {
|
||||
setRoomId(event.roomId);
|
||||
}
|
||||
|
||||
setLatestReceivedEvent(event, roomState);
|
||||
|
||||
// if no summary is provided
|
||||
if (null == fromSummary) {
|
||||
if (null != event) {
|
||||
setReadMarkerEventId(event.eventId);
|
||||
setReadReceiptEventId(event.eventId);
|
||||
}
|
||||
|
||||
if (null != roomState) {
|
||||
setHighlightCount(roomState.getHighlightCount());
|
||||
setNotificationCount(roomState.getHighlightCount());
|
||||
}
|
||||
setUnreadEventsCount(Math.max(getHighlightCount(), getNotificationCount()));
|
||||
} else {
|
||||
// else use the provided summary data
|
||||
setReadMarkerEventId(fromSummary.getReadMarkerEventId());
|
||||
setReadReceiptEventId(fromSummary.getReadReceiptEventId());
|
||||
setUnreadEventsCount(fromSummary.getUnreadEventsCount());
|
||||
setHighlightCount(fromSummary.getHighlightCount());
|
||||
setNotificationCount(fromSummary.getNotificationCount());
|
||||
|
||||
mHeroes.addAll(fromSummary.mHeroes);
|
||||
mJoinedMembersCountFromSyncRoomSummary = fromSummary.mJoinedMembersCountFromSyncRoomSummary;
|
||||
mInvitedMembersCountFromSyncRoomSummary = fromSummary.mInvitedMembersCountFromSyncRoomSummary;
|
||||
|
||||
mUserMembership = fromSummary.mUserMembership;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the event can be summarized.
|
||||
* Some event types are not yet supported.
|
||||
*
|
||||
* @param event the event to test.
|
||||
* @return true if the event can be summarized
|
||||
*/
|
||||
public static boolean isSupportedEvent(Event event) {
|
||||
String type = event.getType();
|
||||
boolean isSupported = false;
|
||||
|
||||
// check if the msgtype is supported
|
||||
if (TextUtils.equals(Event.EVENT_TYPE_MESSAGE, type)) {
|
||||
try {
|
||||
JsonObject eventContent = event.getContentAsJsonObject();
|
||||
String msgType = "";
|
||||
|
||||
JsonElement element = eventContent.get("msgtype");
|
||||
|
||||
if (null != element) {
|
||||
msgType = element.getAsString();
|
||||
}
|
||||
|
||||
isSupported = TextUtils.equals(msgType, Message.MSGTYPE_TEXT)
|
||||
|| TextUtils.equals(msgType, Message.MSGTYPE_EMOTE)
|
||||
|| TextUtils.equals(msgType, Message.MSGTYPE_NOTICE)
|
||||
|| TextUtils.equals(msgType, Message.MSGTYPE_IMAGE)
|
||||
|| TextUtils.equals(msgType, Message.MSGTYPE_AUDIO)
|
||||
|| TextUtils.equals(msgType, Message.MSGTYPE_VIDEO)
|
||||
|| TextUtils.equals(msgType, Message.MSGTYPE_FILE);
|
||||
|
||||
if (!isSupported && !TextUtils.isEmpty(msgType)) {
|
||||
Log.e(LOG_TAG, "isSupportedEvent : Unsupported msg type " + msgType);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "isSupportedEvent failed " + e.getMessage(), e);
|
||||
}
|
||||
} else if (TextUtils.equals(Event.EVENT_TYPE_MESSAGE_ENCRYPTED, type)) {
|
||||
isSupported = event.hasContentFields();
|
||||
} else if (TextUtils.equals(Event.EVENT_TYPE_STATE_ROOM_MEMBER, type)) {
|
||||
JsonObject eventContentAsJsonObject = event.getContentAsJsonObject();
|
||||
|
||||
if (null != eventContentAsJsonObject) {
|
||||
if (eventContentAsJsonObject.entrySet().isEmpty()) {
|
||||
Log.d(LOG_TAG, "isSupportedEvent : room member with no content is not supported");
|
||||
} else {
|
||||
// do not display the avatar / display name update
|
||||
EventContent prevEventContent = event.getPrevContent();
|
||||
EventContent eventContent = event.getEventContent();
|
||||
|
||||
String membership = null;
|
||||
String preMembership = null;
|
||||
|
||||
if (eventContent != null) {
|
||||
membership = eventContent.membership;
|
||||
}
|
||||
|
||||
if (prevEventContent != null) {
|
||||
preMembership = prevEventContent.membership;
|
||||
}
|
||||
|
||||
isSupported = !TextUtils.equals(membership, preMembership);
|
||||
|
||||
if (!isSupported) {
|
||||
Log.d(LOG_TAG, "isSupportedEvent : do not support avatar display name update");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
isSupported = sSupportedType.contains(type)
|
||||
|| (event.isCallEvent() && !TextUtils.isEmpty(type) && !Event.EVENT_TYPE_CALL_CANDIDATES.equals(type));
|
||||
}
|
||||
|
||||
if (!isSupported) {
|
||||
// some events are known to be never traced
|
||||
// avoid warning when it is not required.
|
||||
if (!sKnownUnsupportedType.contains(type)) {
|
||||
Log.e(LOG_TAG, "isSupportedEvent : Unsupported event type " + type);
|
||||
}
|
||||
}
|
||||
|
||||
return isSupported;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the user id
|
||||
*/
|
||||
public String getUserId() {
|
||||
return mUserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the room id
|
||||
*/
|
||||
public String getRoomId() {
|
||||
return mRoomId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the topic.
|
||||
*/
|
||||
public String getRoomTopic() {
|
||||
return mTopic;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the room summary event.
|
||||
*/
|
||||
public Event getLatestReceivedEvent() {
|
||||
return mLatestReceivedEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the dedicated room state.
|
||||
*/
|
||||
public RoomState getLatestRoomState() {
|
||||
return mLatestRoomState;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the current user is invited
|
||||
*/
|
||||
public boolean isInvited() {
|
||||
return RoomMember.MEMBERSHIP_INVITE.equals(mUserMembership);
|
||||
}
|
||||
|
||||
/**
|
||||
* To call when the room is in the invited section of the sync response
|
||||
*/
|
||||
public void setIsInvited() {
|
||||
mUserMembership = RoomMember.MEMBERSHIP_INVITE;
|
||||
}
|
||||
|
||||
/**
|
||||
* To call when the room is in the joined section of the sync response
|
||||
*/
|
||||
public void setIsJoined() {
|
||||
mUserMembership = RoomMember.MEMBERSHIP_JOIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the current user is invited
|
||||
*/
|
||||
public boolean isJoined() {
|
||||
return RoomMember.MEMBERSHIP_JOIN.equals(mUserMembership);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the inviter user id.
|
||||
*/
|
||||
public String getInviterUserId() {
|
||||
return mInviterUserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the room's {@link org.matrix.androidsdk.rest.model.Event#EVENT_TYPE_STATE_ROOM_TOPIC}.
|
||||
*
|
||||
* @param topic The topic
|
||||
* @return This summary for chaining calls.
|
||||
*/
|
||||
public RoomSummary setTopic(String topic) {
|
||||
mTopic = topic;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the room's ID..
|
||||
*
|
||||
* @param roomId The room ID
|
||||
* @return This summary for chaining calls.
|
||||
*/
|
||||
public RoomSummary setRoomId(String roomId) {
|
||||
mRoomId = roomId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the latest tracked event (e.g. the latest m.room.message)
|
||||
*
|
||||
* @param event The most-recent event.
|
||||
* @param roomState The room state
|
||||
* @return This summary for chaining calls.
|
||||
*/
|
||||
public RoomSummary setLatestReceivedEvent(Event event, RoomState roomState) {
|
||||
setLatestReceivedEvent(event);
|
||||
setLatestRoomState(roomState);
|
||||
|
||||
if (null != roomState) {
|
||||
setTopic(roomState.topic);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the latest tracked event (e.g. the latest m.room.message)
|
||||
*
|
||||
* @param event The most-recent event.
|
||||
* @return This summary for chaining calls.
|
||||
*/
|
||||
public RoomSummary setLatestReceivedEvent(Event event) {
|
||||
mLatestReceivedEvent = event;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the latest RoomState
|
||||
*
|
||||
* @param roomState The room state of the latest event.
|
||||
* @return This summary for chaining calls.
|
||||
*/
|
||||
public RoomSummary setLatestRoomState(RoomState roomState) {
|
||||
mLatestRoomState = roomState;
|
||||
|
||||
// Keep this code for compatibility?
|
||||
boolean isInvited = false;
|
||||
|
||||
// check for the invitation status
|
||||
if (null != mLatestRoomState) {
|
||||
RoomMember member = mLatestRoomState.getMember(mUserId);
|
||||
isInvited = (null != member) && RoomMember.MEMBERSHIP_INVITE.equals(member.membership);
|
||||
}
|
||||
// when invited, the only received message should be the invitation one
|
||||
if (isInvited) {
|
||||
mInviterName = null;
|
||||
|
||||
if (null != mLatestReceivedEvent) {
|
||||
mInviterName = mInviterUserId = mLatestReceivedEvent.getSender();
|
||||
|
||||
// try to retrieve a display name
|
||||
if (null != mLatestRoomState) {
|
||||
mInviterName = mLatestRoomState.getMemberName(mLatestReceivedEvent.getSender());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mInviterUserId = mInviterName = null;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the read receipt event Id
|
||||
*
|
||||
* @param eventId the read receipt event id.
|
||||
*/
|
||||
public void setReadReceiptEventId(String eventId) {
|
||||
Log.d(LOG_TAG, "## setReadReceiptEventId() : " + eventId + " roomId " + getRoomId());
|
||||
mReadReceiptEventId = eventId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the read receipt event id
|
||||
*/
|
||||
public String getReadReceiptEventId() {
|
||||
return mReadReceiptEventId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the read marker event Id
|
||||
*
|
||||
* @param eventId the read marker event id.
|
||||
*/
|
||||
public void setReadMarkerEventId(String eventId) {
|
||||
Log.d(LOG_TAG, "## setReadMarkerEventId() : " + eventId + " roomId " + getRoomId());
|
||||
|
||||
if (TextUtils.isEmpty(eventId)) {
|
||||
Log.e(LOG_TAG, "## setReadMarkerEventId') : null mReadMarkerEventId, in " + getRoomId());
|
||||
}
|
||||
|
||||
mReadMarkerEventId = eventId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the read receipt event id
|
||||
*/
|
||||
public String getReadMarkerEventId() {
|
||||
if (TextUtils.isEmpty(mReadMarkerEventId)) {
|
||||
Log.e(LOG_TAG, "## getReadMarkerEventId') : null mReadMarkerEventId, in " + getRoomId());
|
||||
mReadMarkerEventId = getReadReceiptEventId();
|
||||
}
|
||||
|
||||
return mReadMarkerEventId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the unread message counter
|
||||
*
|
||||
* @param count the unread events count.
|
||||
*/
|
||||
public void setUnreadEventsCount(int count) {
|
||||
Log.d(LOG_TAG, "## setUnreadEventsCount() : " + count + " roomId " + getRoomId());
|
||||
mUnreadEventsCount = count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the unread events count
|
||||
*/
|
||||
public int getUnreadEventsCount() {
|
||||
return mUnreadEventsCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the notification counter
|
||||
*
|
||||
* @param count the notification counter
|
||||
*/
|
||||
public void setNotificationCount(int count) {
|
||||
Log.d(LOG_TAG, "## setNotificationCount() : " + count + " roomId " + getRoomId());
|
||||
mNotificationCount = count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the notification count
|
||||
*/
|
||||
public int getNotificationCount() {
|
||||
return mNotificationCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the highlight counter
|
||||
*
|
||||
* @param count the highlight counter
|
||||
*/
|
||||
public void setHighlightCount(int count) {
|
||||
Log.d(LOG_TAG, "## setHighlightCount() : " + count + " roomId " + getRoomId());
|
||||
mHighlightsCount = count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the highlight count
|
||||
*/
|
||||
public int getHighlightCount() {
|
||||
return mHighlightsCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the room tags
|
||||
*/
|
||||
public Set<String> getRoomTags() {
|
||||
return mRoomTags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the room tags
|
||||
*
|
||||
* @param roomTags the room tags
|
||||
*/
|
||||
public void setRoomTags(final Set<String> roomTags) {
|
||||
if (roomTags != null) {
|
||||
// wraps the set into a serializable one
|
||||
mRoomTags = new HashSet<>(roomTags);
|
||||
} else {
|
||||
mRoomTags = new HashSet<>();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isConferenceUserRoom() {
|
||||
// test if it is not yet initialized
|
||||
if (null == mIsConferenceUserRoom) {
|
||||
|
||||
mIsConferenceUserRoom = false;
|
||||
|
||||
// FIXME LazyLoading Heroes does not contains me
|
||||
// FIXME I'ms not sure this code will work anymore
|
||||
|
||||
Collection<String> membersId = getHeroes();
|
||||
|
||||
// works only with 1:1 room
|
||||
if (2 == membersId.size()) {
|
||||
for (String userId : membersId) {
|
||||
if (MXCallsManager.isConferenceUserId(userId)) {
|
||||
mIsConferenceUserRoom = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mIsConferenceUserRoom;
|
||||
}
|
||||
|
||||
public void setIsConferenceUserRoom(boolean isConferenceUserRoom) {
|
||||
mIsConferenceUserRoom = isConferenceUserRoom;
|
||||
}
|
||||
|
||||
public void setRoomSyncSummary(@NonNull RoomSyncSummary roomSyncSummary) {
|
||||
if (roomSyncSummary.heroes != null) {
|
||||
mHeroes.clear();
|
||||
mHeroes.addAll(roomSyncSummary.heroes);
|
||||
}
|
||||
|
||||
if (roomSyncSummary.joinedMembersCount != null) {
|
||||
// Update the value
|
||||
mJoinedMembersCountFromSyncRoomSummary = roomSyncSummary.joinedMembersCount;
|
||||
}
|
||||
|
||||
if (roomSyncSummary.invitedMembersCount != null) {
|
||||
// Update the value
|
||||
mInvitedMembersCountFromSyncRoomSummary = roomSyncSummary.invitedMembersCount;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<String> getHeroes() {
|
||||
return mHeroes;
|
||||
}
|
||||
|
||||
public int getNumberOfJoinedMembers() {
|
||||
return mJoinedMembersCountFromSyncRoomSummary;
|
||||
}
|
||||
|
||||
public int getNumberOfInvitedMembers() {
|
||||
return mInvitedMembersCountFromSyncRoomSummary;
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.RoomTags;
|
||||
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Class representing a room tag.
|
||||
*/
|
||||
public class RoomTag implements java.io.Serializable {
|
||||
private static final long serialVersionUID = 5172602958896551204L;
|
||||
private static final String LOG_TAG = RoomTag.class.getSimpleName();
|
||||
|
||||
//
|
||||
public static final String ROOM_TAG_FAVOURITE = "m.favourite";
|
||||
public static final String ROOM_TAG_LOW_PRIORITY = "m.lowpriority";
|
||||
public static final String ROOM_TAG_NO_TAG = "m.recent";
|
||||
public static final String ROOM_TAG_SERVER_NOTICE = "m.server_notice";
|
||||
|
||||
/**
|
||||
* The name of a tag.
|
||||
*/
|
||||
public String mName;
|
||||
|
||||
/**
|
||||
* Try to parse order as Double.
|
||||
* Provides nil if the items cannot be parsed.
|
||||
*/
|
||||
public Double mOrder;
|
||||
|
||||
/**
|
||||
* RoomTag creator.
|
||||
*
|
||||
* @param aName the tag name.
|
||||
* @param anOrder the tag order
|
||||
*/
|
||||
public RoomTag(String aName, Double anOrder) {
|
||||
mName = aName;
|
||||
mOrder = anOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a list of tags from a room tag event.
|
||||
*
|
||||
* @param event a room tag event (which can contains several tags)
|
||||
* @return a dictionary containing the tags the user defined for one room.
|
||||
*/
|
||||
public static Map<String, RoomTag> roomTagsWithTagEvent(Event event) {
|
||||
Map<String, RoomTag> tags = new HashMap<>();
|
||||
|
||||
try {
|
||||
RoomTags roomtags = JsonUtils.toRoomTags(event.getContent());
|
||||
|
||||
if ((null != roomtags.tags) && (0 != roomtags.tags.size())) {
|
||||
for (String tagName : roomtags.tags.keySet()) {
|
||||
Map<String, Double> params = roomtags.tags.get(tagName);
|
||||
if (params != null) {
|
||||
tags.put(tagName, new RoomTag(tagName, params.get("order")));
|
||||
} else {
|
||||
tags.put(tagName, new RoomTag(tagName, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "roomTagsWithTagEvent fails " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data.comparator;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.interfaces.DatedObject;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
public class Comparators {
|
||||
|
||||
// comparator to sort from the oldest to the latest.
|
||||
public static final Comparator<DatedObject> ascComparator = new Comparator<DatedObject>() {
|
||||
@Override
|
||||
public int compare(DatedObject datedObject1, DatedObject datedObject2) {
|
||||
return (int) (datedObject1.getDate() - datedObject2.getDate());
|
||||
}
|
||||
};
|
||||
|
||||
// comparator to sort from the latest to the oldest.
|
||||
public static final Comparator<DatedObject> descComparator = new Comparator<DatedObject>() {
|
||||
@Override
|
||||
public int compare(DatedObject datedObject1, DatedObject datedObject2) {
|
||||
return (int) (datedObject2.getDate() - datedObject1.getDate());
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data.comparator;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.data.Room;
|
||||
import im.vector.matrix.android.internal.legacy.data.RoomTag;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* This class is responsible for comparing rooms by the tag's order
|
||||
*/
|
||||
public class RoomComparatorWithTag implements Comparator<Room> {
|
||||
|
||||
private final String mTag;
|
||||
|
||||
public RoomComparatorWithTag(final String tag) {
|
||||
mTag = tag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(final Room r1, final Room r2) {
|
||||
final int res;
|
||||
final RoomTag tag1 = r1.getAccountData().roomTag(mTag);
|
||||
final RoomTag tag2 = r2.getAccountData().roomTag(mTag);
|
||||
|
||||
if (tag1 != null && tag1.mOrder != null && tag2 != null && tag2.mOrder != null) {
|
||||
res = Double.compare(tag1.mOrder, tag2.mOrder);
|
||||
} else if (tag1 != null && tag1.mOrder != null) {
|
||||
res = 1;
|
||||
} else {
|
||||
res = -1;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
@ -0,0 +1,310 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data.cryptostore;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.crypto.IncomingRoomKeyRequest;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.OutgoingRoomKeyRequest;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXDeviceInfo;
|
||||
import im.vector.matrix.android.internal.legacy.crypto.data.MXOlmInboundGroupSession2;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.login.Credentials;
|
||||
import org.matrix.olm.OlmAccount;
|
||||
import org.matrix.olm.OlmSession;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* the crypto data store
|
||||
*/
|
||||
public interface IMXCryptoStore {
|
||||
/**
|
||||
* Init a crypto store for the passed credentials.
|
||||
*
|
||||
* @param context the application context
|
||||
* @param credentials the credentials of the account.
|
||||
*/
|
||||
void initWithCredentials(Context context, Credentials credentials);
|
||||
|
||||
/**
|
||||
* @return if the corrupted is corrupted.
|
||||
*/
|
||||
boolean isCorrupted();
|
||||
|
||||
/**
|
||||
* Indicate if the store contains data for the passed account.
|
||||
*
|
||||
* @return true means that the user enabled the crypto in a previous session
|
||||
*/
|
||||
boolean hasData();
|
||||
|
||||
/**
|
||||
* Delete the crypto store for the passed credentials.
|
||||
*/
|
||||
void deleteStore();
|
||||
|
||||
/**
|
||||
* open any existing crypto store
|
||||
*/
|
||||
void open();
|
||||
|
||||
/**
|
||||
* Close the store
|
||||
*/
|
||||
void close();
|
||||
|
||||
/**
|
||||
* Store the device id.
|
||||
*
|
||||
* @param deviceId the device id
|
||||
*/
|
||||
void storeDeviceId(String deviceId);
|
||||
|
||||
/**
|
||||
* @return the device id
|
||||
*/
|
||||
String getDeviceId();
|
||||
|
||||
/**
|
||||
* Store the end to end account for the logged-in user.
|
||||
*
|
||||
* @param account the account to save
|
||||
*/
|
||||
void storeAccount(OlmAccount account);
|
||||
|
||||
/**
|
||||
* @return the olm account
|
||||
*/
|
||||
OlmAccount getAccount();
|
||||
|
||||
/**
|
||||
* Store a device for a user.
|
||||
*
|
||||
* @param userId The user's id.
|
||||
* @param device the device to store.
|
||||
*/
|
||||
void storeUserDevice(String userId, MXDeviceInfo device);
|
||||
|
||||
/**
|
||||
* Retrieve a device for a user.
|
||||
*
|
||||
* @param deviceId The device id.
|
||||
* @param userId The user's id.
|
||||
* @return A map from device id to 'MXDevice' object for the device.
|
||||
*/
|
||||
MXDeviceInfo getUserDevice(String deviceId, String userId);
|
||||
|
||||
/**
|
||||
* Store the known devices for a user.
|
||||
*
|
||||
* @param userId The user's id.
|
||||
* @param devices A map from device id to 'MXDevice' object for the device.
|
||||
*/
|
||||
void storeUserDevices(String userId, Map<String, MXDeviceInfo> devices);
|
||||
|
||||
/**
|
||||
* Retrieve the known devices for a user.
|
||||
*
|
||||
* @param userId The user's id.
|
||||
* @return The devices map if some devices are known, else null
|
||||
*/
|
||||
Map<String, MXDeviceInfo> getUserDevices(String userId);
|
||||
|
||||
/**
|
||||
* Store the crypto algorithm for a room.
|
||||
*
|
||||
* @param roomId the id of the room.
|
||||
* @param algorithm the algorithm.
|
||||
*/
|
||||
void storeRoomAlgorithm(String roomId, String algorithm);
|
||||
|
||||
/**
|
||||
* Provides the algorithm used in a dedicated room.
|
||||
*
|
||||
* @param roomId the room id
|
||||
* @return the algorithm, null is the room is not encrypted
|
||||
*/
|
||||
String getRoomAlgorithm(String roomId);
|
||||
|
||||
/**
|
||||
* Store a session between the logged-in user and another device.
|
||||
*
|
||||
* @param session the end-to-end session.
|
||||
* @param deviceKey the public key of the other device.
|
||||
*/
|
||||
void storeSession(OlmSession session, String deviceKey);
|
||||
|
||||
/**
|
||||
* Retrieve the end-to-end sessions between the logged-in user and another
|
||||
* device.
|
||||
*
|
||||
* @param deviceKey the public key of the other device.
|
||||
* @return A map from sessionId to Base64 end-to-end session.
|
||||
*/
|
||||
Map<String, OlmSession> getDeviceSessions(String deviceKey);
|
||||
|
||||
/**
|
||||
* Store an inbound group session.
|
||||
*
|
||||
* @param session the inbound group session and its context.
|
||||
*/
|
||||
void storeInboundGroupSession(MXOlmInboundGroupSession2 session);
|
||||
|
||||
/**
|
||||
* Retrieve an inbound group session.
|
||||
*
|
||||
* @param sessionId the session identifier.
|
||||
* @param senderKey the base64-encoded curve25519 key of the sender.
|
||||
* @return an inbound group session.
|
||||
*/
|
||||
MXOlmInboundGroupSession2 getInboundGroupSession(String sessionId, String senderKey);
|
||||
|
||||
/**
|
||||
* Retrieve the known inbound group sessions.
|
||||
*
|
||||
* @return an inbound group session.
|
||||
*/
|
||||
List<MXOlmInboundGroupSession2> getInboundGroupSessions();
|
||||
|
||||
/**
|
||||
* Remove an inbound group session
|
||||
*
|
||||
* @param sessionId the session identifier.
|
||||
* @param senderKey the base64-encoded curve25519 key of the sender.
|
||||
*/
|
||||
void removeInboundGroupSession(String sessionId, String senderKey);
|
||||
|
||||
/**
|
||||
* Set the global override for whether the client should ever send encrypted
|
||||
* messages to unverified devices.
|
||||
* If false, it can still be overridden per-room.
|
||||
* If true, it overrides the per-room settings.
|
||||
*
|
||||
* @param block true to unilaterally blacklist all
|
||||
*/
|
||||
void setGlobalBlacklistUnverifiedDevices(boolean block);
|
||||
|
||||
/**
|
||||
* @return true to unilaterally blacklist all unverified devices.
|
||||
*/
|
||||
boolean getGlobalBlacklistUnverifiedDevices();
|
||||
|
||||
/**
|
||||
* Updates the rooms ids list in which the messages are not encrypted for the unverified devices.
|
||||
*
|
||||
* @param roomIds the room ids list
|
||||
*/
|
||||
void setRoomsListBlacklistUnverifiedDevices(List<String> roomIds);
|
||||
|
||||
/**
|
||||
* Provides the rooms ids list in which the messages are not encrypted for the unverified devices.
|
||||
*
|
||||
* @return the room Ids list
|
||||
*/
|
||||
List<String> getRoomsListBlacklistUnverifiedDevices();
|
||||
|
||||
/**
|
||||
* @return the devices statuses map
|
||||
*/
|
||||
Map<String, Integer> getDeviceTrackingStatuses();
|
||||
|
||||
/**
|
||||
* Save the device statuses
|
||||
*
|
||||
* @param deviceTrackingStatuses the device tracking statuses
|
||||
*/
|
||||
void saveDeviceTrackingStatuses(Map<String, Integer> deviceTrackingStatuses);
|
||||
|
||||
/**
|
||||
* Get the tracking status of a specified userId devices.
|
||||
*
|
||||
* @param userId the user id
|
||||
* @param defaultValue the default avlue
|
||||
* @return the tracking status
|
||||
*/
|
||||
int getDeviceTrackingStatus(String userId, int defaultValue);
|
||||
|
||||
/**
|
||||
* Look for an existing outgoing room key request, and if none is found,
|
||||
*
|
||||
* @param requestBody the request body
|
||||
* @return an OutgoingRoomKeyRequest instance or null
|
||||
*/
|
||||
OutgoingRoomKeyRequest getOutgoingRoomKeyRequest(Map<String, String> requestBody);
|
||||
|
||||
/**
|
||||
* Look for an existing outgoing room key request, and if none is found,
|
||||
* + add a new one.
|
||||
*
|
||||
* @param request the request
|
||||
* @return either the same instance as passed in, or the existing one.
|
||||
*/
|
||||
OutgoingRoomKeyRequest getOrAddOutgoingRoomKeyRequest(OutgoingRoomKeyRequest request);
|
||||
|
||||
/**
|
||||
* Look for room key requests by state.
|
||||
*
|
||||
* @param states the states
|
||||
* @return an OutgoingRoomKeyRequest or null
|
||||
*/
|
||||
OutgoingRoomKeyRequest getOutgoingRoomKeyRequestByState(Set<OutgoingRoomKeyRequest.RequestState> states);
|
||||
|
||||
/**
|
||||
* Update an existing outgoing request.
|
||||
*
|
||||
* @param request the request
|
||||
*/
|
||||
void updateOutgoingRoomKeyRequest(OutgoingRoomKeyRequest request);
|
||||
|
||||
/**
|
||||
* Delete an outgoing room key request.
|
||||
*
|
||||
* @param transactionId the transaction id.
|
||||
*/
|
||||
void deleteOutgoingRoomKeyRequest(String transactionId);
|
||||
|
||||
/**
|
||||
* Store an incomingRoomKeyRequest instance
|
||||
*
|
||||
* @param incomingRoomKeyRequest the incoming key request
|
||||
*/
|
||||
void storeIncomingRoomKeyRequest(IncomingRoomKeyRequest incomingRoomKeyRequest);
|
||||
|
||||
/**
|
||||
* Delete an incomingRoomKeyRequest instance
|
||||
*
|
||||
* @param incomingRoomKeyRequest the incoming key request
|
||||
*/
|
||||
void deleteIncomingRoomKeyRequest(IncomingRoomKeyRequest incomingRoomKeyRequest);
|
||||
|
||||
/**
|
||||
* Search an IncomingRoomKeyRequest
|
||||
*
|
||||
* @param userId the user id
|
||||
* @param deviceId the device id
|
||||
* @param requestId the request id
|
||||
* @return an IncomingRoomKeyRequest if it exists, else null
|
||||
*/
|
||||
IncomingRoomKeyRequest getIncomingRoomKeyRequest(String userId, String deviceId, String requestId);
|
||||
|
||||
/**
|
||||
* @return the pending IncomingRoomKeyRequest requests
|
||||
*/
|
||||
List<IncomingRoomKeyRequest> getPendingIncomingRoomKeyRequests();
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data.cryptostore;
|
||||
|
||||
public class MXFileCryptoStoreMetaData implements java.io.Serializable {
|
||||
// The obtained user id.
|
||||
public String mUserId;
|
||||
|
||||
// the device id
|
||||
public String mDeviceId;
|
||||
|
||||
// The current version of the store.
|
||||
public int mVersion;
|
||||
|
||||
// flag to tell if the device is announced
|
||||
public boolean mDeviceAnnounced;
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data.cryptostore;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MXFileCryptoStoreMetaData2 implements java.io.Serializable {
|
||||
// avoid creating another MXFileCryptoStoreMetaData3
|
||||
// set a serialVersionUID allows to update the class.
|
||||
private static final long serialVersionUID = 9166554107081078408L;
|
||||
|
||||
// The obtained user id.
|
||||
public String mUserId;
|
||||
|
||||
// the device id
|
||||
public String mDeviceId;
|
||||
|
||||
// The current version of the store.
|
||||
public int mVersion;
|
||||
|
||||
// flag to tell if the device is announced
|
||||
// not anymore used
|
||||
public boolean mDeviceAnnounced;
|
||||
|
||||
// flag to tell if the unverified devices are blacklisted for any room.
|
||||
public boolean mGlobalBlacklistUnverifiedDevices;
|
||||
|
||||
// Room ids list in which the unverified devices are blacklisted
|
||||
public List<String> mBlacklistUnverifiedDevicesRoomIdsList;
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
*
|
||||
* @param userId the user id
|
||||
* @param deviceId the device id
|
||||
* @param version the version
|
||||
*/
|
||||
public MXFileCryptoStoreMetaData2(String userId, String deviceId, int version) {
|
||||
mUserId = new String(userId);
|
||||
mDeviceId = (null != deviceId) ? new String(deviceId) : null;
|
||||
mVersion = version;
|
||||
mDeviceAnnounced = false;
|
||||
mGlobalBlacklistUnverifiedDevices = false;
|
||||
mBlacklistUnverifiedDevicesRoomIdsList = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with the genuine metadata format data.
|
||||
*
|
||||
* @param metadata the genuine metadata format data.
|
||||
*/
|
||||
public MXFileCryptoStoreMetaData2(MXFileCryptoStoreMetaData metadata) {
|
||||
mUserId = metadata.mUserId;
|
||||
mDeviceId = metadata.mDeviceId;
|
||||
mVersion = metadata.mVersion;
|
||||
mDeviceAnnounced = metadata.mDeviceAnnounced;
|
||||
mGlobalBlacklistUnverifiedDevices = false;
|
||||
mBlacklistUnverifiedDevicesRoomIdsList = new ArrayList<>();
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.internal.legacy.data.metrics;
|
||||
|
||||
/**
|
||||
* This interface defines methods for collecting metrics data associated with startup times and stats
|
||||
* Those callbacks can be called from any threads
|
||||
*/
|
||||
public interface MetricsListener {
|
||||
|
||||
/**
|
||||
* Called when the initial sync is finished
|
||||
*
|
||||
* @param duration of the sync
|
||||
*/
|
||||
void onInitialSyncFinished(long duration);
|
||||
|
||||
/**
|
||||
* Called when the incremental sync is finished
|
||||
*
|
||||
* @param duration of the sync
|
||||
*/
|
||||
void onIncrementalSyncFinished(long duration);
|
||||
|
||||
/**
|
||||
* Called when a store is preloaded
|
||||
*
|
||||
* @param duration of the preload
|
||||
*/
|
||||
void onStorePreloaded(long duration);
|
||||
|
||||
/**
|
||||
* Called when a sync is complete
|
||||
*
|
||||
* @param nbOfRooms loaded in the @SyncResponse
|
||||
*/
|
||||
void onRoomsLoaded(int nbOfRooms);
|
||||
|
||||
}
|
@ -0,0 +1,645 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data.store;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.data.timeline.EventTimeline;
|
||||
import im.vector.matrix.android.internal.legacy.data.Room;
|
||||
import im.vector.matrix.android.internal.legacy.data.RoomAccountData;
|
||||
import im.vector.matrix.android.internal.legacy.data.RoomSummary;
|
||||
import im.vector.matrix.android.internal.legacy.data.metrics.MetricsListener;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.ReceiptData;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.RoomMember;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.TokensChunkEvents;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.User;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.group.Group;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.pid.ThirdPartyIdentifier;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* An interface for storing and retrieving Matrix objects.
|
||||
*/
|
||||
public interface IMXStore {
|
||||
/**
|
||||
* Save changes in the store.
|
||||
* If the store uses permanent storage like database or file, it is the optimised time
|
||||
* to commit the last changes.
|
||||
*/
|
||||
void commit();
|
||||
|
||||
/**
|
||||
* Open the store.
|
||||
*/
|
||||
void open();
|
||||
|
||||
/**
|
||||
* Close the store.
|
||||
* Any pending operation must be complete in this call.
|
||||
*/
|
||||
void close();
|
||||
|
||||
/**
|
||||
* Clear the store.
|
||||
* Any pending operation must be complete in this call.
|
||||
*/
|
||||
void clear();
|
||||
|
||||
/**
|
||||
* @return the used context
|
||||
*/
|
||||
Context getContext();
|
||||
|
||||
/**
|
||||
* Indicate if the MXStore implementation stores data permanently.
|
||||
* Permanent storage allows the SDK to make less requests at the startup.
|
||||
*
|
||||
* @return true if permanent.
|
||||
*/
|
||||
boolean isPermanent();
|
||||
|
||||
/**
|
||||
* Check if the initial load is performed.
|
||||
*
|
||||
* @return true if it is ready.
|
||||
*/
|
||||
boolean isReady();
|
||||
|
||||
/**
|
||||
* Check if the read receipts are ready to be used.
|
||||
*
|
||||
* @return true if they are ready.
|
||||
*/
|
||||
boolean areReceiptsReady();
|
||||
|
||||
/**
|
||||
* @return true if the store is corrupted.
|
||||
*/
|
||||
boolean isCorrupted();
|
||||
|
||||
/**
|
||||
* Warn that the store data are corrupted.
|
||||
* It might append if an update request failed.
|
||||
*
|
||||
* @param reason the corruption reason
|
||||
*/
|
||||
void setCorrupted(String reason);
|
||||
|
||||
/**
|
||||
* Returns to disk usage size in bytes.
|
||||
*
|
||||
* @return disk usage size
|
||||
*/
|
||||
long diskUsage();
|
||||
|
||||
/**
|
||||
* Returns the latest known event stream token
|
||||
*
|
||||
* @return the event stream token
|
||||
*/
|
||||
String getEventStreamToken();
|
||||
|
||||
/**
|
||||
* Set the event stream token.
|
||||
*
|
||||
* @param token the event stream token
|
||||
*/
|
||||
void setEventStreamToken(String token);
|
||||
|
||||
/**
|
||||
* Add a MXStore listener.
|
||||
*
|
||||
* @param listener the listener
|
||||
*/
|
||||
void addMXStoreListener(IMXStoreListener listener);
|
||||
|
||||
/**
|
||||
* remove a MXStore listener.
|
||||
*
|
||||
* @param listener the listener
|
||||
*/
|
||||
void removeMXStoreListener(IMXStoreListener listener);
|
||||
|
||||
/**
|
||||
* @return the display name
|
||||
*/
|
||||
String displayName();
|
||||
|
||||
/**
|
||||
* Update the user display name
|
||||
*
|
||||
* @param displayName the displayname
|
||||
* @param ts the timestamp update
|
||||
* @return true if there is an update
|
||||
*/
|
||||
boolean setDisplayName(String displayName, long ts);
|
||||
|
||||
/**
|
||||
* @return the avatar URL
|
||||
*/
|
||||
String avatarURL();
|
||||
|
||||
/**
|
||||
* Update the avatar URL
|
||||
*
|
||||
* @param avatarURL the new URL
|
||||
* @param ts the timestamp update
|
||||
* @return true if there is an update
|
||||
*/
|
||||
boolean setAvatarURL(String avatarURL, long ts);
|
||||
|
||||
/**
|
||||
* @return the third party identifiers list
|
||||
*/
|
||||
List<ThirdPartyIdentifier> thirdPartyIdentifiers();
|
||||
|
||||
/**
|
||||
* Update the third party identifiers list.
|
||||
*
|
||||
* @param identifiers the identifiers list
|
||||
*/
|
||||
void setThirdPartyIdentifiers(List<ThirdPartyIdentifier> identifiers);
|
||||
|
||||
/**
|
||||
* Update the ignored user ids list.
|
||||
*
|
||||
* @param users the user ids list
|
||||
*/
|
||||
void setIgnoredUserIdsList(List<String> users);
|
||||
|
||||
/**
|
||||
* Update the direct chat rooms list
|
||||
*
|
||||
* @param directChatRoomsDict the direct chats map
|
||||
*/
|
||||
void setDirectChatRoomsDict(Map<String, List<String>> directChatRoomsDict);
|
||||
|
||||
/**
|
||||
* @return the known rooms list
|
||||
*/
|
||||
Collection<Room> getRooms();
|
||||
|
||||
/**
|
||||
* Retrieve a room from its room id
|
||||
*
|
||||
* @param roomId the room id
|
||||
* @return the room if it exists
|
||||
*/
|
||||
Room getRoom(String roomId);
|
||||
|
||||
/**
|
||||
* @return the known users lists
|
||||
*/
|
||||
Collection<User> getUsers();
|
||||
|
||||
/**
|
||||
* Retrieves an user by its user id.
|
||||
*
|
||||
* @param userId the user id
|
||||
* @return the user
|
||||
*/
|
||||
User getUser(String userId);
|
||||
|
||||
/**
|
||||
* @return the ignored user ids list
|
||||
*/
|
||||
List<String> getIgnoredUserIdsList();
|
||||
|
||||
/**
|
||||
* @return the direct chats rooms list
|
||||
*/
|
||||
Map<String, List<String>> getDirectChatRoomsDict();
|
||||
|
||||
/**
|
||||
* Flush an updated user.
|
||||
*
|
||||
* @param user the user
|
||||
*/
|
||||
void storeUser(User user);
|
||||
|
||||
/**
|
||||
* Flush an user from a room member.
|
||||
*
|
||||
* @param roomMember the room member
|
||||
*/
|
||||
void updateUserWithRoomMemberEvent(RoomMember roomMember);
|
||||
|
||||
/**
|
||||
* Flush a room.
|
||||
*
|
||||
* @param room the room
|
||||
*/
|
||||
void storeRoom(Room room);
|
||||
|
||||
/**
|
||||
* Store a block of room events either live or from pagination.
|
||||
*
|
||||
* @param roomId the room id
|
||||
* @param tokensChunkEvents the events to be stored.
|
||||
* @param direction the direction; forwards for live, backwards for pagination
|
||||
*/
|
||||
void storeRoomEvents(String roomId, TokensChunkEvents tokensChunkEvents, EventTimeline.Direction direction);
|
||||
|
||||
/**
|
||||
* Store the back token of a room.
|
||||
*
|
||||
* @param roomId the room id.
|
||||
* @param backToken the back token
|
||||
*/
|
||||
void storeBackToken(String roomId, String backToken);
|
||||
|
||||
/**
|
||||
* Store a live room event.
|
||||
*
|
||||
* @param event The event to be stored.
|
||||
*/
|
||||
void storeLiveRoomEvent(Event event);
|
||||
|
||||
/**
|
||||
* @param eventId the id of the event to retrieve.
|
||||
* @param roomId the id of the room.
|
||||
* @return true if the event exists in the store.
|
||||
*/
|
||||
boolean doesEventExist(String eventId, String roomId);
|
||||
|
||||
/**
|
||||
* Retrieve an event from its room Id and its Event id
|
||||
*
|
||||
* @param eventId the event id
|
||||
* @param roomId the room Id
|
||||
* @return the event (null if it is not found)
|
||||
*/
|
||||
Event getEvent(String eventId, String roomId);
|
||||
|
||||
/**
|
||||
* Delete an event
|
||||
*
|
||||
* @param event The event to be deleted.
|
||||
*/
|
||||
void deleteEvent(Event event);
|
||||
|
||||
/**
|
||||
* Remove all sent messages in a room.
|
||||
*
|
||||
* @param roomId the id of the room.
|
||||
* @param keepUnsent set to true to do not delete the unsent message
|
||||
*/
|
||||
void deleteAllRoomMessages(String roomId, boolean keepUnsent);
|
||||
|
||||
/**
|
||||
* Flush the room events.
|
||||
*
|
||||
* @param roomId the id of the room.
|
||||
*/
|
||||
void flushRoomEvents(String roomId);
|
||||
|
||||
/**
|
||||
* Delete the room from the storage.
|
||||
* The room data and its reference will be deleted.
|
||||
*
|
||||
* @param roomId the roomId.
|
||||
*/
|
||||
void deleteRoom(String roomId);
|
||||
|
||||
/**
|
||||
* Delete the room data from the storage;
|
||||
* The room data are cleared but the getRoom returned object will be the same.
|
||||
*
|
||||
* @param roomId the roomId.
|
||||
*/
|
||||
void deleteRoomData(String roomId);
|
||||
|
||||
/**
|
||||
* Retrieve all non-state room events for this room.
|
||||
*
|
||||
* @param roomId The room ID
|
||||
* @return A collection of events. null if there is no cached event.
|
||||
*/
|
||||
Collection<Event> getRoomMessages(final String roomId);
|
||||
|
||||
/**
|
||||
* Retrieve all non-state room events for this room.
|
||||
*
|
||||
* @param roomId The room ID
|
||||
* @param fromToken the token
|
||||
* @param limit the maximum number of messages to retrieve.
|
||||
* @return A collection of events. null if there is no cached event.
|
||||
*/
|
||||
TokensChunkEvents getEarlierMessages(final String roomId, final String fromToken, final int limit);
|
||||
|
||||
/**
|
||||
* Get the oldest event from the given room (to prevent pagination overlap).
|
||||
*
|
||||
* @param roomId the room id
|
||||
* @return the event
|
||||
*/
|
||||
Event getOldestEvent(String roomId);
|
||||
|
||||
/**
|
||||
* Get the latest event from the given room (to update summary for example)
|
||||
*
|
||||
* @param roomId the room id
|
||||
* @return the event
|
||||
*/
|
||||
Event getLatestEvent(String roomId);
|
||||
|
||||
/**
|
||||
* Count the number of events after the provided events id
|
||||
*
|
||||
* @param roomId the room id.
|
||||
* @param eventId the event id to find.
|
||||
* @return the events count after this event if
|
||||
*/
|
||||
int eventsCountAfter(String roomId, String eventId);
|
||||
|
||||
// Design note: This is part of the store interface so the concrete implementation can leverage
|
||||
// how they are storing the data to do this in an efficient manner (e.g. SQL JOINs)
|
||||
// compared to calling getRooms() then getRoomEvents(roomId, limit=1) for each room
|
||||
// (which forces single SELECTs)
|
||||
|
||||
/**
|
||||
* <p>Retrieve a list of all the room summaries stored.</p>
|
||||
* Typically this method will be called when generating a 'Recent Activity' list.
|
||||
*
|
||||
* @return A collection of room summaries.
|
||||
*/
|
||||
Collection<RoomSummary> getSummaries();
|
||||
|
||||
/**
|
||||
* Get the stored summary for the given room.
|
||||
*
|
||||
* @param roomId the room id
|
||||
* @return the summary for the room, or null in case of error
|
||||
*/
|
||||
@Nullable
|
||||
RoomSummary getSummary(String roomId);
|
||||
|
||||
/**
|
||||
* Flush a room summary
|
||||
*
|
||||
* @param summary the summary.
|
||||
*/
|
||||
void flushSummary(RoomSummary summary);
|
||||
|
||||
/**
|
||||
* Flush the room summaries
|
||||
*/
|
||||
void flushSummaries();
|
||||
|
||||
/**
|
||||
* Store a new summary.
|
||||
*
|
||||
* @param summary the summary
|
||||
*/
|
||||
void storeSummary(RoomSummary summary);
|
||||
|
||||
/**
|
||||
* Store the room liveState.
|
||||
*
|
||||
* @param roomId roomId the id of the room.
|
||||
*/
|
||||
void storeLiveStateForRoom(String roomId);
|
||||
|
||||
/**
|
||||
* Store a room state event.
|
||||
* The room states are built with several events.
|
||||
*
|
||||
* @param roomId the room id
|
||||
* @param event the event
|
||||
*/
|
||||
void storeRoomStateEvent(String roomId, Event event);
|
||||
|
||||
/**
|
||||
* Retrieve the room state creation events
|
||||
*
|
||||
* @param roomId the room id
|
||||
* @param callback the asynchronous callback
|
||||
*/
|
||||
void getRoomStateEvents(String roomId, ApiCallback<List<Event>> callback);
|
||||
|
||||
/**
|
||||
* Return the list of latest unsent events.
|
||||
* The provided events are the unsent ones since the last sent one.
|
||||
* They are ordered.
|
||||
*
|
||||
* @param roomId the room id
|
||||
* @return list of unsent events
|
||||
*/
|
||||
List<Event> getLatestUnsentEvents(String roomId);
|
||||
|
||||
/**
|
||||
* Return the list of undelivered events
|
||||
*
|
||||
* @param roomId the room id
|
||||
* @return list of undelivered events
|
||||
*/
|
||||
List<Event> getUndeliveredEvents(String roomId);
|
||||
|
||||
/**
|
||||
* Return the list of unknown device events.
|
||||
*
|
||||
* @param roomId the room id
|
||||
* @return list of unknown device events
|
||||
*/
|
||||
List<Event> getUnknownDeviceEvents(String roomId);
|
||||
|
||||
/**
|
||||
* Returns the receipts list for an event in a dedicated room.
|
||||
* if sort is set to YES, they are sorted from the latest to the oldest ones.
|
||||
*
|
||||
* @param roomId The room Id.
|
||||
* @param eventId The event Id. (null to retrieve all existing receipts)
|
||||
* @param excludeSelf exclude the oneself read receipts.
|
||||
* @param sort to sort them from the latest to the oldest
|
||||
* @return the receipts for an event in a dedicated room.
|
||||
*/
|
||||
List<ReceiptData> getEventReceipts(String roomId, String eventId, boolean excludeSelf, boolean sort);
|
||||
|
||||
/**
|
||||
* Store the receipt for an user in a room.
|
||||
* The receipt validity is checked i.e the receipt is not for an already read message.
|
||||
*
|
||||
* @param receipt The event
|
||||
* @param roomId The roomId
|
||||
* @return true if the receipt has been stored
|
||||
*/
|
||||
boolean storeReceipt(ReceiptData receipt, String roomId);
|
||||
|
||||
/**
|
||||
* Get the receipt for an user in a dedicated room.
|
||||
*
|
||||
* @param roomId the room id.
|
||||
* @param userId the user id.
|
||||
* @return the dedicated receipt
|
||||
*/
|
||||
ReceiptData getReceipt(String roomId, String userId);
|
||||
|
||||
/**
|
||||
* Provides the unread events list.
|
||||
*
|
||||
* @param roomId the room id.
|
||||
* @param types an array of event types strings (Event.EVENT_TYPE_XXX).
|
||||
* @return the unread events list.
|
||||
*/
|
||||
List<Event> unreadEvents(String roomId, List<String> types);
|
||||
|
||||
/**
|
||||
* Check if an event has been read by an user.
|
||||
*
|
||||
* @param roomId the room Id
|
||||
* @param userId the user id
|
||||
* @param eventId the event id
|
||||
* @return true if the user has read the message.
|
||||
*/
|
||||
boolean isEventRead(String roomId, String userId, String eventId);
|
||||
|
||||
/**
|
||||
* Store the user data for a room.
|
||||
*
|
||||
* @param roomId The room Id.
|
||||
* @param accountData the account data.
|
||||
*/
|
||||
void storeAccountData(String roomId, RoomAccountData accountData);
|
||||
|
||||
/**
|
||||
* Provides the store preload time in milliseconds.
|
||||
*
|
||||
* @return the store preload time in milliseconds.
|
||||
*/
|
||||
long getPreloadTime();
|
||||
|
||||
/**
|
||||
* Provides some store stats
|
||||
*
|
||||
* @return the store stats
|
||||
*/
|
||||
Map<String, Long> getStats();
|
||||
|
||||
/**
|
||||
* Start a runnable from the store thread
|
||||
*
|
||||
* @param runnable the runnable to call
|
||||
*/
|
||||
void post(Runnable runnable);
|
||||
|
||||
/**
|
||||
* Store a group
|
||||
*
|
||||
* @param group the group to store
|
||||
*/
|
||||
void storeGroup(Group group);
|
||||
|
||||
/**
|
||||
* Flush a group in store.
|
||||
*
|
||||
* @param group the group
|
||||
*/
|
||||
void flushGroup(Group group);
|
||||
|
||||
/**
|
||||
* Delete a group
|
||||
*
|
||||
* @param groupId the group id to delete
|
||||
*/
|
||||
void deleteGroup(String groupId);
|
||||
|
||||
/**
|
||||
* Retrieve a group from its id.
|
||||
*
|
||||
* @param groupId the group id
|
||||
* @return the group if it exists
|
||||
*/
|
||||
Group getGroup(String groupId);
|
||||
|
||||
/**
|
||||
* @return the stored groups
|
||||
*/
|
||||
Collection<Group> getGroups();
|
||||
|
||||
/**
|
||||
* Set the URL preview status
|
||||
*
|
||||
* @param value the URL preview status
|
||||
*/
|
||||
void setURLPreviewEnabled(boolean value);
|
||||
|
||||
/**
|
||||
* Tells if the global URL preview is enabled.
|
||||
*
|
||||
* @return true if it is enabled
|
||||
*/
|
||||
boolean isURLPreviewEnabled();
|
||||
|
||||
/**
|
||||
* Update the rooms list which don't have URL previews
|
||||
*
|
||||
* @param roomIds the room ids list
|
||||
*/
|
||||
void setRoomsWithoutURLPreview(Set<String> roomIds);
|
||||
|
||||
/**
|
||||
* Set the user widgets
|
||||
*/
|
||||
void setUserWidgets(Map<String, Object> contentDict);
|
||||
|
||||
/**
|
||||
* Get the user widgets
|
||||
*/
|
||||
Map<String, Object> getUserWidgets();
|
||||
|
||||
/**
|
||||
* @return the room ids list which don't have URL preview enabled
|
||||
*/
|
||||
Set<String> getRoomsWithoutURLPreviews();
|
||||
|
||||
/**
|
||||
* Add a couple Json filter / filterId
|
||||
*/
|
||||
void addFilter(String jsonFilter, String filterId);
|
||||
|
||||
/**
|
||||
* Get the Map of all filters configured server side (note: only by this current instance of Riot)
|
||||
*/
|
||||
Map<String, String> getFilters();
|
||||
|
||||
/**
|
||||
* Set the public key of the antivirus server
|
||||
*/
|
||||
void setAntivirusServerPublicKey(@Nullable String key);
|
||||
|
||||
/**
|
||||
* @return the public key of the antivirus server
|
||||
*/
|
||||
@Nullable
|
||||
String getAntivirusServerPublicKey();
|
||||
|
||||
/**
|
||||
* Update the metrics listener
|
||||
*
|
||||
* @param metricsListener the metrics listener
|
||||
*/
|
||||
void setMetricsListener(MetricsListener metricsListener);
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data.store;
|
||||
|
||||
/**
|
||||
* An interface for listening the store events
|
||||
*/
|
||||
public interface IMXStoreListener {
|
||||
/**
|
||||
* The store has loaded its internal data.
|
||||
* Let any post processing data management.
|
||||
* It is called in the store thread before calling onStoreReady.
|
||||
*
|
||||
* @param accountId the account id
|
||||
*/
|
||||
void postProcess(String accountId);
|
||||
|
||||
/**
|
||||
* Called when the store is initialized
|
||||
*
|
||||
* @param accountId the account identifier
|
||||
*/
|
||||
void onStoreReady(String accountId);
|
||||
|
||||
/**
|
||||
* Called when the store initialization fails.
|
||||
*
|
||||
* @param accountId the account identifier
|
||||
* @param description the corruption error messages
|
||||
*/
|
||||
void onStoreCorrupted(String accountId, String description);
|
||||
|
||||
/**
|
||||
* Called when the store has no more memory
|
||||
*
|
||||
* @param accountId the account identifier
|
||||
* @param description the corruption error messages
|
||||
*/
|
||||
void onStoreOOM(String accountId, String description);
|
||||
|
||||
/**
|
||||
* The read receipts of a room is loaded are loaded
|
||||
*
|
||||
* @param roomId the room id
|
||||
*/
|
||||
void onReadReceiptsLoaded(String roomId);
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data.store;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.pid.ThirdPartyIdentifier;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class MXFileStoreMetaData implements java.io.Serializable {
|
||||
// The obtained user id.
|
||||
public String mUserId = null;
|
||||
|
||||
// The access token to create a MXRestClient.
|
||||
public String mAccessToken = null;
|
||||
|
||||
// The token indicating from where to start listening event stream to get live events.
|
||||
public String mEventStreamToken = null;
|
||||
|
||||
//The current version of the store.
|
||||
public int mVersion = -1;
|
||||
|
||||
/**
|
||||
* User information
|
||||
*/
|
||||
public String mUserDisplayName = null;
|
||||
public String mUserAvatarUrl = null;
|
||||
public List<ThirdPartyIdentifier> mThirdPartyIdentifiers = null;
|
||||
public List<String> mIgnoredUsers = new ArrayList<>();
|
||||
public Map<String, List<String>> mDirectChatRoomsMap = null;
|
||||
public boolean mIsUrlPreviewEnabled = false;
|
||||
public Map<String, Object> mUserWidgets = new HashMap<>();
|
||||
public Set<String> mRoomsListWithoutURLPrevew = new HashSet<>();
|
||||
|
||||
// To store known filters by the server. Keys are the filter as a Json String, Values are the filterId returned by the server
|
||||
// Mainly used to store a filterId related to a corresponding Json string.
|
||||
public Map<String, String> mKnownFilters = new HashMap<>();
|
||||
|
||||
// crypto
|
||||
public boolean mEndToEndDeviceAnnounced = false;
|
||||
|
||||
public String mAntivirusServerPublicKey;
|
||||
|
||||
public MXFileStoreMetaData deepCopy() {
|
||||
MXFileStoreMetaData copy = new MXFileStoreMetaData();
|
||||
|
||||
copy.mUserId = mUserId;
|
||||
copy.mAccessToken = mAccessToken;
|
||||
copy.mEventStreamToken = mEventStreamToken;
|
||||
copy.mVersion = mVersion;
|
||||
copy.mUserDisplayName = mUserDisplayName;
|
||||
|
||||
if (null != copy.mUserDisplayName) {
|
||||
copy.mUserDisplayName.trim();
|
||||
}
|
||||
|
||||
copy.mUserAvatarUrl = mUserAvatarUrl;
|
||||
copy.mThirdPartyIdentifiers = mThirdPartyIdentifiers;
|
||||
copy.mIgnoredUsers = mIgnoredUsers;
|
||||
copy.mDirectChatRoomsMap = mDirectChatRoomsMap;
|
||||
copy.mEndToEndDeviceAnnounced = mEndToEndDeviceAnnounced;
|
||||
|
||||
copy.mAntivirusServerPublicKey = mAntivirusServerPublicKey;
|
||||
|
||||
copy.mIsUrlPreviewEnabled = mIsUrlPreviewEnabled;
|
||||
copy.mUserWidgets = mUserWidgets;
|
||||
copy.mRoomsListWithoutURLPrevew = mRoomsListWithoutURLPrevew;
|
||||
|
||||
copy.mKnownFilters = new HashMap<>(mKnownFilters);
|
||||
|
||||
return copy;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data.store;
|
||||
|
||||
/**
|
||||
* An default implementation of IMXStoreListener
|
||||
*/
|
||||
public class MXStoreListener implements IMXStoreListener {
|
||||
@Override
|
||||
public void postProcess(String accountId) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStoreReady(String accountId) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStoreCorrupted(String accountId, String description) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStoreOOM(String accountId, String description) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReadReceiptsLoaded(String roomId) {
|
||||
}
|
||||
}
|
@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data.timeline;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.data.Room;
|
||||
import im.vector.matrix.android.internal.legacy.data.RoomState;
|
||||
import im.vector.matrix.android.internal.legacy.data.store.IMXStore;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.sync.InvitedRoomSync;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.sync.RoomSync;
|
||||
|
||||
/**
|
||||
* A `EventTimeline` instance represents a contiguous sequence of events in a room.
|
||||
* <p>
|
||||
* There are two kinds of timeline:
|
||||
* <p>
|
||||
* - live timelines: they receive live events from the events stream. You can paginate
|
||||
* backwards but not forwards.
|
||||
* All (live or backwards) events they receive are stored in the store of the current
|
||||
* MXSession.
|
||||
* <p>
|
||||
* - past timelines: they start in the past from an `initialEventId`. They are filled
|
||||
* with events on calls of [MXEventTimeline paginate] in backwards or forwards direction.
|
||||
* Events are stored in a in-memory store (MXMemoryStore).
|
||||
*/
|
||||
public interface EventTimeline {
|
||||
/**
|
||||
* Defines that the current timeline is an historical one
|
||||
*
|
||||
* @param isHistorical true when the current timeline is an historical one
|
||||
*/
|
||||
void setIsHistorical(boolean isHistorical);
|
||||
|
||||
/**
|
||||
* Returns true if the current timeline is an historical one
|
||||
*/
|
||||
boolean isHistorical();
|
||||
|
||||
/**
|
||||
* @return the unique identifier
|
||||
*/
|
||||
String getTimelineId();
|
||||
|
||||
/**
|
||||
* @return the dedicated room
|
||||
*/
|
||||
Room getRoom();
|
||||
|
||||
/**
|
||||
* @return the used store
|
||||
*/
|
||||
IMXStore getStore();
|
||||
|
||||
/**
|
||||
* @return the initial event id.
|
||||
*/
|
||||
String getInitialEventId();
|
||||
|
||||
/**
|
||||
* @return true if this timeline is the live one
|
||||
*/
|
||||
boolean isLiveTimeline();
|
||||
|
||||
/**
|
||||
* Get whether we are at the end of the message stream
|
||||
*
|
||||
* @return true if end has been reached
|
||||
*/
|
||||
boolean hasReachedHomeServerForwardsPaginationEnd();
|
||||
|
||||
/**
|
||||
* Reset the back state so that future history requests start over from live.
|
||||
* Must be called when opening a room if interested in history.
|
||||
*/
|
||||
void initHistory();
|
||||
|
||||
/**
|
||||
* @return The state of the room at the top most recent event of the timeline.
|
||||
*/
|
||||
RoomState getState();
|
||||
|
||||
/**
|
||||
* Update the state.
|
||||
*
|
||||
* @param state the new state.
|
||||
*/
|
||||
void setState(RoomState state);
|
||||
|
||||
/**
|
||||
* Handle the invitation room events
|
||||
*
|
||||
* @param invitedRoomSync the invitation room events.
|
||||
*/
|
||||
void handleInvitedRoomSync(InvitedRoomSync invitedRoomSync);
|
||||
|
||||
/**
|
||||
* Manage the joined room events.
|
||||
*
|
||||
* @param roomSync the roomSync.
|
||||
* @param isGlobalInitialSync true if the sync has been triggered by a global initial sync
|
||||
*/
|
||||
void handleJoinedRoomSync(@NonNull RoomSync roomSync, boolean isGlobalInitialSync);
|
||||
|
||||
/**
|
||||
* Store an outgoing event.
|
||||
*
|
||||
* @param event the event to store
|
||||
*/
|
||||
void storeOutgoingEvent(Event event);
|
||||
|
||||
/**
|
||||
* Tells if a back pagination can be triggered.
|
||||
*
|
||||
* @return true if a back pagination can be triggered.
|
||||
*/
|
||||
boolean canBackPaginate();
|
||||
|
||||
/**
|
||||
* Request older messages.
|
||||
*
|
||||
* @param callback the asynchronous callback
|
||||
* @return true if request starts
|
||||
*/
|
||||
boolean backPaginate(ApiCallback<Integer> callback);
|
||||
|
||||
/**
|
||||
* Request older messages.
|
||||
*
|
||||
* @param eventCount number of events we want to retrieve
|
||||
* @param callback callback to implement to be informed that the pagination request has been completed. Can be null.
|
||||
* @return true if request starts
|
||||
*/
|
||||
boolean backPaginate(int eventCount, ApiCallback<Integer> callback);
|
||||
|
||||
/**
|
||||
* Request older messages.
|
||||
*
|
||||
* @param eventCount number of events we want to retrieve
|
||||
* @param useCachedOnly to use the cached events list only (i.e no request will be triggered)
|
||||
* @param callback callback to implement to be informed that the pagination request has been completed. Can be null.
|
||||
* @return true if request starts
|
||||
*/
|
||||
boolean backPaginate(int eventCount, boolean useCachedOnly, ApiCallback<Integer> callback);
|
||||
|
||||
/**
|
||||
* Request newer messages.
|
||||
*
|
||||
* @param callback callback to implement to be informed that the pagination request has been completed. Can be null.
|
||||
* @return true if request starts
|
||||
*/
|
||||
boolean forwardPaginate(ApiCallback<Integer> callback);
|
||||
|
||||
/**
|
||||
* Trigger a pagination in the expected direction.
|
||||
*
|
||||
* @param direction the direction.
|
||||
* @param callback the callback.
|
||||
* @return true if the operation succeeds
|
||||
*/
|
||||
boolean paginate(Direction direction, ApiCallback<Integer> callback);
|
||||
|
||||
/**
|
||||
* Cancel any pending pagination requests
|
||||
*/
|
||||
void cancelPaginationRequests();
|
||||
|
||||
/**
|
||||
* Reset the pagination timeline and start loading the context around its `initialEventId`.
|
||||
* The retrieved (backwards and forwards) events will be sent to registered listeners.
|
||||
*
|
||||
* @param limit the maximum number of messages to get around the initial event.
|
||||
* @param callback the operation callback
|
||||
*/
|
||||
void resetPaginationAroundInitialEvent(int limit, ApiCallback<Void> callback);
|
||||
|
||||
/**
|
||||
* Add an events listener.
|
||||
*
|
||||
* @param listener the listener to add.
|
||||
*/
|
||||
void addEventTimelineListener(Listener listener);
|
||||
|
||||
/**
|
||||
* Remove an events listener.
|
||||
*
|
||||
* @param listener the listener to remove.
|
||||
*/
|
||||
void removeEventTimelineListener(Listener listener);
|
||||
|
||||
/**
|
||||
* The direction from which an incoming event is considered.
|
||||
*/
|
||||
enum Direction {
|
||||
/**
|
||||
* Forwards when the event is added to the end of the timeline.
|
||||
* These events come from the /sync stream or from forwards pagination.
|
||||
*/
|
||||
FORWARDS,
|
||||
|
||||
/**
|
||||
* Backwards when the event is added to the start of the timeline.
|
||||
* These events come from a back pagination.
|
||||
*/
|
||||
BACKWARDS
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
|
||||
/**
|
||||
* Call when an event has been handled in the timeline.
|
||||
*
|
||||
* @param event the event.
|
||||
* @param direction the direction.
|
||||
* @param roomState the room state
|
||||
*/
|
||||
void onEvent(Event event, Direction direction, RoomState roomState);
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data.timeline;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.MXDataHandler;
|
||||
import im.vector.matrix.android.internal.legacy.data.Room;
|
||||
import im.vector.matrix.android.internal.legacy.data.store.MXMemoryStore;
|
||||
|
||||
/**
|
||||
* This factory creates MXEventTimeline instances
|
||||
*/
|
||||
public class EventTimelineFactory {
|
||||
|
||||
/**
|
||||
* Method to create a live timeline associated with the room.
|
||||
*
|
||||
* @param dataHandler the dataHandler
|
||||
* @param room the linked room
|
||||
* @param roomId the room id
|
||||
*/
|
||||
public static EventTimeline liveTimeline(@NonNull final MXDataHandler dataHandler,
|
||||
@NonNull final Room room,
|
||||
@NonNull final String roomId) {
|
||||
return new MXEventTimeline(dataHandler.getStore(roomId), dataHandler, room, roomId, null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to create an in memory timeline for a room.
|
||||
*
|
||||
* @param dataHandler the data handler
|
||||
* @param roomId the room id.
|
||||
*/
|
||||
public static EventTimeline inMemoryTimeline(@NonNull final MXDataHandler dataHandler,
|
||||
@NonNull final String roomId) {
|
||||
return inMemoryTimeline(dataHandler, roomId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to create a past timeline around an eventId.
|
||||
* It will create a memory store and a room
|
||||
*
|
||||
* @param dataHandler the data handler
|
||||
* @param roomId the room id
|
||||
* @param eventId the event id
|
||||
*/
|
||||
public static EventTimeline pastTimeline(@NonNull final MXDataHandler dataHandler,
|
||||
@NonNull final String roomId,
|
||||
@NonNull final String eventId) {
|
||||
return inMemoryTimeline(dataHandler, roomId, eventId);
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Private
|
||||
* ========================================================================================== */
|
||||
|
||||
/**
|
||||
* Method to create a in memory timeline.
|
||||
* It will create a memory store and a room
|
||||
*
|
||||
* @param dataHandler the data handler
|
||||
* @param roomId the room id
|
||||
* @param eventId the event id or null
|
||||
*/
|
||||
private static EventTimeline inMemoryTimeline(@NonNull final MXDataHandler dataHandler,
|
||||
@NonNull final String roomId,
|
||||
@Nullable final String eventId) {
|
||||
final MXMemoryStore store = new MXMemoryStore(dataHandler.getCredentials(), null);
|
||||
final Room room = dataHandler.getRoom(store, roomId, true);
|
||||
final EventTimeline eventTimeline = new MXEventTimeline(store, dataHandler, room, roomId, eventId, false);
|
||||
room.setTimeline(eventTimeline);
|
||||
room.setReadyState(true);
|
||||
return eventTimeline;
|
||||
}
|
||||
}
|
@ -0,0 +1,990 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data.timeline;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Looper;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.MXDataHandler;
|
||||
import im.vector.matrix.android.internal.legacy.data.Room;
|
||||
import im.vector.matrix.android.internal.legacy.data.RoomState;
|
||||
import im.vector.matrix.android.internal.legacy.data.RoomSummary;
|
||||
import im.vector.matrix.android.internal.legacy.data.store.IMXStore;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.SimpleApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.EventContext;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.TokensChunkEvents;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.sync.InvitedRoomSync;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.sync.RoomSync;
|
||||
import im.vector.matrix.android.internal.legacy.util.FilterUtil;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A private implementation of EventTimeline interface. It's not exposed as you don't have to directly instantiate it.
|
||||
* Should be instantiated through EventTimelineFactory.
|
||||
*/
|
||||
class MXEventTimeline implements EventTimeline {
|
||||
private static final String LOG_TAG = MXEventTimeline.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* The initial event id used to initialise the timeline.
|
||||
* null in case of live timeline.
|
||||
*/
|
||||
private String mInitialEventId;
|
||||
|
||||
/**
|
||||
* Indicate if this timeline is a live one.
|
||||
*/
|
||||
private boolean mIsLiveTimeline;
|
||||
|
||||
/**
|
||||
* The associated room.
|
||||
*/
|
||||
private final Room mRoom;
|
||||
|
||||
/**
|
||||
* the room Id
|
||||
*/
|
||||
private String mRoomId;
|
||||
|
||||
/**
|
||||
* The store.
|
||||
*/
|
||||
private IMXStore mStore;
|
||||
|
||||
/**
|
||||
* MXStore does only back pagination. So, the forward pagination token for
|
||||
* past timelines is managed locally.
|
||||
*/
|
||||
private String mForwardsPaginationToken;
|
||||
private boolean mHasReachedHomeServerForwardsPaginationEnd;
|
||||
|
||||
/**
|
||||
* The data handler : used to retrieve data from the store or to trigger REST requests.
|
||||
*/
|
||||
private MXDataHandler mDataHandler;
|
||||
|
||||
/**
|
||||
* Pending request statuses
|
||||
*/
|
||||
private boolean mIsBackPaginating = false;
|
||||
private boolean mIsForwardPaginating = false;
|
||||
|
||||
/**
|
||||
* true if the back history has been retrieved.
|
||||
*/
|
||||
public boolean mCanBackPaginate = true;
|
||||
|
||||
/**
|
||||
* true if the last back chunck has been received
|
||||
*/
|
||||
private boolean mIsLastBackChunk;
|
||||
|
||||
/**
|
||||
* the server provides a token even for the first room message (which should never change it is the creator message).
|
||||
* so requestHistory always triggers a remote request which returns an empty json.
|
||||
* try to avoid such behaviour
|
||||
*/
|
||||
private String mBackwardTopToken = "not yet found";
|
||||
|
||||
// true when the current timeline is an historical one
|
||||
private boolean mIsHistorical;
|
||||
|
||||
/**
|
||||
* Unique identifier
|
||||
*/
|
||||
private final String mTimelineId = System.currentTimeMillis() + "";
|
||||
|
||||
/**
|
||||
* * This class handles storing a live room event in a dedicated store.
|
||||
*/
|
||||
private final TimelineEventSaver mTimelineEventSaver;
|
||||
|
||||
/**
|
||||
* This class is responsible for holding the state and backState of a room timeline
|
||||
*/
|
||||
private final TimelineStateHolder mStateHolder;
|
||||
|
||||
/**
|
||||
* This class handle the timeline event listeners
|
||||
*/
|
||||
private final TimelineEventListeners mEventListeners;
|
||||
|
||||
/**
|
||||
* This class is responsible for handling events coming down from the event stream.
|
||||
*/
|
||||
private final TimelineLiveEventHandler mLiveEventHandler;
|
||||
|
||||
/**
|
||||
* Constructor with package visibility. Creation should be done through EventTimelineFactory
|
||||
*
|
||||
* @param store the store associated (in case of past timeline, the store is memory only)
|
||||
* @param dataHandler the dataHandler
|
||||
* @param room the room
|
||||
* @param roomId the room id
|
||||
* @param eventId the eventId
|
||||
* @param isLive true if the timeline is a live one
|
||||
*/
|
||||
MXEventTimeline(@NonNull final IMXStore store,
|
||||
@NonNull final MXDataHandler dataHandler,
|
||||
@NonNull final Room room,
|
||||
@NonNull final String roomId,
|
||||
@Nullable final String eventId,
|
||||
final boolean isLive) {
|
||||
mIsLiveTimeline = isLive;
|
||||
mInitialEventId = eventId;
|
||||
mDataHandler = dataHandler;
|
||||
mRoom = room;
|
||||
mRoomId = roomId;
|
||||
mStore = store;
|
||||
mEventListeners = new TimelineEventListeners();
|
||||
mStateHolder = new TimelineStateHolder(mDataHandler, mStore, roomId);
|
||||
final StateEventRedactionChecker stateEventRedactionChecker = new StateEventRedactionChecker(this, mStateHolder);
|
||||
mTimelineEventSaver = new TimelineEventSaver(mStore, mRoom, mStateHolder);
|
||||
final TimelinePushWorker timelinePushWorker = new TimelinePushWorker(mDataHandler);
|
||||
mLiveEventHandler = new TimelineLiveEventHandler(this,
|
||||
mTimelineEventSaver,
|
||||
stateEventRedactionChecker,
|
||||
timelinePushWorker,
|
||||
mStateHolder,
|
||||
mEventListeners);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines that the current timeline is an historical one
|
||||
*
|
||||
* @param isHistorical true when the current timeline is an historical one
|
||||
*/
|
||||
@Override
|
||||
public void setIsHistorical(boolean isHistorical) {
|
||||
mIsHistorical = isHistorical;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current timeline is an historical one
|
||||
*/
|
||||
@Override
|
||||
public boolean isHistorical() {
|
||||
return mIsHistorical;
|
||||
}
|
||||
|
||||
/*
|
||||
* @return the unique identifier
|
||||
*/
|
||||
@Override
|
||||
public String getTimelineId() {
|
||||
return mTimelineId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the dedicated room
|
||||
*/
|
||||
@Override
|
||||
public Room getRoom() {
|
||||
return mRoom;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the used store
|
||||
*/
|
||||
@Override
|
||||
public IMXStore getStore() {
|
||||
return mStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the initial event id.
|
||||
*/
|
||||
@Override
|
||||
public String getInitialEventId() {
|
||||
return mInitialEventId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this timeline is the live one
|
||||
*/
|
||||
@Override
|
||||
public boolean isLiveTimeline() {
|
||||
return mIsLiveTimeline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether we are at the end of the message stream
|
||||
*
|
||||
* @return true if end has been reached
|
||||
*/
|
||||
@Override
|
||||
public boolean hasReachedHomeServerForwardsPaginationEnd() {
|
||||
return mHasReachedHomeServerForwardsPaginationEnd;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reset the back state so that future history requests start over from live.
|
||||
* Must be called when opening a room if interested in history.
|
||||
*/
|
||||
@Override
|
||||
public void initHistory() {
|
||||
final RoomState backState = getState().deepCopy();
|
||||
setBackState(backState);
|
||||
mCanBackPaginate = true;
|
||||
|
||||
mIsBackPaginating = false;
|
||||
mIsForwardPaginating = false;
|
||||
|
||||
// sanity check
|
||||
if (null != mDataHandler && null != mDataHandler.getDataRetriever()) {
|
||||
mDataHandler.resetReplayAttackCheckInTimeline(getTimelineId());
|
||||
mDataHandler.getDataRetriever().cancelHistoryRequests(mRoomId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The state of the room at the top most recent event of the timeline.
|
||||
*/
|
||||
@Override
|
||||
public RoomState getState() {
|
||||
return mStateHolder.getState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the state.
|
||||
*
|
||||
* @param state the new state.
|
||||
*/
|
||||
@Override
|
||||
public void setState(RoomState state) {
|
||||
mStateHolder.setState(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the backState.
|
||||
*
|
||||
* @param state the new backState.
|
||||
*/
|
||||
private void setBackState(RoomState state) {
|
||||
mStateHolder.setBackState(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the backState.
|
||||
*/
|
||||
private RoomState getBackState() {
|
||||
return mStateHolder.getBackState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock over the backPaginate process
|
||||
*
|
||||
* @param canBackPaginate the state of the lock (true/false)
|
||||
*/
|
||||
protected void setCanBackPaginate(final boolean canBackPaginate) {
|
||||
mCanBackPaginate = canBackPaginate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a deep copy or the dedicated state.
|
||||
*
|
||||
* @param direction the room state direction to deep copy.
|
||||
*/
|
||||
private void deepCopyState(Direction direction) {
|
||||
mStateHolder.deepCopyState(direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a state event to keep the internal live and back states up to date.
|
||||
*
|
||||
* @param event the state event
|
||||
* @param direction the direction; ie. forwards for live state, backwards for back state
|
||||
* @return true if the event has been processed.
|
||||
*/
|
||||
private boolean processStateEvent(Event event, Direction direction) {
|
||||
return mStateHolder.processStateEvent(event, direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the invitation room events
|
||||
*
|
||||
* @param invitedRoomSync the invitation room events.
|
||||
*/
|
||||
@Override
|
||||
public void handleInvitedRoomSync(InvitedRoomSync invitedRoomSync) {
|
||||
final TimelineInvitedRoomSyncHandler invitedRoomSyncHandler = new TimelineInvitedRoomSyncHandler(mRoom, mLiveEventHandler, invitedRoomSync);
|
||||
invitedRoomSyncHandler.handle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage the joined room events.
|
||||
*
|
||||
* @param roomSync the roomSync.
|
||||
* @param isGlobalInitialSync true if the sync has been triggered by a global initial sync
|
||||
*/
|
||||
@Override
|
||||
public void handleJoinedRoomSync(@NonNull final RoomSync roomSync, final boolean isGlobalInitialSync) {
|
||||
final TimelineJoinRoomSyncHandler joinRoomSyncHandler = new TimelineJoinRoomSyncHandler(this,
|
||||
roomSync,
|
||||
mStateHolder,
|
||||
mLiveEventHandler,
|
||||
isGlobalInitialSync);
|
||||
joinRoomSyncHandler.handle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Store an outgoing event.
|
||||
*
|
||||
* @param event the event to store
|
||||
*/
|
||||
@Override
|
||||
public void storeOutgoingEvent(Event event) {
|
||||
if (mIsLiveTimeline) {
|
||||
storeEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the event and update the dedicated room summary
|
||||
*
|
||||
* @param event the event to store
|
||||
*/
|
||||
private void storeEvent(Event event) {
|
||||
mTimelineEventSaver.storeEvent(event);
|
||||
}
|
||||
|
||||
//================================================================================
|
||||
// History request
|
||||
//================================================================================
|
||||
|
||||
private static final int MAX_EVENT_COUNT_PER_PAGINATION = 30;
|
||||
|
||||
// the storage events are buffered to provide a small bunch of events
|
||||
// the storage can provide a big bunch which slows down the UI.
|
||||
public class SnapshotEvent {
|
||||
public final Event mEvent;
|
||||
public final RoomState mState;
|
||||
|
||||
public SnapshotEvent(Event event, RoomState state) {
|
||||
mEvent = event;
|
||||
mState = state;
|
||||
}
|
||||
}
|
||||
|
||||
// avoid adding to many events
|
||||
// the room history request can provide more than expected event.
|
||||
private final List<SnapshotEvent> mSnapshotEvents = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Send MAX_EVENT_COUNT_PER_PAGINATION events to the caller.
|
||||
*
|
||||
* @param maxEventCount the max event count
|
||||
* @param callback the callback.
|
||||
*/
|
||||
private void manageBackEvents(int maxEventCount, final ApiCallback<Integer> callback) {
|
||||
// check if the SDK was not logged out
|
||||
if (!mDataHandler.isAlive()) {
|
||||
Log.d(LOG_TAG, "manageEvents : mDataHandler is not anymore active.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
int count = Math.min(mSnapshotEvents.size(), maxEventCount);
|
||||
|
||||
Event latestSupportedEvent = null;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
SnapshotEvent snapshotedEvent = mSnapshotEvents.get(0);
|
||||
|
||||
// in some cases, there is no displayed summary
|
||||
// https://github.com/vector-im/vector-android/pull/354
|
||||
if (null == latestSupportedEvent && RoomSummary.isSupportedEvent(snapshotedEvent.mEvent)) {
|
||||
latestSupportedEvent = snapshotedEvent.mEvent;
|
||||
}
|
||||
|
||||
mSnapshotEvents.remove(0);
|
||||
mEventListeners.onEvent(snapshotedEvent.mEvent, Direction.BACKWARDS, snapshotedEvent.mState);
|
||||
}
|
||||
|
||||
// https://github.com/vector-im/vector-android/pull/354
|
||||
// defines a new summary if the known is not supported
|
||||
RoomSummary summary = mStore.getSummary(mRoomId);
|
||||
|
||||
if (null != latestSupportedEvent && (null == summary || !RoomSummary.isSupportedEvent(summary.getLatestReceivedEvent()))) {
|
||||
mStore.storeSummary(new RoomSummary(null, latestSupportedEvent, getState(), mDataHandler.getUserId()));
|
||||
}
|
||||
|
||||
Log.d(LOG_TAG, "manageEvents : commit");
|
||||
mStore.commit();
|
||||
|
||||
if (mSnapshotEvents.size() < MAX_EVENT_COUNT_PER_PAGINATION && mIsLastBackChunk) {
|
||||
mCanBackPaginate = false;
|
||||
}
|
||||
mIsBackPaginating = false;
|
||||
if (callback != null) {
|
||||
try {
|
||||
callback.onSuccess(count);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "requestHistory exception " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add some events in a dedicated direction.
|
||||
*
|
||||
* @param events the events list
|
||||
* @param stateEvents the received state events (in case of lazy loading of room members)
|
||||
* @param direction the direction
|
||||
*/
|
||||
private void addPaginationEvents(List<Event> events,
|
||||
@Nullable List<Event> stateEvents,
|
||||
Direction direction) {
|
||||
RoomSummary summary = mStore.getSummary(mRoomId);
|
||||
boolean shouldCommitStore = false;
|
||||
|
||||
// Process additional state events (this happens in case of lazy loading)
|
||||
if (stateEvents != null) {
|
||||
for (Event stateEvent : stateEvents) {
|
||||
if (direction == Direction.BACKWARDS) {
|
||||
// Enrich the timeline root state with the additional state events observed during back pagination
|
||||
processStateEvent(stateEvent, Direction.FORWARDS);
|
||||
}
|
||||
|
||||
processStateEvent(stateEvent, direction);
|
||||
}
|
||||
}
|
||||
|
||||
// the backward events have a dedicated management to avoid providing too many events for each request
|
||||
for (Event event : events) {
|
||||
boolean processedEvent = true;
|
||||
|
||||
if (event.stateKey != null) {
|
||||
deepCopyState(direction);
|
||||
processedEvent = processStateEvent(event, direction);
|
||||
}
|
||||
|
||||
// Decrypt event if necessary
|
||||
mDataHandler.decryptEvent(event, getTimelineId());
|
||||
|
||||
if (processedEvent) {
|
||||
// warn the listener only if the message is processed.
|
||||
// it should avoid duplicated events.
|
||||
if (direction == Direction.BACKWARDS) {
|
||||
if (mIsLiveTimeline) {
|
||||
// update the summary is the event has been received after the oldest known event
|
||||
// it might happen after a timeline update (hole in the chat history)
|
||||
if (null != summary
|
||||
&& (null == summary.getLatestReceivedEvent()
|
||||
|| event.isValidOriginServerTs()
|
||||
&& summary.getLatestReceivedEvent().originServerTs < event.originServerTs
|
||||
&& RoomSummary.isSupportedEvent(event))) {
|
||||
summary.setLatestReceivedEvent(event, getState());
|
||||
mStore.storeSummary(summary);
|
||||
shouldCommitStore = true;
|
||||
}
|
||||
}
|
||||
mSnapshotEvents.add(new SnapshotEvent(event, getBackState()));
|
||||
// onEvent will be called in manageBackEvents
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldCommitStore) {
|
||||
mStore.commit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add some events in a dedicated direction.
|
||||
*
|
||||
* @param events the events list
|
||||
* @param stateEvents the received state events (in case of lazy loading of room members)
|
||||
* @param direction the direction
|
||||
* @param callback the callback.
|
||||
*/
|
||||
private void addPaginationEvents(final List<Event> events,
|
||||
@Nullable final List<Event> stateEvents,
|
||||
final Direction direction,
|
||||
final ApiCallback<Integer> callback) {
|
||||
AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
addPaginationEvents(events, stateEvents, direction);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void args) {
|
||||
if (direction == Direction.BACKWARDS) {
|
||||
manageBackEvents(MAX_EVENT_COUNT_PER_PAGINATION, callback);
|
||||
} else {
|
||||
for (Event event : events) {
|
||||
mEventListeners.onEvent(event, Direction.FORWARDS, getState());
|
||||
}
|
||||
|
||||
if (null != callback) {
|
||||
callback.onSuccess(events.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
} catch (final Exception e) {
|
||||
Log.e(LOG_TAG, "## addPaginationEvents() failed " + e.getMessage(), e);
|
||||
task.cancel(true);
|
||||
|
||||
new android.os.Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (null != callback) {
|
||||
callback.onUnexpectedError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if a back pagination can be triggered.
|
||||
*
|
||||
* @return true if a back pagination can be triggered.
|
||||
*/
|
||||
@Override
|
||||
public boolean canBackPaginate() {
|
||||
// One at a time please
|
||||
return !mIsBackPaginating
|
||||
// history_visibility flag management
|
||||
&& getState().canBackPaginate(mRoom.isJoined(), mRoom.isInvited())
|
||||
// If we have already reached the end of history
|
||||
&& mCanBackPaginate
|
||||
// If the room is not finished being set up
|
||||
&& mRoom.isReady();
|
||||
}
|
||||
|
||||
/**
|
||||
* Request older messages.
|
||||
*
|
||||
* @param callback the asynchronous callback
|
||||
* @return true if request starts
|
||||
*/
|
||||
@Override
|
||||
public boolean backPaginate(final ApiCallback<Integer> callback) {
|
||||
return backPaginate(MAX_EVENT_COUNT_PER_PAGINATION, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request older messages.
|
||||
*
|
||||
* @param eventCount number of events we want to retrieve
|
||||
* @param callback callback to implement to be informed that the pagination request has been completed. Can be null.
|
||||
* @return true if request starts
|
||||
*/
|
||||
@Override
|
||||
public boolean backPaginate(final int eventCount, final ApiCallback<Integer> callback) {
|
||||
return backPaginate(eventCount, false, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request older messages.
|
||||
*
|
||||
* @param eventCount number of events we want to retrieve
|
||||
* @param useCachedOnly to use the cached events list only (i.e no request will be triggered)
|
||||
* @param callback callback to implement to be informed that the pagination request has been completed. Can be null.
|
||||
* @return true if request starts
|
||||
*/
|
||||
@Override
|
||||
public boolean backPaginate(final int eventCount, final boolean useCachedOnly, final ApiCallback<Integer> callback) {
|
||||
if (!canBackPaginate()) {
|
||||
Log.d(LOG_TAG, "cannot requestHistory " + mIsBackPaginating + " " + !getState().canBackPaginate(mRoom.isJoined(), mRoom.isInvited())
|
||||
+ " " + !mCanBackPaginate + " " + !mRoom.isReady());
|
||||
return false;
|
||||
}
|
||||
|
||||
Log.d(LOG_TAG, "backPaginate starts");
|
||||
|
||||
// restart the pagination
|
||||
if (null == getBackState().getToken()) {
|
||||
mSnapshotEvents.clear();
|
||||
}
|
||||
|
||||
final String fromBackToken = getBackState().getToken();
|
||||
|
||||
mIsBackPaginating = true;
|
||||
|
||||
// enough buffered data
|
||||
if (useCachedOnly
|
||||
|| mSnapshotEvents.size() >= eventCount
|
||||
|| TextUtils.equals(fromBackToken, mBackwardTopToken)
|
||||
|| TextUtils.equals(fromBackToken, Event.PAGINATE_BACK_TOKEN_END)) {
|
||||
|
||||
mIsLastBackChunk = TextUtils.equals(fromBackToken, mBackwardTopToken) || TextUtils.equals(fromBackToken, Event.PAGINATE_BACK_TOKEN_END);
|
||||
|
||||
final android.os.Handler handler = new android.os.Handler(Looper.getMainLooper());
|
||||
final int maxEventsCount;
|
||||
|
||||
if (useCachedOnly) {
|
||||
Log.d(LOG_TAG, "backPaginate : load " + mSnapshotEvents.size() + "cached events list");
|
||||
maxEventsCount = Math.min(mSnapshotEvents.size(), eventCount);
|
||||
} else if (mSnapshotEvents.size() >= eventCount) {
|
||||
Log.d(LOG_TAG, "backPaginate : the events are already loaded.");
|
||||
maxEventsCount = eventCount;
|
||||
} else {
|
||||
Log.d(LOG_TAG, "backPaginate : reach the history top");
|
||||
maxEventsCount = eventCount;
|
||||
}
|
||||
|
||||
// call the callback with a delay
|
||||
// to reproduce the same behaviour as a network request.
|
||||
Runnable r = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
handler.postDelayed(new Runnable() {
|
||||
public void run() {
|
||||
manageBackEvents(maxEventsCount, callback);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
Thread t = new Thread(r);
|
||||
t.start();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
mDataHandler.getDataRetriever().backPaginate(mStore, mRoomId, getBackState().getToken(), eventCount, mDataHandler.isLazyLoadingEnabled(),
|
||||
new SimpleApiCallback<TokensChunkEvents>(callback) {
|
||||
@Override
|
||||
public void onSuccess(TokensChunkEvents response) {
|
||||
if (mDataHandler.isAlive()) {
|
||||
|
||||
if (null != response.chunk) {
|
||||
Log.d(LOG_TAG, "backPaginate : " + response.chunk.size() + " events are retrieved.");
|
||||
} else {
|
||||
Log.d(LOG_TAG, "backPaginate : there is no event");
|
||||
}
|
||||
|
||||
mIsLastBackChunk = null != response.chunk
|
||||
&& 0 == response.chunk.size()
|
||||
&& TextUtils.equals(response.end, response.start)
|
||||
|| null == response.end;
|
||||
|
||||
if (mIsLastBackChunk && null != response.end) {
|
||||
// save its token to avoid useless request
|
||||
mBackwardTopToken = fromBackToken;
|
||||
} else {
|
||||
// the server returns a null pagination token when there is no more available data
|
||||
if (null == response.end) {
|
||||
getBackState().setToken(Event.PAGINATE_BACK_TOKEN_END);
|
||||
} else {
|
||||
getBackState().setToken(response.end);
|
||||
}
|
||||
}
|
||||
|
||||
addPaginationEvents(null == response.chunk ? new ArrayList<Event>() : response.chunk,
|
||||
response.stateEvents,
|
||||
Direction.BACKWARDS,
|
||||
callback);
|
||||
|
||||
} else {
|
||||
Log.d(LOG_TAG, "mDataHandler is not active.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
Log.d(LOG_TAG, "backPaginate onMatrixError");
|
||||
|
||||
// When we've retrieved all the messages from a room, the pagination token is some invalid value
|
||||
if (MatrixError.UNKNOWN.equals(e.errcode)) {
|
||||
mCanBackPaginate = false;
|
||||
}
|
||||
mIsBackPaginating = false;
|
||||
|
||||
super.onMatrixError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
Log.d(LOG_TAG, "backPaginate onNetworkError");
|
||||
|
||||
mIsBackPaginating = false;
|
||||
|
||||
super.onNetworkError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
Log.d(LOG_TAG, "backPaginate onUnexpectedError");
|
||||
|
||||
mIsBackPaginating = false;
|
||||
|
||||
super.onUnexpectedError(e);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request newer messages.
|
||||
*
|
||||
* @param callback callback to implement to be informed that the pagination request has been completed. Can be null.
|
||||
* @return true if request starts
|
||||
*/
|
||||
@Override
|
||||
public boolean forwardPaginate(final ApiCallback<Integer> callback) {
|
||||
if (mIsLiveTimeline) {
|
||||
Log.d(LOG_TAG, "Cannot forward paginate on Live timeline");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mIsForwardPaginating || mHasReachedHomeServerForwardsPaginationEnd) {
|
||||
Log.d(LOG_TAG, "forwardPaginate " + mIsForwardPaginating
|
||||
+ " mHasReachedHomeServerForwardsPaginationEnd " + mHasReachedHomeServerForwardsPaginationEnd);
|
||||
return false;
|
||||
}
|
||||
|
||||
mIsForwardPaginating = true;
|
||||
|
||||
mDataHandler.getDataRetriever().paginate(mStore, mRoomId, mForwardsPaginationToken, Direction.FORWARDS, mDataHandler.isLazyLoadingEnabled(),
|
||||
new SimpleApiCallback<TokensChunkEvents>(callback) {
|
||||
@Override
|
||||
public void onSuccess(TokensChunkEvents response) {
|
||||
if (mDataHandler.isAlive()) {
|
||||
Log.d(LOG_TAG, "forwardPaginate : " + response.chunk.size() + " are retrieved.");
|
||||
|
||||
mHasReachedHomeServerForwardsPaginationEnd = 0 == response.chunk.size() && TextUtils.equals(response.end, response.start);
|
||||
mForwardsPaginationToken = response.end;
|
||||
|
||||
addPaginationEvents(response.chunk,
|
||||
response.stateEvents,
|
||||
Direction.FORWARDS,
|
||||
callback);
|
||||
|
||||
mIsForwardPaginating = false;
|
||||
} else {
|
||||
Log.d(LOG_TAG, "mDataHandler is not active.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
mIsForwardPaginating = false;
|
||||
|
||||
super.onMatrixError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
mIsForwardPaginating = false;
|
||||
|
||||
super.onNetworkError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
mIsForwardPaginating = false;
|
||||
|
||||
super.onUnexpectedError(e);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a pagination in the expected direction.
|
||||
*
|
||||
* @param direction the direction.
|
||||
* @param callback the callback.
|
||||
* @return true if the operation succeeds
|
||||
*/
|
||||
@Override
|
||||
public boolean paginate(Direction direction, final ApiCallback<Integer> callback) {
|
||||
if (Direction.BACKWARDS == direction) {
|
||||
return backPaginate(callback);
|
||||
} else {
|
||||
return forwardPaginate(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel any pending pagination requests
|
||||
*/
|
||||
@Override
|
||||
public void cancelPaginationRequests() {
|
||||
mDataHandler.getDataRetriever().cancelHistoryRequests(mRoomId);
|
||||
mIsBackPaginating = false;
|
||||
mIsForwardPaginating = false;
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// pagination methods
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* Reset the pagination timeline and start loading the context around its `initialEventId`.
|
||||
* The retrieved (backwards and forwards) events will be sent to registered listeners.
|
||||
*
|
||||
* @param limit the maximum number of messages to get around the initial event.
|
||||
* @param callback the operation callback
|
||||
*/
|
||||
@Override
|
||||
public void resetPaginationAroundInitialEvent(final int limit, final ApiCallback<Void> callback) {
|
||||
// Reset the store
|
||||
mStore.deleteRoomData(mRoomId);
|
||||
|
||||
mDataHandler.resetReplayAttackCheckInTimeline(getTimelineId());
|
||||
|
||||
mForwardsPaginationToken = null;
|
||||
mHasReachedHomeServerForwardsPaginationEnd = false;
|
||||
|
||||
mDataHandler.getDataRetriever()
|
||||
.getRoomsRestClient()
|
||||
.getContextOfEvent(mRoomId, mInitialEventId, limit, FilterUtil.createRoomEventFilter(mDataHandler.isLazyLoadingEnabled()),
|
||||
new SimpleApiCallback<EventContext>(callback) {
|
||||
@Override
|
||||
public void onSuccess(final EventContext eventContext) {
|
||||
|
||||
AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
// the state is the one after the latest event of the chunk i.e. the last message of eventContext.eventsAfter
|
||||
for (Event event : eventContext.state) {
|
||||
processStateEvent(event, Direction.FORWARDS);
|
||||
}
|
||||
|
||||
// init the room states
|
||||
initHistory();
|
||||
|
||||
// build the events list
|
||||
List<Event> events = new ArrayList<>();
|
||||
|
||||
Collections.reverse(eventContext.eventsAfter);
|
||||
events.addAll(eventContext.eventsAfter);
|
||||
events.add(eventContext.event);
|
||||
events.addAll(eventContext.eventsBefore);
|
||||
|
||||
// add events after
|
||||
addPaginationEvents(events, null, Direction.BACKWARDS);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void args) {
|
||||
// create dummy forward events list
|
||||
// to center the selected event id
|
||||
// else if might be out of screen
|
||||
List<SnapshotEvent> nextSnapshotEvents = new ArrayList<>(mSnapshotEvents.subList(0, (mSnapshotEvents.size() + 1) / 2));
|
||||
|
||||
// put in the right order
|
||||
Collections.reverse(nextSnapshotEvents);
|
||||
|
||||
// send them one by one
|
||||
for (SnapshotEvent snapshotEvent : nextSnapshotEvents) {
|
||||
mSnapshotEvents.remove(snapshotEvent);
|
||||
mEventListeners.onEvent(snapshotEvent.mEvent, Direction.FORWARDS, snapshotEvent.mState);
|
||||
}
|
||||
|
||||
// init the tokens
|
||||
getBackState().setToken(eventContext.start);
|
||||
mForwardsPaginationToken = eventContext.end;
|
||||
|
||||
// send the back events to complete pagination
|
||||
manageBackEvents(MAX_EVENT_COUNT_PER_PAGINATION, new ApiCallback<Integer>() {
|
||||
@Override
|
||||
public void onSuccess(Integer info) {
|
||||
Log.d(LOG_TAG, "addPaginationEvents succeeds");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
Log.e(LOG_TAG, "addPaginationEvents failed " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
Log.e(LOG_TAG, "addPaginationEvents failed " + e.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
Log.e(LOG_TAG, "addPaginationEvents failed " + e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
|
||||
// everything is done
|
||||
callback.onSuccess(null);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
} catch (final Exception e) {
|
||||
Log.e(LOG_TAG, "## resetPaginationAroundInitialEvent() failed " + e.getMessage(), e);
|
||||
task.cancel(true);
|
||||
|
||||
new android.os.Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (callback != null) {
|
||||
callback.onUnexpectedError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// onEvent listener management.
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* Add an events listener.
|
||||
*
|
||||
* @param listener the listener to add.
|
||||
*/
|
||||
@Override
|
||||
public void addEventTimelineListener(@Nullable final Listener listener) {
|
||||
mEventListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an events listener.
|
||||
*
|
||||
* @param listener the listener to remove.
|
||||
*/
|
||||
@Override
|
||||
public void removeEventTimelineListener(@Nullable final Listener listener) {
|
||||
mEventListeners.remove(listener);
|
||||
}
|
||||
}
|
@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data.timeline;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.MXDataHandler;
|
||||
import im.vector.matrix.android.internal.legacy.data.Room;
|
||||
import im.vector.matrix.android.internal.legacy.data.RoomState;
|
||||
import im.vector.matrix.android.internal.legacy.data.store.IMXStore;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.SimpleApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.RoomMember;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* This class is responsible of checking state events redaction.
|
||||
*/
|
||||
class StateEventRedactionChecker {
|
||||
|
||||
private static final String LOG_TAG = StateEventRedactionChecker.class.getSimpleName();
|
||||
private final EventTimeline mEventTimeline;
|
||||
private final TimelineStateHolder mTimelineStateHolder;
|
||||
|
||||
StateEventRedactionChecker(@NonNull final EventTimeline eventTimeline,
|
||||
@NonNull final TimelineStateHolder timelineStateHolder) {
|
||||
mEventTimeline = eventTimeline;
|
||||
mTimelineStateHolder = timelineStateHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redaction of a state event might require to reload the timeline
|
||||
* because the room states has to be updated.
|
||||
*
|
||||
* @param redactionEvent the redaction event
|
||||
*/
|
||||
public void checkStateEventRedaction(@NonNull final Event redactionEvent) {
|
||||
final IMXStore store = mEventTimeline.getStore();
|
||||
final Room room = mEventTimeline.getRoom();
|
||||
final MXDataHandler dataHandler = room.getDataHandler();
|
||||
final String roomId = room.getRoomId();
|
||||
final String eventId = redactionEvent.getRedactedEventId();
|
||||
final RoomState state = mTimelineStateHolder.getState();
|
||||
Log.d(LOG_TAG, "checkStateEventRedaction of event " + eventId);
|
||||
// check if the state events is locally known
|
||||
state.getStateEvents(store, null, new SimpleApiCallback<List<Event>>() {
|
||||
@Override
|
||||
public void onSuccess(List<Event> stateEvents) {
|
||||
|
||||
// Check whether the current room state depends on this redacted event.
|
||||
boolean isFound = false;
|
||||
for (int index = 0; index < stateEvents.size(); index++) {
|
||||
Event stateEvent = stateEvents.get(index);
|
||||
|
||||
if (TextUtils.equals(stateEvent.eventId, eventId)) {
|
||||
|
||||
Log.d(LOG_TAG, "checkStateEventRedaction: the current room state has been modified by the event redaction");
|
||||
|
||||
// remove expected keys
|
||||
stateEvent.prune(redactionEvent);
|
||||
stateEvents.set(index, stateEvent);
|
||||
// digest the updated state
|
||||
mTimelineStateHolder.processStateEvent(stateEvent, EventTimeline.Direction.FORWARDS);
|
||||
isFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isFound) {
|
||||
// Else try to find the redacted event among members which
|
||||
// are stored apart from other state events
|
||||
|
||||
// Reason: The membership events are not anymore stored in the application store
|
||||
// until we have found a way to improve the way they are stored.
|
||||
// It used to have many out of memory errors because they are too many stored small memory objects.
|
||||
// see https://github.com/matrix-org/matrix-android-sdk/issues/196
|
||||
|
||||
// Note: if lazy loading is on, getMemberByEventId() can return null, but it is ok, because we just want to update our cache
|
||||
RoomMember member = state.getMemberByEventId(eventId);
|
||||
if (member != null) {
|
||||
Log.d(LOG_TAG, "checkStateEventRedaction: the current room members list has been modified by the event redaction");
|
||||
|
||||
// the android SDK does not store stock member events but a representation of them, RoomMember.
|
||||
// Prune this representation
|
||||
member.prune();
|
||||
|
||||
isFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isFound) {
|
||||
store.storeLiveStateForRoom(roomId);
|
||||
// warn that there was a flush
|
||||
mEventTimeline.initHistory();
|
||||
dataHandler.onRoomFlush(roomId);
|
||||
} else {
|
||||
Log.d(LOG_TAG, "checkStateEventRedaction: the redacted event is unknown. Fetch it from the homeserver");
|
||||
checkStateEventRedactionWithHomeserver(dataHandler, roomId, eventId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check with the HS whether the redacted event impacts the room data we have locally.
|
||||
* If yes, local data must be pruned.
|
||||
*
|
||||
* @param eventId the redacted event id
|
||||
*/
|
||||
private void checkStateEventRedactionWithHomeserver(@Nonnull final MXDataHandler dataHandler,
|
||||
@Nonnull final String roomId,
|
||||
@Nonnull final String eventId) {
|
||||
Log.d(LOG_TAG, "checkStateEventRedactionWithHomeserver on event Id " + eventId);
|
||||
|
||||
// We need to figure out if this redacted event is a room state in the past.
|
||||
// If yes, we must prune the `prev_content` of the state event that replaced it.
|
||||
// Indeed, redacted information shouldn't spontaneously appear when you backpaginate...
|
||||
// TODO: This is no more implemented (see https://github.com/vector-im/riot-ios/issues/443).
|
||||
// The previous implementation based on a room initial sync was too heavy server side
|
||||
// and has been removed.
|
||||
if (!TextUtils.isEmpty(eventId)) {
|
||||
Log.d(LOG_TAG, "checkStateEventRedactionWithHomeserver : retrieving the event");
|
||||
dataHandler.getDataRetriever().getRoomsRestClient().getEvent(roomId, eventId, new ApiCallback<Event>() {
|
||||
@Override
|
||||
public void onSuccess(Event event) {
|
||||
if (null != event && null != event.stateKey) {
|
||||
Log.d(LOG_TAG, "checkStateEventRedactionWithHomeserver : the redacted event is a state event in the past." +
|
||||
" TODO: prune prev_content of the new state event");
|
||||
|
||||
} else {
|
||||
Log.d(LOG_TAG, "checkStateEventRedactionWithHomeserver : the redacted event is a not state event -> job is done");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
Log.e(LOG_TAG, "checkStateEventRedactionWithHomeserver : failed to retrieved the redacted event: onNetworkError " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
Log.e(LOG_TAG, "checkStateEventRedactionWithHomeserver : failed to retrieved the redacted event: onNetworkError " + e.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
Log.e(LOG_TAG, "checkStateEventRedactionWithHomeserver : failed to retrieved the redacted event: onNetworkError " + e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data.timeline;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.data.RoomState;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Handle the timeline event listeners
|
||||
* Is responsible for dispatching events
|
||||
*/
|
||||
class TimelineEventListeners {
|
||||
|
||||
private static final String LOG_TAG = TimelineEventListeners.class.getSimpleName();
|
||||
|
||||
// The inner listeners
|
||||
private final List<EventTimeline.Listener> mListeners = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Add an events listener.
|
||||
*
|
||||
* @param listener the listener to add.
|
||||
*/
|
||||
public void add(@Nullable final EventTimeline.Listener listener) {
|
||||
if (listener != null) {
|
||||
synchronized (this) {
|
||||
if (!mListeners.contains(listener)) {
|
||||
mListeners.add(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an events listener.
|
||||
*
|
||||
* @param listener the listener to remove.
|
||||
*/
|
||||
public void remove(@Nullable final EventTimeline.Listener listener) {
|
||||
if (null != listener) {
|
||||
synchronized (this) {
|
||||
mListeners.remove(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the onEvent callback.
|
||||
*
|
||||
* @param event the event.
|
||||
* @param direction the direction.
|
||||
* @param roomState the roomState.
|
||||
*/
|
||||
public void onEvent(@NonNull final Event event,
|
||||
@NonNull final EventTimeline.Direction direction,
|
||||
@NonNull final RoomState roomState) {
|
||||
// ensure that the listeners are called in the UI thread
|
||||
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
|
||||
final List<EventTimeline.Listener> listeners;
|
||||
synchronized (this) {
|
||||
listeners = new ArrayList<>(mListeners);
|
||||
}
|
||||
for (EventTimeline.Listener listener : listeners) {
|
||||
try {
|
||||
listener.onEvent(event, direction, roomState);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "EventTimeline.onEvent " + listener + " crashes " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final Handler handler = new Handler(Looper.getMainLooper());
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onEvent(event, direction, roomState);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data.timeline;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.MXDataHandler;
|
||||
import im.vector.matrix.android.internal.legacy.data.Room;
|
||||
import im.vector.matrix.android.internal.legacy.data.RoomState;
|
||||
import im.vector.matrix.android.internal.legacy.data.RoomSummary;
|
||||
import im.vector.matrix.android.internal.legacy.data.store.IMXStore;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.ReceiptData;
|
||||
|
||||
/**
|
||||
* This class handles storing a live room event in a dedicated store.
|
||||
*/
|
||||
class TimelineEventSaver {
|
||||
|
||||
private final IMXStore mStore;
|
||||
private final Room mRoom;
|
||||
private final TimelineStateHolder mTimelineStateHolder;
|
||||
|
||||
TimelineEventSaver(@NonNull final IMXStore store,
|
||||
@NonNull final Room room,
|
||||
@NonNull final TimelineStateHolder timelineStateHolder) {
|
||||
mStore = store;
|
||||
mRoom = room;
|
||||
mTimelineStateHolder = timelineStateHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* * Store a live room event.
|
||||
*
|
||||
* @param event the event to be stored.
|
||||
*/
|
||||
|
||||
public void storeEvent(@NonNull final Event event) {
|
||||
final MXDataHandler dataHandler = mRoom.getDataHandler();
|
||||
final String myUserId = dataHandler.getCredentials().userId;
|
||||
|
||||
// create dummy read receipt for any incoming event
|
||||
// to avoid not synchronized read receipt and event
|
||||
if (event.getSender() != null && event.eventId != null) {
|
||||
mRoom.handleReceiptData(new ReceiptData(event.getSender(), event.eventId, event.originServerTs));
|
||||
}
|
||||
mStore.storeLiveRoomEvent(event);
|
||||
if (RoomSummary.isSupportedEvent(event)) {
|
||||
final RoomState roomState = mTimelineStateHolder.getState();
|
||||
RoomSummary summary = mStore.getSummary(event.roomId);
|
||||
if (summary == null) {
|
||||
summary = new RoomSummary(summary, event, roomState, myUserId);
|
||||
} else {
|
||||
summary.setLatestReceivedEvent(event, roomState);
|
||||
}
|
||||
mStore.storeSummary(summary);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data.timeline;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.data.Room;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.sync.InvitedRoomSync;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* This class is responsible for handling the invitation room events from the SyncResponse
|
||||
*/
|
||||
class TimelineInvitedRoomSyncHandler {
|
||||
|
||||
private final Room mRoom;
|
||||
private final TimelineLiveEventHandler mLiveEventHandler;
|
||||
private final InvitedRoomSync mInvitedRoomSync;
|
||||
|
||||
TimelineInvitedRoomSyncHandler(@NonNull final Room room,
|
||||
@NonNull final TimelineLiveEventHandler liveEventHandler,
|
||||
@Nullable final InvitedRoomSync invitedRoomSync) {
|
||||
mRoom = room;
|
||||
mLiveEventHandler = liveEventHandler;
|
||||
mInvitedRoomSync = invitedRoomSync;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the invitation room events
|
||||
*/
|
||||
public void handle() {
|
||||
// Handle the state events as live events (the room state will be updated, and the listeners (if any) will be notified).
|
||||
if (mInvitedRoomSync != null && mInvitedRoomSync.inviteState != null && mInvitedRoomSync.inviteState.events != null) {
|
||||
final String roomId = mRoom.getRoomId();
|
||||
|
||||
for (Event event : mInvitedRoomSync.inviteState.events) {
|
||||
// Add a fake event id if none in order to be able to store the event
|
||||
if (event.eventId == null) {
|
||||
event.eventId = roomId + "-" + System.currentTimeMillis() + "-" + event.hashCode();
|
||||
}
|
||||
|
||||
// The roomId is not defined.
|
||||
event.roomId = roomId;
|
||||
mLiveEventHandler.handleLiveEvent(event, false, true);
|
||||
}
|
||||
// The room related to the pending invite can be considered as ready from now
|
||||
mRoom.setReadyState(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,298 @@
|
||||
package im.vector.matrix.android.internal.legacy.data.timeline;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.MXDataHandler;
|
||||
import im.vector.matrix.android.internal.legacy.data.Room;
|
||||
import im.vector.matrix.android.internal.legacy.data.RoomState;
|
||||
import im.vector.matrix.android.internal.legacy.data.RoomSummary;
|
||||
import im.vector.matrix.android.internal.legacy.data.store.IMXStore;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.RoomMember;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.sync.RoomSync;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This class is responsible for handling a Join RoomSync
|
||||
*/
|
||||
class TimelineJoinRoomSyncHandler {
|
||||
|
||||
private static final String LOG_TAG = TimelineJoinRoomSyncHandler.class.getSimpleName();
|
||||
|
||||
private final MXEventTimeline mEventTimeline;
|
||||
private final RoomSync mRoomSync;
|
||||
private final TimelineStateHolder mTimelineStateHolder;
|
||||
private final TimelineLiveEventHandler mTimelineLiveEventHandler;
|
||||
private final boolean mIsGlobalInitialSync;
|
||||
|
||||
TimelineJoinRoomSyncHandler(@NonNull final MXEventTimeline eventTimeline,
|
||||
@NonNull final RoomSync roomSync,
|
||||
@NonNull final TimelineStateHolder timelineStateHolder,
|
||||
@NonNull final TimelineLiveEventHandler timelineLiveEventHandler,
|
||||
final boolean isGlobalInitialSync) {
|
||||
mEventTimeline = eventTimeline;
|
||||
mRoomSync = roomSync;
|
||||
mTimelineStateHolder = timelineStateHolder;
|
||||
mTimelineLiveEventHandler = timelineLiveEventHandler;
|
||||
mIsGlobalInitialSync = isGlobalInitialSync;
|
||||
}
|
||||
|
||||
|
||||
public void handle() {
|
||||
final IMXStore store = mEventTimeline.getStore();
|
||||
final Room room = mEventTimeline.getRoom();
|
||||
final MXDataHandler dataHandler = room.getDataHandler();
|
||||
final String roomId = room.getRoomId();
|
||||
final String myUserId = dataHandler.getMyUser().user_id;
|
||||
final RoomMember selfMember = mTimelineStateHolder.getState().getMember(myUserId);
|
||||
final RoomSummary currentSummary = store.getSummary(roomId);
|
||||
|
||||
final String membership = selfMember != null ? selfMember.membership : null;
|
||||
final boolean isRoomInitialSync = membership == null || TextUtils.equals(membership, RoomMember.MEMBERSHIP_INVITE);
|
||||
|
||||
// Check whether the room was pending on an invitation.
|
||||
if (RoomMember.MEMBERSHIP_INVITE.equals(membership)) {
|
||||
// Reset the storage of this room. An initial sync of the room will be done with the provided 'roomSync'.
|
||||
cleanInvitedRoom(store, roomId);
|
||||
}
|
||||
if (mRoomSync.state != null && mRoomSync.state.events != null && mRoomSync.state.events.size() > 0) {
|
||||
handleRoomSyncState(room, store, isRoomInitialSync);
|
||||
}
|
||||
// Handle now timeline.events, the room state is updated during this step too (Note: timeline events are in chronological order)
|
||||
if (mRoomSync.timeline != null) {
|
||||
handleRoomSyncTimeline(store, myUserId, roomId, currentSummary, isRoomInitialSync);
|
||||
}
|
||||
if (isRoomInitialSync) {
|
||||
// any request history can be triggered by now.
|
||||
room.setReadyState(true);
|
||||
} else if (mRoomSync.timeline != null && mRoomSync.timeline.limited) {
|
||||
// Finalize initial sync
|
||||
// The room has been synced with a limited timeline
|
||||
dataHandler.onRoomFlush(roomId);
|
||||
}
|
||||
// the EventTimeLine is used when displaying a room preview
|
||||
// so, the following items should only be called when it is a live one.
|
||||
if (mEventTimeline.isLiveTimeline()) {
|
||||
handleLiveTimeline(dataHandler, store, roomId, myUserId, currentSummary);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRoomSyncState(@NonNull final Room room,
|
||||
@NonNull final IMXStore store,
|
||||
final boolean isRoomInitialSync) {
|
||||
if (isRoomInitialSync) {
|
||||
Log.d(LOG_TAG, "##" + mRoomSync.state.events.size() + " events "
|
||||
+ "for room " + room.getRoomId()
|
||||
+ "in store " + store
|
||||
);
|
||||
}
|
||||
|
||||
// Build/Update first the room state corresponding to the 'start' of the timeline.
|
||||
// Note: We consider it is not required to clone the existing room state here, because no notification is posted for these events.
|
||||
if (room.getDataHandler().isAlive()) {
|
||||
for (Event event : mRoomSync.state.events) {
|
||||
try {
|
||||
mTimelineStateHolder.processStateEvent(event, EventTimeline.Direction.FORWARDS);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "processStateEvent failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
room.setReadyState(true);
|
||||
} else {
|
||||
Log.e(LOG_TAG, "## mDataHandler.isAlive() is false");
|
||||
}
|
||||
// if it is an initial sync, the live state is initialized here
|
||||
// so the back state must also be initialized
|
||||
if (isRoomInitialSync) {
|
||||
final RoomState state = mTimelineStateHolder.getState();
|
||||
Log.d(LOG_TAG, "## handleJoinedRoomSync() : retrieve X " + state.getLoadedMembers().size() + " members for room " + room.getRoomId());
|
||||
mTimelineStateHolder.setBackState(state.deepCopy());
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanInvitedRoom(@NonNull final IMXStore store,
|
||||
@NonNull final String roomId) {
|
||||
Log.d(LOG_TAG, "clean invited room from the store " + roomId);
|
||||
store.deleteRoomData(roomId);
|
||||
mTimelineStateHolder.clear();
|
||||
}
|
||||
|
||||
private void handleRoomSyncTimeline(@NonNull final IMXStore store,
|
||||
@NonNull final String myUserId,
|
||||
@NonNull final String roomId,
|
||||
@Nullable final RoomSummary currentSummary,
|
||||
final boolean isRoomInitialSync) {
|
||||
if (mRoomSync.timeline.limited) {
|
||||
if (!isRoomInitialSync) {
|
||||
final RoomState state = mTimelineStateHolder.getState();
|
||||
// There is a gap between known events and received events in this incremental sync.
|
||||
// define a summary if some messages are left
|
||||
// the unsent messages are often displayed messages.
|
||||
final Event oldestEvent = store.getOldestEvent(roomId);
|
||||
// Flush the existing messages for this room by keeping state events.
|
||||
store.deleteAllRoomMessages(roomId, true);
|
||||
if (oldestEvent != null) {
|
||||
if (RoomSummary.isSupportedEvent(oldestEvent)) {
|
||||
if (currentSummary != null) {
|
||||
currentSummary.setLatestReceivedEvent(oldestEvent, state);
|
||||
store.storeSummary(currentSummary);
|
||||
} else {
|
||||
store.storeSummary(new RoomSummary(null, oldestEvent, state, myUserId));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Force a fetch of the loaded members the next time they will be requested
|
||||
state.forceMembersRequest();
|
||||
}
|
||||
|
||||
// if the prev batch is set to null
|
||||
// it implies there is no more data on server side.
|
||||
if (mRoomSync.timeline.prevBatch == null) {
|
||||
mRoomSync.timeline.prevBatch = Event.PAGINATE_BACK_TOKEN_END;
|
||||
}
|
||||
|
||||
// In case of limited timeline, update token where to start back pagination
|
||||
store.storeBackToken(roomId, mRoomSync.timeline.prevBatch);
|
||||
// reset the state back token
|
||||
// because it does not make anymore sense
|
||||
// by setting at null, the events cache will be cleared when a requesthistory will be called
|
||||
mTimelineStateHolder.getBackState().setToken(null);
|
||||
// reset the back paginate lock
|
||||
mEventTimeline.setCanBackPaginate(true);
|
||||
}
|
||||
|
||||
// any event ?
|
||||
if (mRoomSync.timeline.events != null && !mRoomSync.timeline.events.isEmpty()) {
|
||||
final List<Event> events = mRoomSync.timeline.events;
|
||||
// save the back token
|
||||
events.get(0).mToken = mRoomSync.timeline.prevBatch;
|
||||
|
||||
// Here the events are handled in forward direction (see [handleLiveEvent:]).
|
||||
// They will be added at the end of the stored events, so we keep the chronological order.
|
||||
for (Event event : events) {
|
||||
// the roomId is not defined.
|
||||
event.roomId = roomId;
|
||||
try {
|
||||
boolean isLimited = mRoomSync.timeline != null && mRoomSync.timeline.limited;
|
||||
|
||||
// digest the forward event
|
||||
mTimelineLiveEventHandler.handleLiveEvent(event, !isLimited && !mIsGlobalInitialSync, !mIsGlobalInitialSync && !isRoomInitialSync);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "timeline event failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleLiveTimeline(@NonNull final MXDataHandler dataHandler,
|
||||
@NonNull final IMXStore store,
|
||||
@NonNull final String roomId,
|
||||
@NonNull final String myUserId,
|
||||
@Nullable final RoomSummary currentSummary) {
|
||||
final RoomState state = mTimelineStateHolder.getState();
|
||||
// check if the summary is defined
|
||||
// after a sync, the room summary might not be defined because the latest message did not generate a room summary/
|
||||
if (null != store.getRoom(roomId)) {
|
||||
RoomSummary summary = store.getSummary(roomId);
|
||||
// if there is no defined summary
|
||||
// we have to create a new one
|
||||
if (summary == null) {
|
||||
// define a summary if some messages are left
|
||||
// the unsent messages are often displayed messages.
|
||||
final Event oldestEvent = store.getOldestEvent(roomId);
|
||||
|
||||
// if there is an oldest event, use it to set a summary
|
||||
if (oldestEvent != null) {
|
||||
// always defined a room summary else the room won't be displayed in the recents
|
||||
store.storeSummary(new RoomSummary(null, oldestEvent, state, myUserId));
|
||||
store.commit();
|
||||
|
||||
// if the event is not displayable
|
||||
// back paginate until to find a valid one
|
||||
if (!RoomSummary.isSupportedEvent(oldestEvent)) {
|
||||
Log.e(LOG_TAG, "the room " + roomId + " has no valid summary, back paginate once to find a valid one");
|
||||
}
|
||||
}
|
||||
// use the latest known event
|
||||
else if (currentSummary != null) {
|
||||
currentSummary.setLatestReceivedEvent(currentSummary.getLatestReceivedEvent(), state);
|
||||
store.storeSummary(currentSummary);
|
||||
store.commit();
|
||||
}
|
||||
// try to build a summary from the state events
|
||||
else if (mRoomSync.state != null && mRoomSync.state.events != null && mRoomSync.state.events.size() > 0) {
|
||||
final List<Event> events = new ArrayList<>(mRoomSync.state.events);
|
||||
Collections.reverse(events);
|
||||
|
||||
for (Event event : events) {
|
||||
event.roomId = roomId;
|
||||
if (RoomSummary.isSupportedEvent(event)) {
|
||||
summary = new RoomSummary(store.getSummary(roomId), event, state, myUserId);
|
||||
store.storeSummary(summary);
|
||||
store.commit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null != mRoomSync.unreadNotifications) {
|
||||
int notifCount = 0;
|
||||
int highlightCount = 0;
|
||||
|
||||
if (null != mRoomSync.unreadNotifications.highlightCount) {
|
||||
highlightCount = mRoomSync.unreadNotifications.highlightCount;
|
||||
}
|
||||
|
||||
if (null != mRoomSync.unreadNotifications.notificationCount) {
|
||||
notifCount = mRoomSync.unreadNotifications.notificationCount;
|
||||
}
|
||||
|
||||
if (notifCount != state.getNotificationCount() || state.getHighlightCount() != highlightCount) {
|
||||
Log.d(LOG_TAG, "## handleJoinedRoomSync() : update room state notifs count for room id " + roomId
|
||||
+ ": highlightCount " + highlightCount + " - notifCount " + notifCount);
|
||||
|
||||
state.setNotificationCount(notifCount);
|
||||
state.setHighlightCount(highlightCount);
|
||||
store.storeLiveStateForRoom(roomId);
|
||||
dataHandler.onNotificationCountUpdate(roomId);
|
||||
}
|
||||
|
||||
// some users reported that the summary notification counts were sometimes invalid
|
||||
// so check roomstates and summaries separately
|
||||
final RoomSummary summary = store.getSummary(roomId);
|
||||
if (summary != null && (notifCount != summary.getNotificationCount() || summary.getHighlightCount() != highlightCount)) {
|
||||
Log.d(LOG_TAG, "## handleJoinedRoomSync() : update room summary notifs count for room id " + roomId
|
||||
+ ": highlightCount " + highlightCount + " - notifCount " + notifCount);
|
||||
|
||||
summary.setNotificationCount(notifCount);
|
||||
summary.setHighlightCount(highlightCount);
|
||||
store.flushSummary(summary);
|
||||
dataHandler.onNotificationCountUpdate(roomId);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO LazyLoading, maybe this should be done earlier, because nb of members can be usefull in the instruction above.
|
||||
if (mRoomSync.roomSyncSummary != null) {
|
||||
RoomSummary summary = store.getSummary(roomId);
|
||||
|
||||
if (summary == null) {
|
||||
// Should never happen here
|
||||
Log.e(LOG_TAG, "!!!!!!!!!!!!!!!!!!!!! RoomSummary is null !!!!!!!!!!!!!!!!!!!!!");
|
||||
} else {
|
||||
summary.setRoomSyncSummary(mRoomSync.roomSyncSummary);
|
||||
|
||||
store.flushSummary(summary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,285 @@
|
||||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data.timeline;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.MXDataHandler;
|
||||
import im.vector.matrix.android.internal.legacy.data.MyUser;
|
||||
import im.vector.matrix.android.internal.legacy.data.Room;
|
||||
import im.vector.matrix.android.internal.legacy.data.RoomState;
|
||||
import im.vector.matrix.android.internal.legacy.data.RoomSummary;
|
||||
import im.vector.matrix.android.internal.legacy.data.store.IMXStore;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.EventContent;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.RoomMember;
|
||||
import im.vector.matrix.android.internal.legacy.util.EventDisplay;
|
||||
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* This class is responsible for handling live event
|
||||
*/
|
||||
class TimelineLiveEventHandler {
|
||||
|
||||
private static final String LOG_TAG = TimelineLiveEventHandler.class.getSimpleName();
|
||||
|
||||
private final MXEventTimeline mEventTimeline;
|
||||
private final TimelineEventSaver mTimelineEventSaver;
|
||||
private final StateEventRedactionChecker mStateEventRedactionChecker;
|
||||
private final TimelinePushWorker mTimelinePushWorker;
|
||||
private final TimelineStateHolder mTimelineStateHolder;
|
||||
private final TimelineEventListeners mEventListeners;
|
||||
|
||||
TimelineLiveEventHandler(@Nonnull final MXEventTimeline eventTimeline,
|
||||
@Nonnull final TimelineEventSaver timelineEventSaver,
|
||||
@Nonnull final StateEventRedactionChecker stateEventRedactionChecker,
|
||||
@Nonnull final TimelinePushWorker timelinePushWorker,
|
||||
@NonNull final TimelineStateHolder timelineStateHolder,
|
||||
@NonNull final TimelineEventListeners eventListeners) {
|
||||
mEventTimeline = eventTimeline;
|
||||
mTimelineEventSaver = timelineEventSaver;
|
||||
mStateEventRedactionChecker = stateEventRedactionChecker;
|
||||
mTimelinePushWorker = timelinePushWorker;
|
||||
mTimelineStateHolder = timelineStateHolder;
|
||||
mEventListeners = eventListeners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle events coming down from the event stream.
|
||||
*
|
||||
* @param event the live event
|
||||
* @param checkRedactedStateEvent set to true to check if it triggers a state event redaction
|
||||
* @param withPush set to true to trigger pushes when it is required
|
||||
*/
|
||||
public void handleLiveEvent(@NonNull final Event event,
|
||||
final boolean checkRedactedStateEvent,
|
||||
final boolean withPush) {
|
||||
final IMXStore store = mEventTimeline.getStore();
|
||||
final Room room = mEventTimeline.getRoom();
|
||||
final MXDataHandler dataHandler = room.getDataHandler();
|
||||
final String timelineId = mEventTimeline.getTimelineId();
|
||||
final MyUser myUser = dataHandler.getMyUser();
|
||||
|
||||
// Decrypt event if necessary
|
||||
dataHandler.decryptEvent(event, timelineId);
|
||||
|
||||
// dispatch the call events to the calls manager
|
||||
if (event.isCallEvent()) {
|
||||
final RoomState roomState = mTimelineStateHolder.getState();
|
||||
dataHandler.getCallsManager().handleCallEvent(store, event);
|
||||
storeLiveRoomEvent(dataHandler, store, event, false);
|
||||
// the candidates events are not tracked
|
||||
// because the users don't need to see the peer exchanges.
|
||||
if (!TextUtils.equals(event.getType(), Event.EVENT_TYPE_CALL_CANDIDATES)) {
|
||||
// warn the listeners
|
||||
// general listeners
|
||||
dataHandler.onLiveEvent(event, roomState);
|
||||
// timeline listeners
|
||||
mEventListeners.onEvent(event, EventTimeline.Direction.FORWARDS, roomState);
|
||||
}
|
||||
|
||||
// trigger pushes when it is required
|
||||
if (withPush) {
|
||||
mTimelinePushWorker.triggerPush(roomState, event);
|
||||
}
|
||||
|
||||
} else {
|
||||
final Event storedEvent = store.getEvent(event.eventId, event.roomId);
|
||||
|
||||
// avoid processing event twice
|
||||
if (storedEvent != null) {
|
||||
// an event has been echoed
|
||||
if (storedEvent.getAge() == Event.DUMMY_EVENT_AGE) {
|
||||
store.deleteEvent(storedEvent);
|
||||
store.storeLiveRoomEvent(event);
|
||||
store.commit();
|
||||
Log.d(LOG_TAG, "handleLiveEvent : the event " + event.eventId + " in " + event.roomId + " has been echoed");
|
||||
} else {
|
||||
Log.d(LOG_TAG, "handleLiveEvent : the event " + event.eventId + " in " + event.roomId + " already exist.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Room event
|
||||
if (event.roomId != null) {
|
||||
// check if the room has been joined
|
||||
// the initial sync + the first requestHistory call is done here
|
||||
// instead of being done in the application
|
||||
if (Event.EVENT_TYPE_STATE_ROOM_MEMBER.equals(event.getType()) && TextUtils.equals(event.getSender(), dataHandler.getUserId())) {
|
||||
EventContent eventContent = JsonUtils.toEventContent(event.getContentAsJsonObject());
|
||||
EventContent prevEventContent = event.getPrevContent();
|
||||
|
||||
String prevMembership = null;
|
||||
|
||||
if (prevEventContent != null) {
|
||||
prevMembership = prevEventContent.membership;
|
||||
}
|
||||
|
||||
// if the membership keeps the same value "join".
|
||||
// it should mean that the user profile has been updated.
|
||||
if (!event.isRedacted() && TextUtils.equals(prevMembership, eventContent.membership)
|
||||
&& TextUtils.equals(RoomMember.MEMBERSHIP_JOIN, eventContent.membership)) {
|
||||
// check if the user updates his profile from another device.
|
||||
|
||||
boolean hasAccountInfoUpdated = false;
|
||||
|
||||
if (!TextUtils.equals(eventContent.displayname, myUser.displayname)) {
|
||||
hasAccountInfoUpdated = true;
|
||||
myUser.displayname = eventContent.displayname;
|
||||
store.setDisplayName(myUser.displayname, event.getOriginServerTs());
|
||||
}
|
||||
|
||||
if (!TextUtils.equals(eventContent.avatar_url, myUser.getAvatarUrl())) {
|
||||
hasAccountInfoUpdated = true;
|
||||
myUser.setAvatarUrl(eventContent.avatar_url);
|
||||
store.setAvatarURL(myUser.avatar_url, event.getOriginServerTs());
|
||||
}
|
||||
|
||||
if (hasAccountInfoUpdated) {
|
||||
dataHandler.onAccountInfoUpdate(myUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final RoomState previousState = mTimelineStateHolder.getState();
|
||||
if (event.stateKey != null) {
|
||||
// copy the live state before applying any update
|
||||
mTimelineStateHolder.deepCopyState(EventTimeline.Direction.FORWARDS);
|
||||
// check if the event has been processed
|
||||
if (!mTimelineStateHolder.processStateEvent(event, EventTimeline.Direction.FORWARDS)) {
|
||||
// not processed -> do not warn the application
|
||||
// assume that the event is a duplicated one.
|
||||
return;
|
||||
}
|
||||
}
|
||||
storeLiveRoomEvent(dataHandler, store, event, checkRedactedStateEvent);
|
||||
|
||||
// warn the listeners
|
||||
// general listeners
|
||||
dataHandler.onLiveEvent(event, previousState);
|
||||
|
||||
// timeline listeners
|
||||
mEventListeners.onEvent(event, EventTimeline.Direction.FORWARDS, previousState);
|
||||
|
||||
// trigger pushes when it is required
|
||||
if (withPush) {
|
||||
mTimelinePushWorker.triggerPush(mTimelineStateHolder.getState(), event);
|
||||
}
|
||||
} else {
|
||||
Log.e(LOG_TAG, "Unknown live event type: " + event.getType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a live room event.
|
||||
*
|
||||
* @param event The event to be stored.
|
||||
* @param checkRedactedStateEvent true to check if this event redacts a state event
|
||||
*/
|
||||
private void storeLiveRoomEvent(@NonNull final MXDataHandler dataHandler,
|
||||
@NonNull final IMXStore store,
|
||||
@NonNull Event event,
|
||||
final boolean checkRedactedStateEvent) {
|
||||
boolean shouldBeSaved = false;
|
||||
String myUserId = dataHandler.getCredentials().userId;
|
||||
|
||||
if (Event.EVENT_TYPE_REDACTION.equals(event.getType())) {
|
||||
if (event.getRedactedEventId() != null) {
|
||||
Event eventToPrune = store.getEvent(event.getRedactedEventId(), event.roomId);
|
||||
|
||||
// when an event is redacted, some fields must be kept.
|
||||
if (eventToPrune != null) {
|
||||
shouldBeSaved = true;
|
||||
// remove expected keys
|
||||
eventToPrune.prune(event);
|
||||
// store the prune event
|
||||
mTimelineEventSaver.storeEvent(eventToPrune);
|
||||
// store the redaction event too (for the read markers management)
|
||||
mTimelineEventSaver.storeEvent(event);
|
||||
// the redaction check must not be done during an initial sync
|
||||
// or the redacted event is received with roomSync.timeline.limited
|
||||
if (checkRedactedStateEvent && eventToPrune.stateKey != null) {
|
||||
mStateEventRedactionChecker.checkStateEventRedaction(event);
|
||||
}
|
||||
// search the latest displayable event
|
||||
// to replace the summary text
|
||||
final List<Event> events = new ArrayList<>(store.getRoomMessages(event.roomId));
|
||||
for (int index = events.size() - 1; index >= 0; index--) {
|
||||
final Event indexedEvent = events.get(index);
|
||||
if (RoomSummary.isSupportedEvent(indexedEvent)) {
|
||||
// Decrypt event if necessary
|
||||
if (TextUtils.equals(indexedEvent.getType(), Event.EVENT_TYPE_MESSAGE_ENCRYPTED)) {
|
||||
if (null != dataHandler.getCrypto()) {
|
||||
dataHandler.decryptEvent(indexedEvent, mEventTimeline.getTimelineId());
|
||||
}
|
||||
}
|
||||
final RoomState state = mTimelineStateHolder.getState();
|
||||
final EventDisplay eventDisplay = new EventDisplay(store.getContext(), indexedEvent, state);
|
||||
// ensure that message can be displayed
|
||||
if (!TextUtils.isEmpty(eventDisplay.getTextualDisplay())) {
|
||||
event = indexedEvent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} else if (checkRedactedStateEvent) {
|
||||
// the redaction check must not be done during an initial sync
|
||||
// or the redacted event is received with roomSync.timeline.limited
|
||||
mStateEventRedactionChecker.checkStateEventRedaction(event);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// the candidate events are not stored.
|
||||
shouldBeSaved = !event.isCallEvent() || !Event.EVENT_TYPE_CALL_CANDIDATES.equals(event.getType());
|
||||
// thread issue
|
||||
// if the user leaves a room,
|
||||
if (Event.EVENT_TYPE_STATE_ROOM_MEMBER.equals(event.getType()) && myUserId.equals(event.stateKey)) {
|
||||
final String membership = event.getContentAsJsonObject().getAsJsonPrimitive("membership").getAsString();
|
||||
if (RoomMember.MEMBERSHIP_LEAVE.equals(membership) || RoomMember.MEMBERSHIP_BAN.equals(membership)) {
|
||||
shouldBeSaved = mEventTimeline.isHistorical();
|
||||
// delete the room and warn the listener of the leave event only at the end of the events chunk processing
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldBeSaved) {
|
||||
mTimelineEventSaver.storeEvent(event);
|
||||
}
|
||||
// warn the listener that a new room has been created
|
||||
if (Event.EVENT_TYPE_STATE_ROOM_CREATE.equals(event.getType())) {
|
||||
dataHandler.onNewRoom(event.roomId);
|
||||
}
|
||||
// warn the listeners that a room has been joined
|
||||
if (Event.EVENT_TYPE_STATE_ROOM_MEMBER.equals(event.getType()) && myUserId.equals(event.stateKey)) {
|
||||
final String membership = event.getContentAsJsonObject().getAsJsonPrimitive("membership").getAsString();
|
||||
if (RoomMember.MEMBERSHIP_JOIN.equals(membership)) {
|
||||
dataHandler.onJoinRoom(event.roomId);
|
||||
} else if (RoomMember.MEMBERSHIP_INVITE.equals(membership)) {
|
||||
dataHandler.onNewRoom(event.roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data.timeline;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.MXDataHandler;
|
||||
import im.vector.matrix.android.internal.legacy.call.MXCall;
|
||||
import im.vector.matrix.android.internal.legacy.data.RoomState;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.bingrules.BingRule;
|
||||
import im.vector.matrix.android.internal.legacy.util.BingRulesManager;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
/**
|
||||
* This class is responsible for handling push rules for an event
|
||||
*/
|
||||
class TimelinePushWorker {
|
||||
|
||||
private static final String LOG_TAG = TimelinePushWorker.class.getSimpleName();
|
||||
|
||||
private final MXDataHandler mDataHandler;
|
||||
|
||||
TimelinePushWorker(@NonNull final MXDataHandler dataHandler) {
|
||||
mDataHandler = dataHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a push if there is a dedicated push rules which implies it.
|
||||
*
|
||||
* @param event the event
|
||||
*/
|
||||
public void triggerPush(@NonNull final RoomState state,
|
||||
@NonNull final Event event) {
|
||||
BingRule bingRule;
|
||||
boolean outOfTimeEvent = false;
|
||||
long maxLifetime = 0;
|
||||
long eventLifetime = 0;
|
||||
final JsonObject eventContent = event.getContentAsJsonObject();
|
||||
if (eventContent != null && eventContent.has("lifetime")) {
|
||||
maxLifetime = eventContent.get("lifetime").getAsLong();
|
||||
eventLifetime = System.currentTimeMillis() - event.getOriginServerTs();
|
||||
outOfTimeEvent = eventLifetime > maxLifetime;
|
||||
}
|
||||
final BingRulesManager bingRulesManager = mDataHandler.getBingRulesManager();
|
||||
// If the bing rules apply, bing
|
||||
if (!outOfTimeEvent
|
||||
&& bingRulesManager != null
|
||||
&& (bingRule = bingRulesManager.fulfilledBingRule(event)) != null) {
|
||||
|
||||
if (bingRule.shouldNotify()) {
|
||||
// bing the call events only if they make sense
|
||||
if (Event.EVENT_TYPE_CALL_INVITE.equals(event.getType())) {
|
||||
long lifeTime = event.getAge();
|
||||
if (Long.MAX_VALUE == lifeTime) {
|
||||
lifeTime = System.currentTimeMillis() - event.getOriginServerTs();
|
||||
}
|
||||
if (lifeTime > MXCall.CALL_TIMEOUT_MS) {
|
||||
Log.d(LOG_TAG, "IGNORED onBingEvent rule id " + bingRule.ruleId + " event id " + event.eventId
|
||||
+ " in " + event.roomId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Log.d(LOG_TAG, "onBingEvent rule id " + bingRule.ruleId + " event id " + event.eventId + " in " + event.roomId);
|
||||
mDataHandler.onBingEvent(event, state, bingRule);
|
||||
} else {
|
||||
Log.d(LOG_TAG, "rule id " + bingRule.ruleId + " event id " + event.eventId
|
||||
+ " in " + event.roomId + " has a mute notify rule");
|
||||
}
|
||||
} else if (outOfTimeEvent) {
|
||||
Log.e(LOG_TAG, "outOfTimeEvent for " + event.eventId + " in " + event.roomId);
|
||||
Log.e(LOG_TAG, "outOfTimeEvent maxlifetime " + maxLifetime + " eventLifeTime " + eventLifetime);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.data.timeline;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.MXDataHandler;
|
||||
import im.vector.matrix.android.internal.legacy.data.RoomState;
|
||||
import im.vector.matrix.android.internal.legacy.data.store.IMXStore;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.Event;
|
||||
|
||||
/**
|
||||
* This class is responsible for holding the state and backState of a room timeline
|
||||
*/
|
||||
class TimelineStateHolder {
|
||||
|
||||
private final MXDataHandler mDataHandler;
|
||||
private final IMXStore mStore;
|
||||
private String mRoomId;
|
||||
|
||||
/**
|
||||
* The state of the room at the top most recent event of the timeline.
|
||||
*/
|
||||
private RoomState mState;
|
||||
|
||||
/**
|
||||
* The historical state of the room when paginating back.
|
||||
*/
|
||||
private RoomState mBackState;
|
||||
|
||||
TimelineStateHolder(@NonNull final MXDataHandler dataHandler,
|
||||
@NonNull final IMXStore store,
|
||||
@NonNull final String roomId) {
|
||||
mDataHandler = dataHandler;
|
||||
mStore = store;
|
||||
mRoomId = roomId;
|
||||
initStates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the states
|
||||
*/
|
||||
public void clear() {
|
||||
initStates();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The state of the room at the top most recent event of the timeline.
|
||||
*/
|
||||
@NonNull
|
||||
public RoomState getState() {
|
||||
return mState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the state.
|
||||
*
|
||||
* @param state the new state.
|
||||
*/
|
||||
public void setState(@NonNull final RoomState state) {
|
||||
mState = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the backState.
|
||||
*/
|
||||
@NonNull
|
||||
public RoomState getBackState() {
|
||||
return mBackState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the backState.
|
||||
*
|
||||
* @param state the new backState.
|
||||
*/
|
||||
public void setBackState(@NonNull final RoomState state) {
|
||||
mBackState = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a deep copy or the dedicated state.
|
||||
*
|
||||
* @param direction the room state direction to deep copy.
|
||||
*/
|
||||
public void deepCopyState(final EventTimeline.Direction direction) {
|
||||
if (direction == EventTimeline.Direction.FORWARDS) {
|
||||
mState = mState.deepCopy();
|
||||
} else {
|
||||
mBackState = mBackState.deepCopy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a state event to keep the internal live and back states up to date.
|
||||
*
|
||||
* @param event the state event
|
||||
* @param direction the direction; ie. forwards for live state, backwards for back state
|
||||
* @return true if the event has been processed.
|
||||
*/
|
||||
public boolean processStateEvent(@NonNull final Event event,
|
||||
@NonNull final EventTimeline.Direction direction) {
|
||||
final RoomState affectedState = direction == EventTimeline.Direction.FORWARDS ? mState : mBackState;
|
||||
final boolean isProcessed = affectedState.applyState(mStore, event, direction);
|
||||
if (isProcessed && direction == EventTimeline.Direction.FORWARDS) {
|
||||
mStore.storeLiveStateForRoom(mRoomId);
|
||||
}
|
||||
return isProcessed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the room Id
|
||||
*
|
||||
* @param roomId the new room id.
|
||||
*/
|
||||
public void setRoomId(@NonNull final String roomId) {
|
||||
mRoomId = roomId;
|
||||
mState.roomId = roomId;
|
||||
mBackState.roomId = roomId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the state and backState to default, with roomId and dataHandler
|
||||
*/
|
||||
private void initStates() {
|
||||
mBackState = new RoomState();
|
||||
mBackState.setDataHandler(mDataHandler);
|
||||
mBackState.roomId = mRoomId;
|
||||
mState = new RoomState();
|
||||
mState.setDataHandler(mDataHandler);
|
||||
mState.roomId = mRoomId;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.db;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import im.vector.matrix.android.internal.legacy.util.ContentUtils;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class MXLatestChatMessageCache {
|
||||
private static final String LOG_TAG = MXLatestChatMessageCache.class.getSimpleName();
|
||||
private static final String FILENAME = "ConsoleLatestChatMessageCache";
|
||||
|
||||
final String MXLATESTMESSAGES_STORE_FOLDER = "MXLatestMessagesStore";
|
||||
|
||||
private Map<String, String> mLatestMesssageByRoomId = null;
|
||||
private String mUserId = null;
|
||||
private File mLatestMessagesDirectory = null;
|
||||
private File mLatestMessagesFile = null;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param userId the user id
|
||||
*/
|
||||
public MXLatestChatMessageCache(String userId) {
|
||||
mUserId = userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the text caches.
|
||||
*
|
||||
* @param context The application context to use.
|
||||
*/
|
||||
public void clearCache(Context context) {
|
||||
ContentUtils.deleteDirectory(mLatestMessagesDirectory);
|
||||
mLatestMesssageByRoomId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the texts cache file.
|
||||
*
|
||||
* @param context the context.
|
||||
*/
|
||||
private void openLatestMessagesDict(Context context) {
|
||||
|
||||
// already checked
|
||||
if (null != mLatestMesssageByRoomId) {
|
||||
return;
|
||||
}
|
||||
|
||||
mLatestMesssageByRoomId = new HashMap<>();
|
||||
|
||||
try {
|
||||
mLatestMessagesDirectory = new File(context.getApplicationContext().getFilesDir(), MXLATESTMESSAGES_STORE_FOLDER);
|
||||
mLatestMessagesDirectory = new File(mLatestMessagesDirectory, mUserId);
|
||||
|
||||
mLatestMessagesFile = new File(mLatestMessagesDirectory, FILENAME.hashCode() + "");
|
||||
|
||||
if (!mLatestMessagesDirectory.exists()) {
|
||||
|
||||
// create dir tree
|
||||
mLatestMessagesDirectory.mkdirs();
|
||||
|
||||
File oldFile = new File(context.getApplicationContext().getFilesDir(), FILENAME.hashCode() + "");
|
||||
|
||||
// backward compatibility
|
||||
if (oldFile.exists()) {
|
||||
oldFile.renameTo(mLatestMessagesFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (mLatestMessagesFile.exists()) {
|
||||
FileInputStream fis = new FileInputStream(mLatestMessagesFile);
|
||||
ObjectInputStream ois = new ObjectInputStream(fis);
|
||||
mLatestMesssageByRoomId = (Map) ois.readObject();
|
||||
ois.close();
|
||||
fis.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## openLatestMessagesDict failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest written text for a dedicated room.
|
||||
*
|
||||
* @param context the context.
|
||||
* @param roomId the roomId
|
||||
* @return the latest message
|
||||
*/
|
||||
public String getLatestText(Context context, String roomId) {
|
||||
if (null == mLatestMesssageByRoomId) {
|
||||
openLatestMessagesDict(context);
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(roomId)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (mLatestMesssageByRoomId.containsKey(roomId)) {
|
||||
return mLatestMesssageByRoomId.get(roomId);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the latest message dictionnary.
|
||||
*
|
||||
* @param context the context.
|
||||
*/
|
||||
private void saveLatestMessagesDict(Context context) {
|
||||
try {
|
||||
FileOutputStream fos = new FileOutputStream(mLatestMessagesFile);
|
||||
ObjectOutputStream oos = new ObjectOutputStream(fos);
|
||||
oos.writeObject(mLatestMesssageByRoomId);
|
||||
oos.close();
|
||||
fos.close();
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "## saveLatestMessagesDict() failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the latest message for a dedicated roomId.
|
||||
*
|
||||
* @param context the context.
|
||||
* @param roomId the roomId.
|
||||
* @param message the message.
|
||||
*/
|
||||
public void updateLatestMessage(Context context, String roomId, String message) {
|
||||
if (null == mLatestMesssageByRoomId) {
|
||||
openLatestMessagesDict(context);
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(message)) {
|
||||
mLatestMesssageByRoomId.remove(roomId);
|
||||
}
|
||||
|
||||
mLatestMesssageByRoomId.put(roomId, message);
|
||||
saveLatestMessagesDict(context);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,569 @@
|
||||
/*
|
||||
* Copyright 2015 OpenMarket Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.legacy.db;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import im.vector.matrix.android.internal.legacy.RestClient;
|
||||
import im.vector.matrix.android.internal.legacy.listeners.IMXMediaUploadListener;
|
||||
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.ContentResponse;
|
||||
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
|
||||
import im.vector.matrix.android.internal.legacy.ssl.CertUtil;
|
||||
import im.vector.matrix.android.internal.legacy.util.ContentManager;
|
||||
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
|
||||
import im.vector.matrix.android.internal.legacy.util.Log;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.EOFException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
/**
|
||||
* Private AsyncTask used to upload files.
|
||||
*/
|
||||
public class MXMediaUploadWorkerTask extends AsyncTask<Void, Void, String> {
|
||||
|
||||
private static final String LOG_TAG = MXMediaUploadWorkerTask.class.getSimpleName();
|
||||
|
||||
// upload ID -> task
|
||||
private static final Map<String, MXMediaUploadWorkerTask> mPendingUploadByUploadId = new HashMap<>();
|
||||
|
||||
// progress listener
|
||||
private final List<IMXMediaUploadListener> mUploadListeners = new ArrayList<>();
|
||||
|
||||
// the upload stats
|
||||
private IMXMediaUploadListener.UploadStats mUploadStats;
|
||||
|
||||
// the media mimeType
|
||||
private final String mMimeType;
|
||||
|
||||
// the media to upload
|
||||
private final InputStream mContentStream;
|
||||
|
||||
// its unique identifier
|
||||
private final String mUploadId;
|
||||
|
||||
// store the server response to provide it the listeners
|
||||
private String mResponseFromServer;
|
||||
|
||||
// tells if the current upload has been cancelled.
|
||||
private boolean mIsCancelled;
|
||||
|
||||
/**
|
||||
* Tells if the upload has been completed
|
||||
*/
|
||||
private boolean mIsDone;
|
||||
|
||||
// upload const
|
||||
private static final int UPLOAD_BUFFER_READ_SIZE = 1024 * 32;
|
||||
|
||||
// dummy ApiCallback uses to be warned when the upload must be declared as "undeliverable".
|
||||
private final ApiCallback mApiCallback = new ApiCallback() {
|
||||
@Override
|
||||
public void onSuccess(Object info) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkError(Exception e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMatrixError(MatrixError e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnexpectedError(Exception e) {
|
||||
dispatchResult(mResponseFromServer);
|
||||
}
|
||||
};
|
||||
|
||||
// the upload server HTTP response code
|
||||
private int mResponseCode = -1;
|
||||
|
||||
// the media file name
|
||||
private String mFilename;
|
||||
|
||||
// the content manager
|
||||
private final ContentManager mContentManager;
|
||||
|
||||
/**
|
||||
* Check if there is a pending download for the url.
|
||||
*
|
||||
* @param uploadId The id to check the existence
|
||||
* @return the dedicated BitmapWorkerTask if it exists.
|
||||
*/
|
||||
public static MXMediaUploadWorkerTask getMediaUploadWorkerTask(String uploadId) {
|
||||
if (uploadId != null) {
|
||||
MXMediaUploadWorkerTask task = null;
|
||||
synchronized (mPendingUploadByUploadId) {
|
||||
if (mPendingUploadByUploadId.containsKey(uploadId)) {
|
||||
task = mPendingUploadByUploadId.get(uploadId);
|
||||
}
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the pending uploads.
|
||||
*/
|
||||
public static void cancelPendingUploads() {
|
||||
Collection<MXMediaUploadWorkerTask> tasks = mPendingUploadByUploadId.values();
|
||||
|
||||
// cancels the running task
|
||||
for (MXMediaUploadWorkerTask task : tasks) {
|
||||
try {
|
||||
task.cancelUpload();
|
||||
task.cancel(true);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "cancelPendingUploads " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
mPendingUploadByUploadId.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param contentManager the content manager
|
||||
* @param contentStream the stream to upload
|
||||
* @param mimeType the mime type
|
||||
* @param uploadId the upload id
|
||||
* @param filename the dest filename
|
||||
* @param listener the upload listener
|
||||
*/
|
||||
public MXMediaUploadWorkerTask(ContentManager contentManager,
|
||||
InputStream contentStream,
|
||||
String mimeType,
|
||||
String uploadId,
|
||||
String filename,
|
||||
IMXMediaUploadListener listener) {
|
||||
if (contentStream.markSupported()) {
|
||||
try {
|
||||
contentStream.reset();
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "MXMediaUploadWorkerTask " + e.getMessage(), e);
|
||||
}
|
||||
} else {
|
||||
Log.w(LOG_TAG, "Warning, reset() is not supported for this stream");
|
||||
}
|
||||
|
||||
|
||||
mContentManager = contentManager;
|
||||
mContentStream = contentStream;
|
||||
mMimeType = mimeType;
|
||||
mUploadId = uploadId;
|
||||
mFilename = filename;
|
||||
|
||||
addListener(listener);
|
||||
|
||||
if (null != uploadId) {
|
||||
mPendingUploadByUploadId.put(uploadId, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an upload listener
|
||||
*
|
||||
* @param aListener the listener to add.
|
||||
*/
|
||||
public void addListener(IMXMediaUploadListener aListener) {
|
||||
if (null != aListener && mUploadListeners.indexOf(aListener) < 0) {
|
||||
mUploadListeners.add(aListener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the upload progress
|
||||
*/
|
||||
public int getProgress() {
|
||||
if (null != mUploadStats) {
|
||||
return mUploadStats.mProgress;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the upload stats
|
||||
*/
|
||||
public IMXMediaUploadListener.UploadStats getStats() {
|
||||
return mUploadStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the current upload has been cancelled.
|
||||
*/
|
||||
private synchronized boolean isUploadCancelled() {
|
||||
return mIsCancelled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the current upload.
|
||||
*/
|
||||
public synchronized void cancelUpload() {
|
||||
mIsCancelled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* refresh the progress info
|
||||
*/
|
||||
private void publishProgress(long startUploadTime) {
|
||||
mUploadStats.mElapsedTime = (int) ((System.currentTimeMillis() - startUploadTime) / 1000);
|
||||
|
||||
if (0 != mUploadStats.mFileSize) {
|
||||
// Uploading data is 90% of the job
|
||||
// the other 10% is the end of the connection related actions
|
||||
mUploadStats.mProgress = (int) (((long) mUploadStats.mUploadedSize) * 96 / mUploadStats.mFileSize);
|
||||
}
|
||||
|
||||
// avoid zero div
|
||||
if (System.currentTimeMillis() != startUploadTime) {
|
||||
mUploadStats.mBitRate = (int) (((long) mUploadStats.mUploadedSize) * 1000 / (System.currentTimeMillis() - startUploadTime) / 1024);
|
||||
} else {
|
||||
mUploadStats.mBitRate = 0;
|
||||
}
|
||||
|
||||
if (0 != mUploadStats.mBitRate) {
|
||||
mUploadStats.mEstimatedRemainingTime = (mUploadStats.mFileSize - mUploadStats.mUploadedSize) / 1024 / mUploadStats.mBitRate;
|
||||
} else {
|
||||
mUploadStats.mEstimatedRemainingTime = -1;
|
||||
}
|
||||
|
||||
publishProgress();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doInBackground(Void... params) {
|
||||
HttpURLConnection conn;
|
||||
DataOutputStream dos;
|
||||
|
||||
mResponseCode = -1;
|
||||
|
||||
int bytesRead, bytesAvailable;
|
||||
int totalWritten, totalSize;
|
||||
int bufferSize;
|
||||
byte[] buffer;
|
||||
|
||||
String serverResponse = null;
|
||||
|
||||
String urlString = mContentManager.getHsConfig().getHomeserverUri().toString() + ContentManager.URI_PREFIX_CONTENT_API + "upload";
|
||||
|
||||
if (null != mFilename) {
|
||||
try {
|
||||
String utf8Filename = URLEncoder.encode(mFilename, "utf-8");
|
||||
urlString += "?filename=" + utf8Filename;
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "doInBackground " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
URL url = new URL(urlString);
|
||||
|
||||
conn = (HttpURLConnection) url.openConnection();
|
||||
if (RestClient.getUserAgent() != null) {
|
||||
conn.setRequestProperty("User-Agent", RestClient.getUserAgent());
|
||||
}
|
||||
conn.setRequestProperty("Authorization", "Bearer " + mContentManager.getHsConfig().getCredentials().accessToken);
|
||||
conn.setDoInput(true);
|
||||
conn.setDoOutput(true);
|
||||
conn.setUseCaches(false);
|
||||
conn.setRequestMethod("POST");
|
||||
|
||||
if (conn instanceof HttpsURLConnection) {
|
||||
// Add SSL Socket factory.
|
||||
HttpsURLConnection sslConn = (HttpsURLConnection) conn;
|
||||
try {
|
||||
Pair<SSLSocketFactory, X509TrustManager> pair = CertUtil.newPinnedSSLSocketFactory(mContentManager.getHsConfig());
|
||||
sslConn.setSSLSocketFactory(pair.first);
|
||||
sslConn.setHostnameVerifier(CertUtil.newHostnameVerifier(mContentManager.getHsConfig()));
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "sslConn " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
conn.setRequestProperty("Content-Type", mMimeType);
|
||||
conn.setRequestProperty("Content-Length", Integer.toString(mContentStream.available()));
|
||||
// avoid caching data before really sending them.
|
||||
conn.setFixedLengthStreamingMode(mContentStream.available());
|
||||
|
||||
conn.connect();
|
||||
|
||||
dos = new DataOutputStream(conn.getOutputStream());
|
||||
|
||||
// create a buffer of maximum size
|
||||
totalSize = bytesAvailable = mContentStream.available();
|
||||
totalWritten = 0;
|
||||
bufferSize = Math.min(bytesAvailable, UPLOAD_BUFFER_READ_SIZE);
|
||||
buffer = new byte[bufferSize];
|
||||
|
||||
mUploadStats = new IMXMediaUploadListener.UploadStats();
|
||||
mUploadStats.mUploadId = mUploadId;
|
||||
mUploadStats.mProgress = 0;
|
||||
mUploadStats.mUploadedSize = 0;
|
||||
mUploadStats.mFileSize = totalSize;
|
||||
mUploadStats.mElapsedTime = 0;
|
||||
mUploadStats.mEstimatedRemainingTime = -1;
|
||||
mUploadStats.mBitRate = 0;
|
||||
|
||||
final long startUploadTime = System.currentTimeMillis();
|
||||
|
||||
Log.d(LOG_TAG, "doInBackground : start Upload (" + totalSize + " bytes)");
|
||||
|
||||
// read file and write it into form...
|
||||
bytesRead = mContentStream.read(buffer, 0, bufferSize);
|
||||
|
||||
dispatchOnUploadStart();
|
||||
|
||||
final Timer refreshTimer = new Timer();
|
||||
|
||||
// Publish progress every 100ms
|
||||
refreshTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!mIsDone) {
|
||||
publishProgress(startUploadTime);
|
||||
}
|
||||
}
|
||||
}, new Date(), 100);
|
||||
|
||||
while ((bytesRead > 0) && !isUploadCancelled()) {
|
||||
dos.write(buffer, 0, bytesRead);
|
||||
totalWritten += bytesRead;
|
||||
bytesAvailable = mContentStream.available();
|
||||
bufferSize = Math.min(bytesAvailable, UPLOAD_BUFFER_READ_SIZE);
|
||||
|
||||
Log.d(LOG_TAG, "doInBackground : totalWritten " + totalWritten + " / totalSize " + totalSize);
|
||||
mUploadStats.mUploadedSize = totalWritten;
|
||||
bytesRead = mContentStream.read(buffer, 0, bufferSize);
|
||||
}
|
||||
mIsDone = true;
|
||||
|
||||
refreshTimer.cancel();
|
||||
|
||||
if (!isUploadCancelled()) {
|
||||
mUploadStats.mProgress = 96;
|
||||
publishProgress(startUploadTime);
|
||||
dos.flush();
|
||||
mUploadStats.mProgress = 97;
|
||||
publishProgress(startUploadTime);
|
||||
dos.close();
|
||||
mUploadStats.mProgress = 98;
|
||||
publishProgress(startUploadTime);
|
||||
|
||||
try {
|
||||
// Read the SERVER RESPONSE
|
||||
mResponseCode = conn.getResponseCode();
|
||||
} catch (EOFException eofEx) {
|
||||
mResponseCode = HttpURLConnection.HTTP_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
mUploadStats.mProgress = 99;
|
||||
publishProgress(startUploadTime);
|
||||
|
||||
Log.d(LOG_TAG, "doInBackground : Upload is done with response code " + mResponseCode);
|
||||
|
||||
InputStream is;
|
||||
|
||||
if (mResponseCode == HttpURLConnection.HTTP_OK) {
|
||||
is = conn.getInputStream();
|
||||
} else {
|
||||
is = conn.getErrorStream();
|
||||
}
|
||||
|
||||
int ch;
|
||||
StringBuffer b = new StringBuffer();
|
||||
while ((ch = is.read()) != -1) {
|
||||
b.append((char) ch);
|
||||
}
|
||||
serverResponse = b.toString();
|
||||
is.close();
|
||||
|
||||
// the server should provide an error description
|
||||
if (mResponseCode != HttpURLConnection.HTTP_OK) {
|
||||
try {
|
||||
JSONObject responseJSON = new JSONObject(serverResponse);
|
||||
serverResponse = responseJSON.getString("error");
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOG_TAG, "doInBackground : Error parsing " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dos.flush();
|
||||
dos.close();
|
||||
}
|
||||
|
||||
if (null != conn) {
|
||||
conn.disconnect();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
serverResponse = e.getLocalizedMessage();
|
||||
Log.e(LOG_TAG, "doInBackground ; failed with error " + e.getClass() + " - " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
mResponseFromServer = serverResponse;
|
||||
|
||||
return serverResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProgressUpdate(Void... aVoid) {
|
||||
super.onProgressUpdate();
|
||||
|
||||
Log.d(LOG_TAG, "Upload " + this + " : " + mUploadStats.mProgress);
|
||||
|
||||
dispatchOnUploadProgress(mUploadStats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the result to the callbacks
|
||||
*
|
||||
* @param serverResponse the server response
|
||||
*/
|
||||
private void dispatchResult(final String serverResponse) {
|
||||
if (null != mUploadId) {
|
||||
mPendingUploadByUploadId.remove(mUploadId);
|
||||
}
|
||||
|
||||
mContentManager.getUnsentEventsManager().onEventSent(mApiCallback);
|
||||
|
||||
// close the source stream
|
||||
try {
|
||||
mContentStream.close();
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "dispatchResult " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
if (isUploadCancelled()) {
|
||||
dispatchOnUploadCancel();
|
||||
} else {
|
||||
ContentResponse uploadResponse = (mResponseCode != 200 || serverResponse == null) ? null : JsonUtils.toContentResponse(serverResponse);
|
||||
|
||||
if (null == uploadResponse || null == uploadResponse.contentUri) {
|
||||
dispatchOnUploadError(mResponseCode, serverResponse);
|
||||
} else {
|
||||
dispatchOnUploadComplete(uploadResponse.contentUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(final String serverResponseMessage) {
|
||||
// do not call the callback if cancelled.
|
||||
if (!isCancelled()) {
|
||||
dispatchResult(serverResponseMessage);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================================================
|
||||
// Dispatchers
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* Dispatch Upload start
|
||||
*/
|
||||
private void dispatchOnUploadStart() {
|
||||
for (IMXMediaUploadListener listener : mUploadListeners) {
|
||||
try {
|
||||
listener.onUploadStart(mUploadId);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "dispatchOnUploadStart failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch Upload start
|
||||
*
|
||||
* @param stats the upload stats
|
||||
*/
|
||||
private void dispatchOnUploadProgress(IMXMediaUploadListener.UploadStats stats) {
|
||||
for (IMXMediaUploadListener listener : mUploadListeners) {
|
||||
try {
|
||||
listener.onUploadProgress(mUploadId, stats);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "dispatchOnUploadProgress failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch Upload cancel.
|
||||
*/
|
||||
private void dispatchOnUploadCancel() {
|
||||
for (IMXMediaUploadListener listener : mUploadListeners) {
|
||||
try {
|
||||
listener.onUploadCancel(mUploadId);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "listener failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch Upload error.
|
||||
*
|
||||
* @param serverResponseCode the server response code.
|
||||
* @param serverErrorMessage the server error message
|
||||
*/
|
||||
private void dispatchOnUploadError(int serverResponseCode, String serverErrorMessage) {
|
||||
for (IMXMediaUploadListener listener : mUploadListeners) {
|
||||
try {
|
||||
listener.onUploadError(mUploadId, serverResponseCode, serverErrorMessage);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "dispatchOnUploadError failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch Upload complete.
|
||||
*
|
||||
* @param contentUri the media uri.
|
||||
*/
|
||||
private void dispatchOnUploadComplete(String contentUri) {
|
||||
for (IMXMediaUploadListener listener : mUploadListeners) {
|
||||
try {
|
||||
listener.onUploadComplete(mUploadId, contentUri);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "dispatchOnUploadComplete failed " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user