diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser index 11f62eee62..9d3f201340 100644 Binary files a/.idea/caches/build_file_checksums.ser and b/.idea/caches/build_file_checksums.ser differ diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt index 4955741e63..c3b92a8930 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt @@ -44,7 +44,6 @@ class HomeActivity : RiotActivity() { }) } - companion object { fun newIntent(context: Context): Intent { diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 2c8cb8419d..93a785c048 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -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" diff --git a/matrix-sdk-android/libs/olm-sdk.aar b/matrix-sdk-android/libs/olm-sdk.aar new file mode 100644 index 0000000000..66be8a65ac Binary files /dev/null and b/matrix-sdk-android/libs/olm-sdk.aar differ diff --git a/matrix-sdk-android/libs/react-native-webrtc.aar b/matrix-sdk-android/libs/react-native-webrtc.aar new file mode 100644 index 0000000000..ff5bb99f86 Binary files /dev/null and b/matrix-sdk-android/libs/react-native-webrtc.aar differ diff --git a/matrix-sdk-android/src/main/AndroidManifest.xml b/matrix-sdk-android/src/main/AndroidManifest.xml index 90cd94ab3e..8139d75a37 100644 --- a/matrix-sdk-android/src/main/AndroidManifest.xml +++ b/matrix-sdk-android/src/main/AndroidManifest.xml @@ -1,2 +1,5 @@ + package="im.vector.matrix.android" > + + + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/events/EventContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/events/EventContent.kt deleted file mode 100644 index 4b7d7b5dbe..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/events/EventContent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package im.vector.matrix.android.api.events - -class EventContent : HashMap() \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt index ab1024ca1e..89106b2958 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt @@ -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" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/SessionModule.kt index 9129d1bc57..e40d018e8d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/SessionModule.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/events/sync/SyncModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/events/sync/SyncModule.kt index 2d14559a34..386dc2d240 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/events/sync/SyncModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/events/sync/SyncModule.kt @@ -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() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/events/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/events/sync/SyncResponseHandler.kt deleted file mode 100644 index 4ac521ec66..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/events/sync/SyncResponseHandler.kt +++ /dev/null @@ -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?, isInitialSync: Boolean) { - try { - if (accountData!!.containsKey("events")) { - val events = accountData["events"] as List> - - 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>) { - 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>, 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>): List? { - var ignoredUsers: List? = 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 - - if (contentDict.containsKey(AccountDataRestClient.ACCOUNT_DATA_KEY_IGNORED_USERS)) { - val ignored_users = contentDict[AccountDataRestClient.ACCOUNT_DATA_KEY_IGNORED_USERS] as Map - - 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>, 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> - - 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>) { - 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 - - 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>) { - 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 - - 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>? = 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 - 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() { - 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}}" - } - - -} -*/ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/events/sync/Synchronizer.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/events/sync/Synchronizer.kt index ef5b1dbcd4..65456a8c0f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/events/sync/Synchronizer.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/events/sync/Synchronizer.kt @@ -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): Cancelable { val job = GlobalScope.launch(coroutineDispatchers.main) { val params = HashMap() + val filterBody = FilterBody() + FilterUtil.enableLazyLoading(filterBody, true) params["timeout"] = "0" - params["filter"] = "{}" + params["filter"] = filterBody.toJSONString() val syncResponse = executeRequest { apiCall = syncAPI.sync(params) moshi = jsonMapper diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/HomeServerConnectionConfig.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/HomeServerConnectionConfig.java new file mode 100644 index 0000000000..cc928ef073 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/HomeServerConnectionConfig.java @@ -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 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 mTlsVersions; + // the accepted TLS cipher suites + private List 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 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 getAcceptedTlsVersions() { + return mTlsVersions; + } + + /** + * TLS cipher suites accepted for TLS connections with the home server. + */ + @Nullable + public List 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 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 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 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 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 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; + } + + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/MXDataHandler.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/MXDataHandler.java new file mode 100644 index 0000000000..6235bc503e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/MXDataHandler.java @@ -0,0 +1,2137 @@ +/* + * 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.os.Handler; +import android.os.Looper; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; + +import im.vector.matrix.android.internal.legacy.call.MXCallsManager; +import im.vector.matrix.android.internal.legacy.crypto.MXCrypto; +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.data.DataRetriever; +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.metrics.MetricsListener; +import im.vector.matrix.android.internal.legacy.data.store.IMXStore; +import im.vector.matrix.android.internal.legacy.data.store.MXMemoryStore; +import im.vector.matrix.android.internal.legacy.data.timeline.EventTimeline; +import im.vector.matrix.android.internal.legacy.db.MXMediasCache; +import im.vector.matrix.android.internal.legacy.groups.GroupsManager; +import im.vector.matrix.android.internal.legacy.listeners.IMXEventListener; +import im.vector.matrix.android.internal.legacy.network.NetworkConnectivityReceiver; +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.AccountDataRestClient; +import im.vector.matrix.android.internal.legacy.rest.client.EventsRestClient; +import im.vector.matrix.android.internal.legacy.rest.client.PresenceRestClient; +import im.vector.matrix.android.internal.legacy.rest.client.ProfileRestClient; +import im.vector.matrix.android.internal.legacy.rest.client.RoomsRestClient; +import im.vector.matrix.android.internal.legacy.rest.client.ThirdPidRestClient; +import im.vector.matrix.android.internal.legacy.rest.model.ChunkEvents; +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.ReceiptData; +import im.vector.matrix.android.internal.legacy.rest.model.RoomAliasDescription; +import im.vector.matrix.android.internal.legacy.rest.model.RoomMember; +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.rest.model.bingrules.PushRuleSet; +import im.vector.matrix.android.internal.legacy.rest.model.bingrules.PushRulesResponse; +import im.vector.matrix.android.internal.legacy.rest.model.group.InvitedGroupSync; +import im.vector.matrix.android.internal.legacy.rest.model.login.Credentials; +import im.vector.matrix.android.internal.legacy.rest.model.sync.InvitedRoomSync; +import im.vector.matrix.android.internal.legacy.rest.model.sync.SyncResponse; +import im.vector.matrix.android.internal.legacy.ssl.UnrecognizedCertificateException; +import im.vector.matrix.android.internal.legacy.util.BingRulesManager; +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.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * The data handler provides a layer to help manage matrix input and output. + *
    + *
  • Handles events
  • + *
  • Stores the data in its storage layer
  • + *
  • Provides the means for an app to get callbacks for data changes
  • + *
+ */ +public class MXDataHandler { + private static final String LOG_TAG = MXDataHandler.class.getSimpleName(); + + private static final String LEFT_ROOMS_FILTER = "{\"room\":{\"timeline\":{\"limit\":1},\"include_leave\":true}}"; + + public interface RequestNetworkErrorListener { + /** + * Call there is a configuration error. + * + * @param matrixErrorCode the matrix error code + */ + void onConfigurationError(String matrixErrorCode); + + /** + * Call when the requests are rejected after a SSL update + * + * @param exception the exception + */ + void onSSLCertificateError(UnrecognizedCertificateException exception); + } + + private MxEventDispatcher mMxEventDispatcher; + + private final IMXStore mStore; + private final Credentials mCredentials; + private volatile String mInitialSyncToToken = null; + private DataRetriever mDataRetriever; + private BingRulesManager mBingRulesManager; + private MXCallsManager mCallsManager; + private MXMediasCache mMediasCache; + + private MetricsListener mMetricsListener; + + private ProfileRestClient mProfileRestClient; + private PresenceRestClient mPresenceRestClient; + private ThirdPidRestClient mThirdPidRestClient; + private RoomsRestClient mRoomsRestClient; + private EventsRestClient mEventsRestClient; + private AccountDataRestClient mAccountDataRestClient; + + private NetworkConnectivityReceiver mNetworkConnectivityReceiver; + + private MyUser mMyUser; + + // list of ignored users + // null -> not initialized + // should be retrieved from the store + private List mIgnoredUserIdsList; + + // list all the roomIds of the current direct chat rooms + private List mLocalDirectChatRoomIdsList = null; + + private boolean mIsAlive = true; + + private RequestNetworkErrorListener mRequestNetworkErrorListener; + + // the left rooms are managed + // by default, they are not supported + private boolean mAreLeftRoomsSynced; + + // + private final List> mLeftRoomsRefreshCallbacks = new ArrayList<>(); + private boolean mIsRetrievingLeftRooms; + + // the left rooms are saved in a dedicated store. + private final MXMemoryStore mLeftRoomsStore; + + // e2e decoder + private MXCrypto mCrypto; + + // the crypto is only started when the sync did not retrieve new device + private boolean mIsStartingCryptoWithInitialSync = false; + + // groups manager + private GroupsManager mGroupsManager; + + // Resource limit exceeded error + @Nullable + private MatrixError mResourceLimitExceededError; + + // tell if the lazy loading is enabled + private boolean mIsLazyLoadingEnabled; + + /** + * Default constructor. + * + * @param store the data storage implementation. + * @param credentials the credentials + */ + public MXDataHandler(IMXStore store, Credentials credentials) { + mStore = store; + mCredentials = credentials; + mMxEventDispatcher = new MxEventDispatcher(); + mLeftRoomsStore = new MXMemoryStore(credentials, store.getContext()); + } + + public void setLazyLoadingEnabled(boolean enabled) { + mIsLazyLoadingEnabled = enabled; + } + + public boolean isLazyLoadingEnabled() { + return mIsLazyLoadingEnabled; + } + + /** + * Set the network error listener. + * + * @param requestNetworkErrorListener the network error listener + */ + public void setRequestNetworkErrorListener(RequestNetworkErrorListener requestNetworkErrorListener) { + mRequestNetworkErrorListener = requestNetworkErrorListener; + } + + /** + * Update the metrics listener + * + * @param metricsListener the metrics listener + */ + public void setMetricsListener(MetricsListener metricsListener) { + mMetricsListener = metricsListener; + } + + /** + * @return the credentials + */ + public Credentials getCredentials() { + return mCredentials; + } + + /** + * Update the profile Rest client. + * + * @param profileRestClient the REST client + */ + public void setProfileRestClient(ProfileRestClient profileRestClient) { + mProfileRestClient = profileRestClient; + } + + /** + * @return the profile REST client + */ + public ProfileRestClient getProfileRestClient() { + return mProfileRestClient; + } + + /** + * Update the presence Rest client. + * + * @param presenceRestClient the REST client + */ + public void setPresenceRestClient(PresenceRestClient presenceRestClient) { + mPresenceRestClient = presenceRestClient; + } + + /** + * @return the presence REST client + */ + public PresenceRestClient getPresenceRestClient() { + return mPresenceRestClient; + } + + /** + * Update the thirdPid Rest client. + * + * @param thirdPidRestClient the REST client + */ + public void setThirdPidRestClient(ThirdPidRestClient thirdPidRestClient) { + mThirdPidRestClient = thirdPidRestClient; + } + + /** + * @return the ThirdPid REST client + */ + public ThirdPidRestClient getThirdPidRestClient() { + return mThirdPidRestClient; + } + + /** + * Update the rooms Rest client. + * + * @param roomsRestClient the rooms client + */ + public void setRoomsRestClient(RoomsRestClient roomsRestClient) { + mRoomsRestClient = roomsRestClient; + } + + /** + * Update the events Rest client. + * + * @param eventsRestClient the events client + */ + public void setEventsRestClient(EventsRestClient eventsRestClient) { + mEventsRestClient = eventsRestClient; + } + + /** + * Update the account data Rest client. + * + * @param accountDataRestClient the account data client + */ + public void setAccountDataRestClient(AccountDataRestClient accountDataRestClient) { + mAccountDataRestClient = accountDataRestClient; + } + + /** + * Update the network connectivity receiver. + * + * @param networkConnectivityReceiver the network connectivity receiver + */ + public void setNetworkConnectivityReceiver(NetworkConnectivityReceiver networkConnectivityReceiver) { + mNetworkConnectivityReceiver = networkConnectivityReceiver; + + if (null != getCrypto()) { + getCrypto().setNetworkConnectivityReceiver(mNetworkConnectivityReceiver); + } + } + + /** + * Set the groups manager. + * + * @param groupsManager the groups manager + */ + public void setGroupsManager(GroupsManager groupsManager) { + mGroupsManager = groupsManager; + } + + /** + * @return the crypto engine + */ + public MXCrypto getCrypto() { + return mCrypto; + } + + /** + * Update the crypto engine. + * + * @param crypto the crypto engine + */ + public void setCrypto(MXCrypto crypto) { + mCrypto = crypto; + } + + /** + * @return true if the crypto is enabled + */ + public boolean isCryptoEnabled() { + return null != mCrypto; + } + + /** + * Provide the list of user Ids to ignore. + * The result cannot be null. + * + * @return the user Ids list + */ + public List getIgnoredUserIds() { + if (null == mIgnoredUserIdsList) { + mIgnoredUserIdsList = mStore.getIgnoredUserIdsList(); + } + + // avoid the null case + if (null == mIgnoredUserIdsList) { + mIgnoredUserIdsList = new ArrayList<>(); + } + + return mIgnoredUserIdsList; + } + + /** + * Test if the current instance is still active. + * When the session is closed, many objects keep a reference to this class + * to dispatch events : isAlive() should be called before calling a method of this class. + */ + private void checkIfAlive() { + synchronized (this) { + if (!mIsAlive) { + Log.e(LOG_TAG, "use of a released dataHandler", new Exception("use of a released dataHandler")); + //throw new AssertionError("Should not used a MXDataHandler"); + } + } + } + + /** + * Tell if the current instance is still active. + * When the session is closed, many objects keep a reference to this class + * to dispatch events : isAlive() should be called before calling a method of this class. + * + * @return true if it is active. + */ + public boolean isAlive() { + synchronized (this) { + return mIsAlive; + } + } + + /** + * Dispatch the configuration error. + * + * @param matrixErrorCode the matrix error code. + */ + public void onConfigurationError(String matrixErrorCode) { + if (null != mRequestNetworkErrorListener) { + mRequestNetworkErrorListener.onConfigurationError(matrixErrorCode); + } + } + + /** + * Call when the requests are rejected after a SSL update. + * + * @param exception the SSL certificate exception + */ + public void onSSLCertificateError(UnrecognizedCertificateException exception) { + if (null != mRequestNetworkErrorListener) { + mRequestNetworkErrorListener.onSSLCertificateError(exception); + } + } + + /** + * Get the last resource limit exceeded error if any or null + * + * @return the last resource limit exceeded error if any or null + */ + @Nullable + public MatrixError getResourceLimitExceededError() { + return mResourceLimitExceededError; + } + + /** + * Get the session's current user. The MyUser object provides methods for updating user properties which are not possible for other users. + * + * @return the session's MyUser object + */ + public MyUser getMyUser() { + checkIfAlive(); + + IMXStore store = getStore(); + + // MyUser is initialized as late as possible to have a better chance at having the info in storage, + // which should be the case if this is called after the initial sync + if (mMyUser == null) { + mMyUser = new MyUser(store.getUser(mCredentials.userId)); + mMyUser.setDataHandler(this); + + // assume the profile is not yet initialized + if (null == store.displayName()) { + store.setAvatarURL(mMyUser.getAvatarUrl(), System.currentTimeMillis()); + store.setDisplayName(mMyUser.displayname, System.currentTimeMillis()); + } else { + // use the latest user information + // The user could have updated his profile in offline mode and kill the application. + mMyUser.displayname = store.displayName(); + mMyUser.setAvatarUrl(store.avatarURL()); + } + + // Handle the case where the user is null by loading the user information from the server + mMyUser.user_id = mCredentials.userId; + } else if (null != store) { + // assume the profile is not yet initialized + if ((null == store.displayName()) && (null != mMyUser.displayname)) { + // setAvatarURL && setDisplayName perform a commit if it is required. + store.setAvatarURL(mMyUser.getAvatarUrl(), System.currentTimeMillis()); + store.setDisplayName(mMyUser.displayname, System.currentTimeMillis()); + } else if (!TextUtils.equals(mMyUser.displayname, store.displayName())) { + mMyUser.displayname = store.displayName(); + mMyUser.setAvatarUrl(store.avatarURL()); + } + } + + // check if there is anything to refresh + mMyUser.refreshUserInfos(null); + + return mMyUser; + } + + /** + * @return true if the initial sync is completed. + */ + public boolean isInitialSyncComplete() { + checkIfAlive(); + return (null != mInitialSyncToToken); + } + + /** + * @return the DataRetriever. + */ + public DataRetriever getDataRetriever() { + checkIfAlive(); + return mDataRetriever; + } + + /** + * Update the dataRetriever. + * + * @param dataRetriever the dataRetriever. + */ + public void setDataRetriever(DataRetriever dataRetriever) { + checkIfAlive(); + mDataRetriever = dataRetriever; + } + + /** + * Update the push rules manager. + * + * @param bingRulesManager the new push rules manager. + */ + public void setPushRulesManager(BingRulesManager bingRulesManager) { + if (isAlive()) { + mBingRulesManager = bingRulesManager; + + mBingRulesManager.loadRules(new SimpleApiCallback() { + @Override + public void onSuccess(Void info) { + onBingRulesUpdate(); + } + }); + } + } + + /** + * Update the calls manager. + * + * @param callsManager the new calls manager. + */ + public void setCallsManager(MXCallsManager callsManager) { + checkIfAlive(); + mCallsManager = callsManager; + } + + /** + * @return the user calls manager. + */ + public MXCallsManager getCallsManager() { + checkIfAlive(); + return mCallsManager; + } + + /** + * Update the medias cache. + * + * @param mediasCache the new medias cache. + */ + public void setMediasCache(MXMediasCache mediasCache) { + checkIfAlive(); + mMediasCache = mediasCache; + } + + /** + * Retrieve the medias cache. + * + * @return the used mediasCache + */ + public MXMediasCache getMediasCache() { + checkIfAlive(); + return mMediasCache; + } + + /** + * @return the used push rules set. + */ + public PushRuleSet pushRules() { + if (isAlive() && (null != mBingRulesManager)) { + return mBingRulesManager.pushRules(); + } + + return null; + } + + /** + * Trigger a push rules refresh. + */ + public void refreshPushRules() { + if (isAlive() && (null != mBingRulesManager)) { + mBingRulesManager.loadRules(new SimpleApiCallback() { + @Override + public void onSuccess(Void info) { + onBingRulesUpdate(); + } + }); + } + } + + /** + * @return the used BingRulesManager. + */ + public BingRulesManager getBingRulesManager() { + checkIfAlive(); + return mBingRulesManager; + } + + /** + * Set the crypto events listener, or remove it + * + * @param listener the listener or null to remove the listener + */ + public void setCryptoEventsListener(@Nullable IMXEventListener listener) { + mMxEventDispatcher.setCryptoEventsListener(listener); + } + + /** + * Add a listener to the listeners list. + * + * @param listener the listener to add. + */ + public void addListener(IMXEventListener listener) { + if (isAlive() && (null != listener)) { + synchronized (mMxEventDispatcher) { + mMxEventDispatcher.addListener(listener); + } + + if (null != mInitialSyncToToken) { + listener.onInitialSyncComplete(mInitialSyncToToken); + } + } + } + + /** + * Remove a listener from the listeners list. + * + * @param listener to remove. + */ + public void removeListener(IMXEventListener listener) { + if (isAlive() && (null != listener)) { + synchronized (mMxEventDispatcher) { + mMxEventDispatcher.removeListener(listener); + } + } + } + + /** + * Clear the instance data. + */ + public void clear() { + synchronized (mMxEventDispatcher) { + mIsAlive = false; + // remove any listener + mMxEventDispatcher.clearListeners(); + } + + // clear the store + mStore.close(); + mStore.clear(); + } + + /** + * @return the current user id. + */ + public String getUserId() { + if (isAlive()) { + return mCredentials.userId; + } else { + return "dummy"; + } + } + + /** + * Update the missing data fields loaded from a permanent storage. + */ + void checkPermanentStorageData() { + if (!isAlive()) { + Log.e(LOG_TAG, "checkPermanentStorageData : the session is not anymore active"); + return; + } + + // When the data are extracted from a persistent storage, + // some fields are not retrieved : + // They are used to retrieve some data + // so add the missing links. + Collection summaries = mStore.getSummaries(); + for (RoomSummary summary : summaries) { + if (null != summary.getLatestRoomState()) { + summary.getLatestRoomState().setDataHandler(this); + } + } + } + + + /** + * @return the used store. + */ + public IMXStore getStore() { + if (isAlive()) { + return mStore; + } else { + Log.e(LOG_TAG, "getStore : the session is not anymore active"); + return null; + } + } + + /** + * Provides the store in which the room is stored. + * + * @param roomId the room id + * @return the used store + */ + public IMXStore getStore(String roomId) { + if (isAlive()) { + if (null == roomId) { + return mStore; + } else { + if (null != mLeftRoomsStore.getRoom(roomId)) { + return mLeftRoomsStore; + } else { + return mStore; + } + } + } else { + Log.e(LOG_TAG, "getStore : the session is not anymore active"); + return null; + } + } + + /** + * Returns the member with userID; + * + * @param members the members List + * @param userID the user ID + * @return the roomMember if it exists. + */ + public RoomMember getMember(Collection members, String userID) { + if (isAlive()) { + for (RoomMember member : members) { + if (TextUtils.equals(userID, member.getUserId())) { + return member; + } + } + } else { + Log.e(LOG_TAG, "getMember : the session is not anymore active"); + } + return null; + } + + /** + * Check a room exists with the dedicated roomId + * + * @param roomId the room ID + * @return true it exists. + */ + public boolean doesRoomExist(String roomId) { + return (null != roomId) && (null != mStore.getRoom(roomId)); + } + + /** + * @return the left rooms + */ + public Collection getLeftRooms() { + return new ArrayList<>(mLeftRoomsStore.getRooms()); + } + + /** + * Get the room object for the corresponding room id. Creates and initializes the object if there is none. + * + * @param roomId the room id + * @return the corresponding room + */ + public Room getRoom(String roomId) { + return getRoom(roomId, true); + } + + /** + * Get the room object for the corresponding room id. + * The left rooms are not included. + * + * @param roomId the room id + * @param create create the room it does not exist. + * @return the corresponding room + */ + public Room getRoom(String roomId, boolean create) { + return getRoom(mStore, roomId, create); + } + + /** + * Get the room object for the corresponding room id. + * By default, the left rooms are not included. + * + * @param roomId the room id + * @param testLeftRooms true to test if the room is a left room + * @param create create the room it does not exist. + * @return the corresponding room + */ + public Room getRoom(String roomId, boolean testLeftRooms, boolean create) { + Room room = null; + + if (null != roomId) { + room = mStore.getRoom(roomId); + + if ((null == room) && testLeftRooms) { + room = mLeftRoomsStore.getRoom(roomId); + } + + if ((null == room) && create) { + room = getRoom(mStore, roomId, create); + } + } + + return room; + } + + /** + * Get the room object from the corresponding room id. + * + * @param store the dedicated store + * @param roomId the room id + * @param create create the room it does not exist. + * @return the corresponding room + */ + public Room getRoom(IMXStore store, String roomId, boolean create) { + if (!isAlive()) { + Log.e(LOG_TAG, "getRoom : the session is not anymore active"); + return null; + } + + // sanity check + if (TextUtils.isEmpty(roomId)) { + return null; + } + + Room room; + + synchronized (this) { + room = store.getRoom(roomId); + if ((room == null) && create) { + Log.d(LOG_TAG, "## getRoom() : create the room " + roomId); + room = new Room(this, store, roomId); + store.storeRoom(room); + } else if ((null != room) && (null == room.getDataHandler())) { + // GA reports that some rooms have no data handler + // so ensure that it is not properly set + Log.e(LOG_TAG, "getRoom " + roomId + " was not initialized"); + store.storeRoom(room); + } + } + + return room; + } + + /** + * Provides the room summaries list. + * + * @param withLeftOnes set to true to include the left rooms + * @return the room summaries + */ + public Collection getSummaries(boolean withLeftOnes) { + List summaries = new ArrayList<>(); + + summaries.addAll(getStore().getSummaries()); + + if (withLeftOnes) { + summaries.addAll(mLeftRoomsStore.getSummaries()); + } + + return summaries; + } + + /** + * Retrieve a room Id by its alias. + * + * @param roomAlias the room alias + * @param callback the asynchronous callback + */ + public void roomIdByAlias(final String roomAlias, final ApiCallback callback) { + String roomId = null; + + Collection rooms = getStore().getRooms(); + + for (Room room : rooms) { + if (TextUtils.equals(room.getState().getCanonicalAlias(), roomAlias)) { + roomId = room.getRoomId(); + break; + } else { + // getAliases cannot be null + List aliases = room.getState().getAliases(); + + for (String alias : aliases) { + if (TextUtils.equals(alias, roomAlias)) { + roomId = room.getRoomId(); + break; + } + } + + // find one matched room id. + if (null != roomId) { + break; + } + } + } + + if (null != roomId) { + final String fRoomId = roomId; + + Handler handler = new Handler(Looper.getMainLooper()); + + handler.post(new Runnable() { + @Override + public void run() { + callback.onSuccess(fRoomId); + } + }); + } else { + mRoomsRestClient.getRoomIdByAlias(roomAlias, new SimpleApiCallback(callback) { + @Override + public void onSuccess(RoomAliasDescription info) { + callback.onSuccess(info.room_id); + } + }); + } + + } + + /** + * Get the members of a Room with a request to the server. it will exclude the members who has left the room + * + * @param roomId the id of the room + * @param callback the callback + */ + public void getMembersAsync(final String roomId, final ApiCallback> callback) { + mRoomsRestClient.getRoomMembers(roomId, getStore().getEventStreamToken(), null, RoomMember.MEMBERSHIP_LEAVE, + new SimpleApiCallback(callback) { + @Override + public void onSuccess(ChunkEvents info) { + Room room = getRoom(roomId); + + if (info.chunk != null) { + for (Event event : info.chunk) { + room.getState().applyState(getStore(), event, EventTimeline.Direction.FORWARDS); + } + } + + callback.onSuccess(room.getState().getLoadedMembers()); + } + }); + } + + /** + * Delete an event. + * + * @param event The event to be stored. + */ + public void deleteRoomEvent(Event event) { + if (isAlive()) { + Room room = getRoom(event.roomId); + + if (null != room) { + mStore.deleteEvent(event); + Event lastEvent = mStore.getLatestEvent(event.roomId); + RoomState beforeLiveRoomState = room.getState().deepCopy(); + + RoomSummary summary = mStore.getSummary(event.roomId); + if (null == summary) { + summary = new RoomSummary(null, lastEvent, beforeLiveRoomState, mCredentials.userId); + } else { + summary.setLatestReceivedEvent(lastEvent, beforeLiveRoomState); + } + + if (TextUtils.equals(summary.getReadReceiptEventId(), event.eventId)) { + summary.setReadReceiptEventId(lastEvent.eventId); + } + + if (TextUtils.equals(summary.getReadMarkerEventId(), event.eventId)) { + summary.setReadMarkerEventId(lastEvent.eventId); + } + + mStore.storeSummary(summary); + } + } else { + Log.e(LOG_TAG, "deleteRoomEvent : the session is not anymore active"); + } + } + + /** + * Return an user from his id. + * + * @param userId the user id;. + * @return the user. + */ + public User getUser(String userId) { + if (!isAlive()) { + Log.e(LOG_TAG, "getUser : the session is not anymore active"); + return null; + } else { + User user = mStore.getUser(userId); + + if (null == user) { + user = mLeftRoomsStore.getUser(userId); + } + + return user; + } + } + + //================================================================================ + // Account Data management + //================================================================================ + + /** + * Manage the sync accountData field + * + * @param accountData the account data + * @param isInitialSync true if it is an initial sync response + */ + private void manageAccountData(Map accountData, boolean isInitialSync) { + try { + if (accountData.containsKey("events")) { + List> events = (List>) accountData.get("events"); + + 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 (Exception e) { + Log.e(LOG_TAG, "manageAccountData failed " + e.getMessage(), e); + } + } + + /** + * Refresh the push rules from the account data events list + * + * @param events the account data events. + */ + private void managePushRulesUpdate(List> events) { + for (Map event : events) { + String type = (String) event.get("type"); + + if (TextUtils.equals(type, "m.push_rules")) { + if (event.containsKey("content")) { + Gson gson = JsonUtils.getGson(false); + + // convert the data to PushRulesResponse + // because BingRulesManager supports only PushRulesResponse + JsonElement element = gson.toJsonTree(event.get("content")); + getBingRulesManager().buildRules(gson.fromJson(element, PushRulesResponse.class)); + + // 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 void manageIgnoredUsers(List> events, boolean isInitialSync) { + List newIgnoredUsers = ignoredUsers(events); + + if (null != newIgnoredUsers) { + List 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 List ignoredUsers(List> events) { + List ignoredUsers = null; + + if (0 != events.size()) { + for (Map event : events) { + String type = (String) event.get("type"); + + if (TextUtils.equals(type, AccountDataRestClient.ACCOUNT_DATA_TYPE_IGNORED_USER_LIST)) { + if (event.containsKey("content")) { + Map contentDict = (Map) event.get("content"); + + if (contentDict.containsKey(AccountDataRestClient.ACCOUNT_DATA_KEY_IGNORED_USERS)) { + Map ignored_users = (Map) contentDict.get(AccountDataRestClient.ACCOUNT_DATA_KEY_IGNORED_USERS); + + if (null != ignored_users) { + ignoredUsers = new ArrayList<>(ignored_users.keySet()); + } + } + } + } + } + + } + + return ignoredUsers; + } + + + /** + * Extract the direct chat rooms list from the dedicated events. + * + * @param events the account data events list. + */ + private void manageDirectChatRooms(List> events, boolean isInitialSync) { + if (0 != events.size()) { + for (Map event : events) { + String type = (String) event.get("type"); + + if (TextUtils.equals(type, AccountDataRestClient.ACCOUNT_DATA_TYPE_DIRECT_MESSAGES)) { + if (event.containsKey("content")) { + Map> contentDict = (Map>) event.get("content"); + + 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 void manageUrlPreview(List> events) { + if (0 != events.size()) { + for (Map event : events) { + String type = (String) event.get("type"); + + if (TextUtils.equals(type, AccountDataRestClient.ACCOUNT_DATA_TYPE_PREVIEW_URLS)) { + if (event.containsKey("content")) { + Map contentDict = (Map) event.get("content"); + + Log.d(LOG_TAG, "## manageUrlPreview() : " + contentDict); + boolean enable = true; + if (contentDict.containsKey(AccountDataRestClient.ACCOUNT_DATA_KEY_URL_PREVIEW_DISABLE)) { + enable = !((boolean) contentDict.get(AccountDataRestClient.ACCOUNT_DATA_KEY_URL_PREVIEW_DISABLE)); + } + + mStore.setURLPreviewEnabled(enable); + } + } + } + } + } + + /** + * Manage the user widgets + * + * @param events the events list + */ + private void manageUserWidgets(List> events) { + if (0 != events.size()) { + for (Map event : events) { + String type = (String) event.get("type"); + + if (TextUtils.equals(type, AccountDataRestClient.ACCOUNT_DATA_TYPE_WIDGETS)) { + if (event.containsKey("content")) { + Map contentDict = (Map) event.get("content"); + + Log.d(LOG_TAG, "## manageUserWidgets() : " + contentDict); + + mStore.setUserWidgets(contentDict); + } + } + } + } + } + + //================================================================================ + // Sync V2 + //================================================================================ + + /** + * Handle a presence event. + * + * @param presenceEvent the presence event. + */ + private void handlePresenceEvent(Event presenceEvent) { + // Presence event + if (Event.EVENT_TYPE_PRESENCE.equals(presenceEvent.getType())) { + User userPresence = JsonUtils.toUser(presenceEvent.getContent()); + + // use the sender by default + if (!TextUtils.isEmpty(presenceEvent.getSender())) { + userPresence.user_id = presenceEvent.getSender(); + } + + User 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); + } + } + + /** + * Manage a syncResponse. + * + * @param syncResponse the syncResponse to manage. + * @param fromToken the start sync token + * @param isCatchingUp true when there is a pending catch-up + */ + public void onSyncResponse(final SyncResponse syncResponse, final String fromToken, final boolean isCatchingUp) { + manageResponse(syncResponse, fromToken, isCatchingUp); + } + + /** + * Delete a room from its room id. + * The room data is copied into the left rooms store. + * + * @param roomId the room id + */ + public void deleteRoom(String roomId) { + // copy the room from a store to another one + Room r = getStore().getRoom(roomId); + + if (null != r) { + if (mAreLeftRoomsSynced) { + Room leftRoom = getRoom(mLeftRoomsStore, roomId, true); + leftRoom.setIsLeft(true); + + // copy the summary + RoomSummary summary = getStore().getSummary(roomId); + if (null != summary) { + mLeftRoomsStore.storeSummary(new RoomSummary(summary, summary.getLatestReceivedEvent(), summary.getLatestRoomState(), getUserId())); + } + + // copy events and receiptData + // it is not required but it is better, it could be useful later + // the room summary should be enough to be displayed in the recent pages + List receipts = new ArrayList<>(); + Collection events = getStore().getRoomMessages(roomId); + + if (null != events) { + for (Event e : events) { + receipts.addAll(getStore().getEventReceipts(roomId, e.eventId, false, false)); + mLeftRoomsStore.storeLiveRoomEvent(e); + } + + for (ReceiptData receipt : receipts) { + mLeftRoomsStore.storeReceipt(receipt, roomId); + } + } + + // copy the state + leftRoom.getTimeline().setState(r.getTimeline().getState()); + } + + // remove the previous definition + getStore().deleteRoom(roomId); + } + } + + /** + * Manage the sync response in a background thread. + * + * @param syncResponse the syncResponse to manage. + * @param fromToken the start sync token + * @param isCatchingUp true when there is a pending catch-up + */ + public void manageResponse(final SyncResponse syncResponse, final String fromToken, final boolean isCatchingUp) { + if (!isAlive()) { + Log.e(LOG_TAG, "manageResponse : ignored because the session has been closed"); + return; + } + + boolean isInitialSync = (null == fromToken); + boolean isEmptyResponse = true; + + // sanity check + if (null != syncResponse) { + Log.d(LOG_TAG, "onSyncComplete"); + + // Handle the to device events before the room ones + // to ensure to decrypt them properly + if ((null != syncResponse.toDevice) + && (null != syncResponse.toDevice.events) + && (syncResponse.toDevice.events.size() > 0)) { + Log.d(LOG_TAG, "manageResponse : receives " + syncResponse.toDevice.events.size() + " toDevice events"); + + for (Event toDeviceEvent : syncResponse.toDevice.events) { + handleToDeviceEvent(toDeviceEvent); + } + } + + // Handle account data before the room events + // to be able to update direct chats dictionary during invites handling. + if (null != syncResponse.accountData) { + Log.d(LOG_TAG, "Received " + syncResponse.accountData.size() + " accountData events"); + manageAccountData(syncResponse.accountData, isInitialSync); + } + + // sanity check + if (null != syncResponse.rooms) { + // joined rooms events + if ((null != syncResponse.rooms.join) && (syncResponse.rooms.join.size() > 0)) { + Log.d(LOG_TAG, "Received " + syncResponse.rooms.join.size() + " joined rooms"); + if (mMetricsListener != null) { + mMetricsListener.onRoomsLoaded(syncResponse.rooms.join.size()); + } + Set roomIds = syncResponse.rooms.join.keySet(); + // Handle first joined rooms + for (String roomId : 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.get(roomId), isInitialSync); + } catch (Exception e) { + Log.e(LOG_TAG, "## manageResponse() : handleJoinedRoomSync failed " + e.getMessage() + " for room " + roomId, e); + } + } + + isEmptyResponse = false; + } + + // invited room management + if ((null != syncResponse.rooms.invite) && (syncResponse.rooms.invite.size() > 0)) { + Log.d(LOG_TAG, "Received " + syncResponse.rooms.invite.size() + " invited rooms"); + + Set roomIds = syncResponse.rooms.invite.keySet(); + + Map> updatedDirectChatRoomsDict = null; + boolean hasChanged = false; + + for (String roomId : roomIds) { + try { + Log.d(LOG_TAG, "## manageResponse() : the user has been invited to " + roomId); + + if (null != mLeftRoomsStore.getRoom(roomId)) { + Log.d(LOG_TAG, "the room " + roomId + " moves from left to the invited ones"); + mLeftRoomsStore.deleteRoom(roomId); + } + + Room room = getRoom(roomId); + InvitedRoomSync invitedRoomSync = syncResponse.rooms.invite.get(roomId); + + room.handleInvitedRoomSync(invitedRoomSync); + + // Handle here the invites to a direct chat. + if (room.isDirectChatInvitation()) { + // Retrieve the inviter user id. + String participantUserId = null; + for (Event event : 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 = new HashMap<>(getStore().getDirectChatRoomsDict()); + } else { + updatedDirectChatRoomsDict = new HashMap<>(); + } + } + + List roomIdsList; + if (updatedDirectChatRoomsDict.containsKey(participantUserId)) { + roomIdsList = new ArrayList<>(updatedDirectChatRoomsDict.get(participantUserId)); + } else { + roomIdsList = new 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.put(participantUserId, roomIdsList); + hasChanged = true; + } + } + } + } catch (Exception e) { + Log.e(LOG_TAG, "## manageResponse() : handleInvitedRoomSync failed " + e.getMessage() + " 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, new ApiCallback() { + @Override + public void onSuccess(Void info) { + Log.d(LOG_TAG, "## manageResponse() : succeeds"); + } + + @Override + public void onNetworkError(Exception e) { + Log.e(LOG_TAG, "## manageResponse() : update account data failed " + e.getMessage(), e); + // TODO: we should try again. + } + + @Override + public void onMatrixError(MatrixError e) { + Log.e(LOG_TAG, "## manageResponse() : update account data failed " + e.getMessage()); + } + + @Override + public void onUnexpectedError(Exception e) { + Log.e(LOG_TAG, "## manageResponse() : update account data failed " + e.getMessage(), 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"); + + Set roomIds = syncResponse.rooms.leave.keySet(); + + for (String roomId : 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 + + String membership = RoomMember.MEMBERSHIP_LEAVE; + Room 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.get(roomId), isInitialSync); + + RoomMember 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)) { + Room leftRoom = getRoom(mLeftRoomsStore, roomId, true); + leftRoom.handleJoinedRoomSync(syncResponse.rooms.leave.get(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 (String groupId : syncResponse.groups.invite.keySet()) { + InvitedGroupSync 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 (String groupId : syncResponse.groups.join.keySet()) { + mGroupsManager.onJoinGroup(groupId, !isInitialSync); + } + } + // Handle left groups + if ((null != syncResponse.groups.leave) && !syncResponse.groups.leave.isEmpty()) { + // Handle joined groups + for (String groupId : 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 (Event presenceEvent : syncResponse.presence.events) { + handlePresenceEvent(presenceEvent); + } + } + + if (null != mCrypto) { + mCrypto.onSyncCompleted(syncResponse, fromToken, isCatchingUp); + } + + IMXStore store = getStore(); + + if (!isEmptyResponse && (null != store)) { + store.setEventStreamToken(syncResponse.nextBatch); + store.commit(); + } + } + + if (isInitialSync) { + if (!isCatchingUp) { + startCrypto(true); + } else { + // the events thread sends a dummy initial sync event + // when the application is restarted. + mIsStartingCryptoWithInitialSync = !isEmptyResponse; + } + + onInitialSyncComplete((null != syncResponse) ? syncResponse.nextBatch : null); + } else { + + if (!isCatchingUp) { + startCrypto(mIsStartingCryptoWithInitialSync); + } + + try { + onLiveEventsChunkProcessed(fromToken, (null != syncResponse) ? syncResponse.nextBatch : fromToken); + } catch (Exception e) { + Log.e(LOG_TAG, "onLiveEventsChunkProcessed failed " + e.getMessage(), e); + } + + try { + // check if an incoming call has been received + mCallsManager.checkPendingIncomingCalls(); + } catch (Exception e) { + Log.e(LOG_TAG, "checkPendingIncomingCalls failed " + e + " " + e.getMessage(), e); + } + } + } + + /** + * Refresh the unread summary counters of the updated rooms. + */ + private void refreshUnreadCounters() { + Set roomIdsList; + + synchronized (mUpdatedRoomIdList) { + roomIdsList = new HashSet<>(mUpdatedRoomIdList); + mUpdatedRoomIdList.clear(); + } + // refresh the unread counter + for (String roomId : roomIdsList) { + Room room = mStore.getRoom(roomId); + + if (null != room) { + room.refreshUnreadCounter(); + } + } + } + + /** + * @return true if the historical rooms loaded + */ + public boolean areLeftRoomsSynced() { + return mAreLeftRoomsSynced; + } + + /** + * @return true if the left rooms are retrieving + */ + public boolean isRetrievingLeftRooms() { + return mIsRetrievingLeftRooms; + } + + /** + * Release the left rooms store + */ + public void releaseLeftRooms() { + if (mAreLeftRoomsSynced) { + mLeftRoomsStore.clear(); + mAreLeftRoomsSynced = false; + } + } + + /** + * Retrieve the historical rooms + * + * @param callback the asynchronous callback. + */ + public void retrieveLeftRooms(ApiCallback callback) { + // already loaded + if (mAreLeftRoomsSynced) { + if (null != callback) { + callback.onSuccess(null); + } + } else { + int count; + + synchronized (mLeftRoomsRefreshCallbacks) { + if (null != callback) { + mLeftRoomsRefreshCallbacks.add(callback); + } + count = mLeftRoomsRefreshCallbacks.size(); + } + + // start the request only for the first listener + if (1 == count) { + mIsRetrievingLeftRooms = true; + + Log.d(LOG_TAG, "## refreshHistoricalRoomsList() : requesting"); + + mEventsRestClient.syncFromToken(null, 0, 30000, null, LEFT_ROOMS_FILTER, new ApiCallback() { + @Override + public void onSuccess(final SyncResponse syncResponse) { + + Runnable r = new Runnable() { + @Override + public void run() { + if (null != syncResponse.rooms.leave) { + Set roomIds = syncResponse.rooms.leave.keySet(); + + // Handle first joined rooms + for (String roomId : roomIds) { + Room room = getRoom(mLeftRoomsStore, roomId, true); + + // sanity check + if (null != room) { + room.setIsLeft(true); + room.handleJoinedRoomSync(syncResponse.rooms.leave.get(roomId), true); + + RoomMember selfMember = room.getState().getMember(getUserId()); + + // keep only the left rooms (i.e not the banned / kicked ones) + if ((null == selfMember) || !TextUtils.equals(selfMember.membership, RoomMember.MEMBERSHIP_LEAVE)) { + mLeftRoomsStore.deleteRoom(roomId); + } + } + } + + Log.d(LOG_TAG, "## refreshHistoricalRoomsList() : " + mLeftRoomsStore.getRooms().size() + " left rooms"); + } + + mIsRetrievingLeftRooms = false; + mAreLeftRoomsSynced = true; + + synchronized (mLeftRoomsRefreshCallbacks) { + for (ApiCallback c : mLeftRoomsRefreshCallbacks) { + c.onSuccess(null); + } + mLeftRoomsRefreshCallbacks.clear(); + } + } + }; + + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + t.start(); + } + + @Override + public void onNetworkError(Exception e) { + synchronized (mLeftRoomsRefreshCallbacks) { + Log.e(LOG_TAG, "## refreshHistoricalRoomsList() : failed " + e.getMessage(), e); + + for (ApiCallback c : mLeftRoomsRefreshCallbacks) { + c.onNetworkError(e); + } + mLeftRoomsRefreshCallbacks.clear(); + } + } + + @Override + public void onMatrixError(MatrixError e) { + synchronized (mLeftRoomsRefreshCallbacks) { + Log.e(LOG_TAG, "## refreshHistoricalRoomsList() : failed " + e.getMessage()); + + for (ApiCallback c : mLeftRoomsRefreshCallbacks) { + c.onMatrixError(e); + } + mLeftRoomsRefreshCallbacks.clear(); + } + } + + @Override + public void onUnexpectedError(Exception e) { + synchronized (mLeftRoomsRefreshCallbacks) { + Log.e(LOG_TAG, "## refreshHistoricalRoomsList() : failed " + e.getMessage(), e); + + for (ApiCallback c : mLeftRoomsRefreshCallbacks) { + c.onUnexpectedError(e); + } + mLeftRoomsRefreshCallbacks.clear(); + } + } + }); + } + } + } + + /* + * Handle a 'toDevice' event + * @param event the event + */ + private void 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); + } + } + + /** + * Decrypt an encrypted event + * + * @param event the event to decrypt + * @param timelineId the timeline identifier + * @return true if the event has been decrypted + */ + public boolean decryptEvent(Event event, String timelineId) { + if ((null != event) && TextUtils.equals(event.getType(), Event.EVENT_TYPE_MESSAGE_ENCRYPTED)) { + if (null != getCrypto()) { + MXEventDecryptionResult result = null; + try { + result = getCrypto().decryptEvent(event, timelineId); + } catch (MXDecryptionException exception) { + event.setCryptoError(exception.getCryptoError()); + } + + if (null != result) { + event.setClearData(result); + return true; + } + } else { + event.setCryptoError(new MXCryptoError(MXCryptoError.ENCRYPTING_NOT_ENABLED_ERROR_CODE, MXCryptoError.ENCRYPTING_NOT_ENABLED_REASON, null)); + } + } + + return false; + } + + /** + * Reset replay attack data for the given timeline. + * + * @param timelineId the timeline id + */ + public void resetReplayAttackCheckInTimeline(String timelineId) { + if ((null != timelineId) && (null != mCrypto) && (null != mCrypto.getOlmDevice())) { + mCrypto.resetReplayAttackCheckInTimeline(timelineId); + } + } + + //================================================================================ + // Listeners management + //================================================================================ + + /** + * Dispatch that the store is ready. + */ + public void onStoreReady() { + mMxEventDispatcher.dispatchOnStoreReady(); + } + + public void onAccountInfoUpdate(final MyUser myUser) { + mMxEventDispatcher.dispatchOnAccountInfoUpdate(myUser); + } + + public void onPresenceUpdate(final Event event, final User user) { + mMxEventDispatcher.dispatchOnPresenceUpdate(event, user); + } + + /** + * Stored the room id of the rooms which have received some events + * which imply an unread messages counter refresh. + */ + private final Set mUpdatedRoomIdList = new HashSet<>(); + + /** + * Tell if a room Id event should be ignored + * + * @param roomId the room id + * @return true to do not dispatch the event. + */ + private boolean ignoreEvent(String roomId) { + if (mIsRetrievingLeftRooms && !TextUtils.isEmpty(roomId)) { + return null != mLeftRoomsStore.getRoom(roomId); + } else { + return false; + } + } + + public void onLiveEvent(final Event event, final RoomState roomState) { + if (ignoreEvent(event.roomId)) { + return; + } + + String type = event.getType(); + + if (!TextUtils.equals(Event.EVENT_TYPE_TYPING, type) + && !TextUtils.equals(Event.EVENT_TYPE_RECEIPT, type) + && !TextUtils.equals(Event.EVENT_TYPE_TYPING, type)) { + synchronized (mUpdatedRoomIdList) { + mUpdatedRoomIdList.add(roomState.roomId); + } + } + + mMxEventDispatcher.dispatchOnLiveEvent(event, roomState); + } + + public void onLiveEventsChunkProcessed(final String startToken, final String toToken) { + // reset the resource limit exceeded error + mResourceLimitExceededError = null; + + refreshUnreadCounters(); + + mMxEventDispatcher.dispatchOnLiveEventsChunkProcessed(startToken, toToken); + } + + public void onBingEvent(final Event event, final RoomState roomState, final BingRule bingRule) { + mMxEventDispatcher.dispatchOnBingEvent(event, roomState, bingRule, ignoreEvent(event.roomId)); + } + + /** + * Update the event state and warn the listener if there is an update. + * + * @param event the event + * @param newState the new state + */ + public void updateEventState(Event event, Event.SentState newState) { + if ((null != event) && (event.mSentState != newState)) { + event.mSentState = newState; + getStore().flushRoomEvents(event.roomId); + onEventSentStateUpdated(event); + } + } + + public void onEventSentStateUpdated(final Event event) { + mMxEventDispatcher.dispatchOnEventSentStateUpdated(event, ignoreEvent(event.roomId)); + } + + public void onEventSent(final Event event, final String prevEventId) { + mMxEventDispatcher.dispatchOnEventSent(event, prevEventId, ignoreEvent(event.roomId)); + } + + public void onBingRulesUpdate() { + mMxEventDispatcher.dispatchOnBingRulesUpdate(); + } + + /** + * Start the crypto + */ + private void startCrypto(final boolean isInitialSync) { + if ((null != getCrypto()) && !getCrypto().isStarted() && !getCrypto().isStarting()) { + getCrypto().setNetworkConnectivityReceiver(mNetworkConnectivityReceiver); + getCrypto().start(isInitialSync, new ApiCallback() { + @Override + public void onSuccess(Void info) { + onCryptoSyncComplete(); + } + + private void onError(String errorMessage) { + Log.e(LOG_TAG, "## onInitialSyncComplete() : getCrypto().start fails " + errorMessage); + } + + @Override + public void onNetworkError(Exception e) { + onError(e.getMessage()); + } + + @Override + public void onMatrixError(MatrixError e) { + onError(e.getMessage()); + } + + @Override + public void onUnexpectedError(Exception e) { + onError(e.getMessage()); + } + }); + } + } + + public void onInitialSyncComplete(String toToken) { + mInitialSyncToToken = toToken; + + refreshUnreadCounters(); + + mMxEventDispatcher.dispatchOnInitialSyncComplete(toToken); + } + + public void onSyncError(final MatrixError matrixError) { + // Store the resource limit exceeded error + if (MatrixError.RESOURCE_LIMIT_EXCEEDED.equals(matrixError.errcode)) { + mResourceLimitExceededError = matrixError; + } + + mMxEventDispatcher.dispatchOnSyncError(matrixError); + } + + public void onCryptoSyncComplete() { + mMxEventDispatcher.dispatchOnCryptoSyncComplete(); + } + + public void onNewRoom(final String roomId) { + mMxEventDispatcher.dispatchOnNewRoom(roomId, ignoreEvent(roomId)); + } + + public void onJoinRoom(final String roomId) { + mMxEventDispatcher.dispatchOnJoinRoom(roomId, ignoreEvent(roomId)); + } + + public void onRoomInternalUpdate(final String roomId) { + mMxEventDispatcher.dispatchOnRoomInternalUpdate(roomId, ignoreEvent(roomId)); + } + + public void onNotificationCountUpdate(final String roomId) { + mMxEventDispatcher.dispatchOnNotificationCountUpdate(roomId, ignoreEvent(roomId)); + } + + public void onLeaveRoom(final String roomId) { + mMxEventDispatcher.dispatchOnLeaveRoom(roomId, ignoreEvent(roomId)); + } + + public void onRoomKick(final String roomId) { + mMxEventDispatcher.dispatchOnRoomKick(roomId, ignoreEvent(roomId)); + } + + public void onReceiptEvent(final String roomId, final List senderIds) { + synchronized (mUpdatedRoomIdList) { + // refresh the unread counter at the end of the process chunk + mUpdatedRoomIdList.add(roomId); + } + + mMxEventDispatcher.dispatchOnReceiptEvent(roomId, senderIds, ignoreEvent(roomId)); + } + + public void onRoomTagEvent(final String roomId) { + mMxEventDispatcher.dispatchOnRoomTagEvent(roomId, ignoreEvent(roomId)); + } + + public void onReadMarkerEvent(final String roomId) { + mMxEventDispatcher.dispatchOnReadMarkerEvent(roomId, ignoreEvent(roomId)); + } + + public void onRoomFlush(final String roomId) { + mMxEventDispatcher.dispatchOnRoomFlush(roomId, ignoreEvent(roomId)); + } + + public void onIgnoredUsersListUpdate() { + mMxEventDispatcher.dispatchOnIgnoredUsersListUpdate(); + } + + public void onToDeviceEvent(final Event event) { + mMxEventDispatcher.dispatchOnToDeviceEvent(event, ignoreEvent(event.roomId)); + } + + public void onDirectMessageChatRoomsListUpdate() { + mMxEventDispatcher.dispatchOnDirectMessageChatRoomsListUpdate(); + } + + public void onEventDecrypted(final Event event) { + mMxEventDispatcher.dispatchOnEventDecrypted(event); + } + + public void onNewGroupInvitation(final String groupId) { + mMxEventDispatcher.dispatchOnNewGroupInvitation(groupId); + } + + public void onJoinGroup(final String groupId) { + mMxEventDispatcher.dispatchOnJoinGroup(groupId); + } + + public void onLeaveGroup(final String groupId) { + mMxEventDispatcher.dispatchOnLeaveGroup(groupId); + } + + public void onGroupProfileUpdate(final String groupId) { + mMxEventDispatcher.dispatchOnGroupProfileUpdate(groupId); + } + + public void onGroupRoomsListUpdate(final String groupId) { + mMxEventDispatcher.dispatchOnGroupRoomsListUpdate(groupId); + } + + public void onGroupUsersListUpdate(final String groupId) { + mMxEventDispatcher.dispatchOnGroupUsersListUpdate(groupId); + } + + public void onGroupInvitedUsersListUpdate(final String groupId) { + mMxEventDispatcher.dispatchOnGroupInvitedUsersListUpdate(groupId); + } + + /** + * @return the direct chat room ids list + */ + public List getDirectChatRoomIdsList() { + if (null != mLocalDirectChatRoomIdsList) return mLocalDirectChatRoomIdsList; + + IMXStore store = getStore(); + List directChatRoomIdsList = new ArrayList<>(); + + if (null == store) { + Log.e(LOG_TAG, "## getDirectChatRoomIdsList() : null store"); + return directChatRoomIdsList; + } + + Collection> listOfList = null; + + if (null != store.getDirectChatRoomsDict()) { + listOfList = store.getDirectChatRoomsDict().values(); + } + + // if the direct messages entry has been defined + if (null != listOfList) { + for (List list : listOfList) { + for (String roomId : list) { + // test if the room is defined once + if ((directChatRoomIdsList.indexOf(roomId) < 0)) { + directChatRoomIdsList.add(roomId); + } + } + } + } + + return mLocalDirectChatRoomIdsList = directChatRoomIdsList; + } + + /** + * Store and upload the provided direct chat rooms map. + * + * @param directChatRoomsMap the direct chats map + * @param callback the asynchronous callback + */ + public void setDirectChatRoomsMap(Map> directChatRoomsMap, ApiCallback callback) { + Log.d(LOG_TAG, "## setDirectChatRoomsMap()"); + IMXStore store = getStore(); + if (null != store) { + // update the store value + // do not wait the server request echo to update the store + store.setDirectChatRoomsDict(directChatRoomsMap); + } else { + Log.e(LOG_TAG, "## setDirectChatRoomsMap() : null store"); + } + mLocalDirectChatRoomIdsList = null; + // Upload the new map + mAccountDataRestClient.setAccountData(getMyUser().user_id, AccountDataRestClient.ACCOUNT_DATA_TYPE_DIRECT_MESSAGES, directChatRoomsMap, callback); + } + + /** + * This class defines a direct chat backward compliancyc structure + */ + private class RoomIdsListRetroCompat { + final String mRoomId; + final String mParticipantUserId; + + public RoomIdsListRetroCompat(String aParticipantUserId, String aRoomId) { + mParticipantUserId = aParticipantUserId; + mRoomId = aRoomId; + } + } + + /** + * Return the list of the direct chat room IDs for the user given in parameter.
+ * Based on the account_data map content, the entry associated with aSearchedUserId is returned. + * + * @param aSearchedUserId user ID + * @return the list of the direct chat room Id + */ + public List getDirectChatRoomIdsList(String aSearchedUserId) { + List directChatRoomIdsList = new ArrayList<>(); + IMXStore store = getStore(); + Room room; + + Map> params; + + if (null != store.getDirectChatRoomsDict()) { + params = new HashMap<>(store.getDirectChatRoomsDict()); + if (params.containsKey(aSearchedUserId)) { + directChatRoomIdsList = new ArrayList<>(); + + for (String roomId : params.get(aSearchedUserId)) { + room = store.getRoom(roomId); + if (null != room) { // skipp empty rooms + directChatRoomIdsList.add(roomId); + } + } + } else { + Log.w(LOG_TAG, "## getDirectChatRoomIdsList(): UserId " + aSearchedUserId + " has no entry in account_data"); + } + } else { + Log.w(LOG_TAG, "## getDirectChatRoomIdsList(): failure - getDirectChatRoomsDict()=null"); + } + + return directChatRoomIdsList; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/MXPatterns.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/MXPatterns.java new file mode 100644 index 0000000000..4dbeb50254 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/MXPatterns.java @@ -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 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(); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/MXSession.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/MXSession.java new file mode 100644 index 0000000000..4ce70572ee --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/MXSession.java @@ -0,0 +1,2578 @@ +/* + * 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.IntentFilter; +import android.net.ConnectivityManager; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import com.google.gson.JsonObject; + +import im.vector.matrix.android.BuildConfig; +import im.vector.matrix.android.R; +import im.vector.matrix.android.internal.legacy.call.MXCallsManager; +import im.vector.matrix.android.internal.legacy.crypto.MXCrypto; +import im.vector.matrix.android.internal.legacy.crypto.MXCryptoConfig; +import im.vector.matrix.android.internal.legacy.data.DataRetriever; +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.RoomSummary; +import im.vector.matrix.android.internal.legacy.data.RoomTag; +import im.vector.matrix.android.internal.legacy.data.comparator.RoomComparatorWithTag; +import im.vector.matrix.android.internal.legacy.data.cryptostore.IMXCryptoStore; +import im.vector.matrix.android.internal.legacy.data.cryptostore.MXFileCryptoStore; +import im.vector.matrix.android.internal.legacy.data.metrics.MetricsListener; +import im.vector.matrix.android.internal.legacy.data.store.IMXStore; +import im.vector.matrix.android.internal.legacy.data.store.MXStoreListener; +import im.vector.matrix.android.internal.legacy.db.MXLatestChatMessageCache; +import im.vector.matrix.android.internal.legacy.db.MXMediasCache; +import im.vector.matrix.android.internal.legacy.groups.GroupsManager; +import im.vector.matrix.android.internal.legacy.network.NetworkConnectivityReceiver; +import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback; +import im.vector.matrix.android.internal.legacy.rest.callback.ApiFailureCallback; +import im.vector.matrix.android.internal.legacy.rest.callback.SimpleApiCallback; +import im.vector.matrix.android.internal.legacy.rest.client.AccountDataRestClient; +import im.vector.matrix.android.internal.legacy.rest.client.CallRestClient; +import im.vector.matrix.android.internal.legacy.rest.client.CryptoRestClient; +import im.vector.matrix.android.internal.legacy.rest.client.EventsRestClient; +import im.vector.matrix.android.internal.legacy.rest.client.FilterRestClient; +import im.vector.matrix.android.internal.legacy.rest.client.GroupsRestClient; +import im.vector.matrix.android.internal.legacy.rest.client.LoginRestClient; +import im.vector.matrix.android.internal.legacy.rest.client.MediaScanRestClient; +import im.vector.matrix.android.internal.legacy.rest.client.PresenceRestClient; +import im.vector.matrix.android.internal.legacy.rest.client.ProfileRestClient; +import im.vector.matrix.android.internal.legacy.rest.client.PushRulesRestClient; +import im.vector.matrix.android.internal.legacy.rest.client.PushersRestClient; +import im.vector.matrix.android.internal.legacy.rest.client.RoomsRestClient; +import im.vector.matrix.android.internal.legacy.rest.client.ThirdPidRestClient; +import im.vector.matrix.android.internal.legacy.rest.model.CreateRoomParams; +import im.vector.matrix.android.internal.legacy.rest.model.CreateRoomResponse; +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.ReceiptData; +import im.vector.matrix.android.internal.legacy.rest.model.RoomDirectoryVisibility; +import im.vector.matrix.android.internal.legacy.rest.model.RoomMember; +import im.vector.matrix.android.internal.legacy.rest.model.User; +import im.vector.matrix.android.internal.legacy.rest.model.Versions; +import im.vector.matrix.android.internal.legacy.rest.model.bingrules.BingRule; +import im.vector.matrix.android.internal.legacy.rest.model.filter.FilterBody; +import im.vector.matrix.android.internal.legacy.rest.model.filter.FilterResponse; +import im.vector.matrix.android.internal.legacy.rest.model.login.Credentials; +import im.vector.matrix.android.internal.legacy.rest.model.login.LoginFlow; +import im.vector.matrix.android.internal.legacy.rest.model.login.RegistrationFlowResponse; +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.pid.DeleteDeviceAuth; +import im.vector.matrix.android.internal.legacy.rest.model.pid.DeleteDeviceParams; +import im.vector.matrix.android.internal.legacy.rest.model.search.SearchResponse; +import im.vector.matrix.android.internal.legacy.rest.model.search.SearchUsersResponse; +import im.vector.matrix.android.internal.legacy.rest.model.sync.DevicesListResponse; +import im.vector.matrix.android.internal.legacy.rest.model.sync.RoomResponse; +import im.vector.matrix.android.internal.legacy.sync.DefaultEventsThreadListener; +import im.vector.matrix.android.internal.legacy.sync.EventsThread; +import im.vector.matrix.android.internal.legacy.sync.EventsThreadListener; +import im.vector.matrix.android.internal.legacy.util.BingRulesManager; +import im.vector.matrix.android.internal.legacy.util.ContentManager; +import im.vector.matrix.android.internal.legacy.util.ContentUtils; +import im.vector.matrix.android.internal.legacy.util.FilterUtil; +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.UnsentEventsManager; +import im.vector.matrix.android.internal.legacy.util.VersionsUtil; +import org.matrix.olm.OlmManager; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * Class that represents one user's session with a particular home server. + * There can potentially be multiple sessions for handling multiple accounts. + */ +public class MXSession { + private static final String LOG_TAG = MXSession.class.getSimpleName(); + + private DataRetriever mDataRetriever; + private MXDataHandler mDataHandler; + private EventsThread mEventsThread; + private final Credentials mCredentials; + + // Api clients + private EventsRestClient mEventsRestClient; + private ProfileRestClient mProfileRestClient; + private PresenceRestClient mPresenceRestClient; + private RoomsRestClient mRoomsRestClient; + private final PushRulesRestClient mPushRulesRestClient; + private PushersRestClient mPushersRestClient; + private final ThirdPidRestClient mThirdPidRestClient; + private final CallRestClient mCallRestClient; + private final AccountDataRestClient mAccountDataRestClient; + private final CryptoRestClient mCryptoRestClient; + private final LoginRestClient mLoginRestClient; + private final GroupsRestClient mGroupsRestClient; + private final MediaScanRestClient mMediaScanRestClient; + private final FilterRestClient mFilterRestClient; + + private ApiFailureCallback mFailureCallback; + + private ContentManager mContentManager; + + public MXCallsManager mCallsManager; + + private MetricsListener mMetricsListener; + + private Context mAppContent; + private NetworkConnectivityReceiver mNetworkConnectivityReceiver; + private UnsentEventsManager mUnsentEventsManager; + + private MXLatestChatMessageCache mLatestChatMessageCache; + private MXMediasCache mMediasCache; + + private BingRulesManager mBingRulesManager = null; + + private boolean mIsAliveSession = true; + + // online status + private boolean mIsOnline = false; + private int mSyncTimeout = 0; + private int mSyncDelay = 0; + + private final HomeServerConnectionConfig mHsConfig; + + // True if file encryption is enabled + private boolean mEnableFileEncryption; + + // the application is launched from a notification + // so, mEventsThread.start might be not ready + private boolean mIsBgCatchupPending = false; + + private FilterBody mCurrentFilter = new FilterBody(); + + // tell if the data save mode is enabled + private boolean mUseDataSaveMode; + + // the groups manager + private GroupsManager mGroupsManager; + + // load the crypto libs. + public static OlmManager mOlmManager = new OlmManager(); + + /** + * Create a basic session for direct API calls. + * + * @param hsConfig the home server connection config + */ + private MXSession(HomeServerConnectionConfig hsConfig) { + mCredentials = hsConfig.getCredentials(); + mHsConfig = hsConfig; + + mEventsRestClient = new EventsRestClient(hsConfig); + mProfileRestClient = new ProfileRestClient(hsConfig); + mPresenceRestClient = new PresenceRestClient(hsConfig); + mRoomsRestClient = new RoomsRestClient(hsConfig); + mPushRulesRestClient = new PushRulesRestClient(hsConfig); + mPushersRestClient = new PushersRestClient(hsConfig); + mThirdPidRestClient = new ThirdPidRestClient(hsConfig); + mCallRestClient = new CallRestClient(hsConfig); + mAccountDataRestClient = new AccountDataRestClient(hsConfig); + mCryptoRestClient = new CryptoRestClient(hsConfig); + mLoginRestClient = new LoginRestClient(hsConfig); + mGroupsRestClient = new GroupsRestClient(hsConfig); + mMediaScanRestClient = new MediaScanRestClient(hsConfig); + mFilterRestClient = new FilterRestClient(hsConfig); + } + + /** + * Create a user session with a data handler. + * Private, please use the MxSession.Builder now + * + * @param hsConfig the home server connection config + * @param dataHandler the data handler + * @param appContext the application context + */ + private MXSession(HomeServerConnectionConfig hsConfig, MXDataHandler dataHandler, Context appContext) { + this(hsConfig); + mDataHandler = dataHandler; + + mDataHandler.getStore().addMXStoreListener(new MXStoreListener() { + @Override + public void onStoreReady(String accountId) { + Log.d(LOG_TAG, "## onStoreReady()"); + getDataHandler().onStoreReady(); + } + + @Override + public void onStoreCorrupted(String accountId, String description) { + Log.d(LOG_TAG, "## onStoreCorrupted() : token " + getDataHandler().getStore().getEventStreamToken()); + + // nothing was saved + if (null == getDataHandler().getStore().getEventStreamToken()) { + getDataHandler().onStoreReady(); + } + } + + @Override + public void postProcess(String accountId) { + getDataHandler().checkPermanentStorageData(); + + // test if the crypto instance has already been created + if (null == mCrypto) { + MXFileCryptoStore store = new MXFileCryptoStore(mEnableFileEncryption); + store.initWithCredentials(mAppContent, mCredentials); + + if (store.hasData() || mEnableCryptoWhenStartingMXSession) { + Log.d(LOG_TAG, "## postProcess() : create the crypto instance for session " + this); + checkCrypto(); + } else { + Log.e(LOG_TAG, "## postProcess() : no crypto data"); + } + } else { + Log.e(LOG_TAG, "## postProcess() : mCrypto is already created"); + } + } + + @Override + public void onReadReceiptsLoaded(final String roomId) { + final List receipts = mDataHandler.getStore().getEventReceipts(roomId, null, false, false); + final List senders = new ArrayList<>(); + + for (ReceiptData receipt : receipts) { + senders.add(receipt.userId); + } + + mDataHandler.onReceiptEvent(roomId, senders); + } + }); + + // Initialize a data retriever with rest clients + mDataRetriever = new DataRetriever(); + mDataRetriever.setRoomsRestClient(mRoomsRestClient); + mDataHandler.setDataRetriever(mDataRetriever); + mDataHandler.setProfileRestClient(mProfileRestClient); + mDataHandler.setPresenceRestClient(mPresenceRestClient); + mDataHandler.setThirdPidRestClient(mThirdPidRestClient); + mDataHandler.setRoomsRestClient(mRoomsRestClient); + mDataHandler.setEventsRestClient(mEventsRestClient); + mDataHandler.setAccountDataRestClient(mAccountDataRestClient); + + // application context + mAppContent = appContext; + + mNetworkConnectivityReceiver = new NetworkConnectivityReceiver(); + mNetworkConnectivityReceiver.checkNetworkConnection(appContext); + mDataHandler.setNetworkConnectivityReceiver(mNetworkConnectivityReceiver); + mAppContent.registerReceiver(mNetworkConnectivityReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); + + mBingRulesManager = new BingRulesManager(this, mNetworkConnectivityReceiver); + mDataHandler.setPushRulesManager(mBingRulesManager); + + mUnsentEventsManager = new UnsentEventsManager(mNetworkConnectivityReceiver, mDataHandler); + + mContentManager = new ContentManager(hsConfig, mUnsentEventsManager); + + // + mCallsManager = new MXCallsManager(this, mAppContent); + mDataHandler.setCallsManager(mCallsManager); + + // the rest client + mEventsRestClient.setUnsentEventsManager(mUnsentEventsManager); + + mProfileRestClient.setUnsentEventsManager(mUnsentEventsManager); + mPresenceRestClient.setUnsentEventsManager(mUnsentEventsManager); + mRoomsRestClient.setUnsentEventsManager(mUnsentEventsManager); + mPushRulesRestClient.setUnsentEventsManager(mUnsentEventsManager); + mThirdPidRestClient.setUnsentEventsManager(mUnsentEventsManager); + mCallRestClient.setUnsentEventsManager(mUnsentEventsManager); + mAccountDataRestClient.setUnsentEventsManager(mUnsentEventsManager); + mCryptoRestClient.setUnsentEventsManager(mUnsentEventsManager); + mLoginRestClient.setUnsentEventsManager(mUnsentEventsManager); + mGroupsRestClient.setUnsentEventsManager(mUnsentEventsManager); + + // return the default cache manager + mLatestChatMessageCache = new MXLatestChatMessageCache(mCredentials.userId); + mMediasCache = new MXMediasCache(mContentManager, mNetworkConnectivityReceiver, mCredentials.userId, appContext); + mDataHandler.setMediasCache(mMediasCache); + + mMediaScanRestClient.setMxStore(mDataHandler.getStore()); + mMediasCache.setMediaScanRestClient(mMediaScanRestClient); + + mGroupsManager = new GroupsManager(mDataHandler, mGroupsRestClient); + mDataHandler.setGroupsManager(mGroupsManager); + } + + private void checkIfAlive() { + synchronized (this) { + if (!mIsAliveSession) { + // Create an Exception to log the stack trace + Log.e(LOG_TAG, "Use of a released session", new Exception("Use of a released session")); + + //throw new AssertionError("Should not used a cleared mxsession "); + } + } + } + + /** + * Init the user-agent used by the REST requests. + * + * @param context the application context + */ + public static void initUserAgent(Context context) { + RestClient.initUserAgent(context); + } + + /** + * Provides the crypto lib version. + * + * @param context the context + * @param longFormat true to have a long version (with date and time) + * @return the crypto lib version + */ + public String getCryptoVersion(Context context, boolean longFormat) { + String version = ""; + + if (null != mOlmManager) { + version = longFormat ? mOlmManager.getDetailedVersion(context) : mOlmManager.getVersion(); + } + + return version; + } + + /** + * Get the data handler. + * + * @return the data handler. + */ + public MXDataHandler getDataHandler() { + checkIfAlive(); + return mDataHandler; + } + + /** + * Get the user credentials. + * + * @return the credentials + */ + public Credentials getCredentials() { + checkIfAlive(); + return mCredentials; + } + + /** + * Get the API client for requests to the events API. + * + * @return the events API client + */ + public EventsRestClient getEventsApiClient() { + checkIfAlive(); + return mEventsRestClient; + } + + /** + * Get the API client for requests to the profile API. + * + * @return the profile API client + */ + public ProfileRestClient getProfileApiClient() { + checkIfAlive(); + return mProfileRestClient; + } + + /** + * Get the API client for requests to the presence API. + * + * @return the presence API client + */ + public PresenceRestClient getPresenceApiClient() { + checkIfAlive(); + return mPresenceRestClient; + } + + public FilterRestClient getFilterRestClient() { + checkIfAlive(); + return mFilterRestClient; + } + + /** + * Refresh the presence info of a dedicated user. + * + * @param userId the user userID. + * @param callback the callback. + */ + public void refreshUserPresence(final String userId, final ApiCallback callback) { + mPresenceRestClient.getPresence(userId, new SimpleApiCallback(callback) { + @Override + public void onSuccess(User user) { + User currentUser = mDataHandler.getStore().getUser(userId); + + if (null != currentUser) { + currentUser.presence = user.presence; + currentUser.currently_active = user.currently_active; + currentUser.lastActiveAgo = user.lastActiveAgo; + } else { + currentUser = user; + } + + currentUser.setLatestPresenceTs(System.currentTimeMillis()); + mDataHandler.getStore().storeUser(currentUser); + if (null != callback) { + callback.onSuccess(null); + } + } + }); + } + + /** + * Get the API client for requests to the bing rules API. + * + * @return the bing rules API client + */ + public PushRulesRestClient getBingRulesApiClient() { + checkIfAlive(); + return mPushRulesRestClient; + } + + public ThirdPidRestClient getThirdPidRestClient() { + checkIfAlive(); + return mThirdPidRestClient; + } + + public CallRestClient getCallRestClient() { + checkIfAlive(); + return mCallRestClient; + } + + public PushersRestClient getPushersRestClient() { + checkIfAlive(); + return mPushersRestClient; + } + + public CryptoRestClient getCryptoRestClient() { + checkIfAlive(); + return mCryptoRestClient; + } + + public HomeServerConnectionConfig getHomeServerConfig() { + checkIfAlive(); + return mHsConfig; + } + + /** + * Get the API client for requests to the rooms API. + * + * @return the rooms API client + */ + public RoomsRestClient getRoomsApiClient() { + checkIfAlive(); + return mRoomsRestClient; + } + + public MediaScanRestClient getMediaScanRestClient() { + checkIfAlive(); + return mMediaScanRestClient; + } + + protected void setEventsApiClient(EventsRestClient eventsRestClient) { + checkIfAlive(); + mEventsRestClient = eventsRestClient; + } + + protected void setProfileApiClient(ProfileRestClient profileRestClient) { + checkIfAlive(); + mProfileRestClient = profileRestClient; + } + + protected void setPresenceApiClient(PresenceRestClient presenceRestClient) { + checkIfAlive(); + mPresenceRestClient = presenceRestClient; + } + + protected void setRoomsApiClient(RoomsRestClient roomsRestClient) { + checkIfAlive(); + mRoomsRestClient = roomsRestClient; + } + + public MXLatestChatMessageCache getLatestChatMessageCache() { + checkIfAlive(); + return mLatestChatMessageCache; + } + + public MXMediasCache getMediasCache() { + checkIfAlive(); + return mMediasCache; + } + + /** + * Provides the application caches size. + * + * @param context the context + * @param callback the asynchronous callback + */ + public static void getApplicationSizeCaches(final Context context, final ApiCallback callback) { + AsyncTask task = new AsyncTask() { + @Override + protected Long doInBackground(Void... params) { + return ContentUtils.getDirectorySize(context, context.getApplicationContext().getFilesDir().getParentFile(), 5); + } + + @Override + protected void onPostExecute(Long result) { + Log.d(LOG_TAG, "## getCacheSize() : " + result); + if (null != callback) { + callback.onSuccess(result); + } + } + }; + try { + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } catch (final Exception e) { + Log.e(LOG_TAG, "## getApplicationSizeCaches() : 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); + } + } + }); + + } + } + + /** + * Clear the application cache + */ + private void clearApplicationCaches(Context context) { + mDataHandler.clear(); + + // network event will not be listened anymore + try { + mAppContent.unregisterReceiver(mNetworkConnectivityReceiver); + } catch (Exception e) { + Log.e(LOG_TAG, "## clearApplicationCaches() : unregisterReceiver failed " + e.getMessage(), e); + } + mNetworkConnectivityReceiver.removeListeners(); + + // auto resent messages will not be resent + mUnsentEventsManager.clear(); + + mLatestChatMessageCache.clearCache(context); + mMediasCache.clear(); + + if (null != mCrypto) { + mCrypto.close(); + } + } + + /** + * Clear the session data synchronously. + * + * @param context the context + */ + public void clear(final Context context) { + clear(context, null); + } + + /** + * Clear the session data. + * if the callback is null, the clear is synchronous. + * + * @param context the context + * @param callback the asynchronous callback + */ + public void clear(final Context context, final ApiCallback callback) { + synchronized (this) { + if (!mIsAliveSession) { + Log.e(LOG_TAG, "## clear() was already called"); + return; + } + + mIsAliveSession = false; + } + + // stop events stream + stopEventStream(); + + if (null == callback) { + clearApplicationCaches(context); + } else { + // clear the caches in a background thread to avoid blocking the UI thread + AsyncTask task = new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + clearApplicationCaches(context); + return null; + } + + @Override + protected void onPostExecute(Void args) { + if (null != callback) { + callback.onSuccess(null); + } + } + }; + + try { + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } catch (final Exception e) { + Log.e(LOG_TAG, "## clear() 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); + } + } + }); + } + } + } + + /** + * Remove the medias older than the provided timestamp. + * + * @param context the context + * @param timestamp the timestamp (in seconds) + */ + public void removeMediasBefore(final Context context, final long timestamp) { + // list the files to keep even if they are older than the provided timestamp + // because their upload failed + final Set filesToKeep = new HashSet<>(); + IMXStore store = getDataHandler().getStore(); + + Collection rooms = store.getRooms(); + + for (Room room : rooms) { + Collection events = store.getRoomMessages(room.getRoomId()); + if (null != events) { + for (Event event : events) { + try { + Message message = null; + + if (TextUtils.equals(Event.EVENT_TYPE_MESSAGE, event.getType())) { + message = JsonUtils.toMessage(event.getContent()); + } else if (TextUtils.equals(Event.EVENT_TYPE_STICKER, event.getType())) { + message = JsonUtils.toStickerMessage(event.getContent()); + } + + if (null != message && message instanceof MediaMessage) { + MediaMessage mediaMessage = (MediaMessage) message; + + if (mediaMessage.isThumbnailLocalContent()) { + filesToKeep.add(Uri.parse(mediaMessage.getThumbnailUrl()).getPath()); + } + + if (mediaMessage.isLocalContent()) { + filesToKeep.add(Uri.parse(mediaMessage.getUrl()).getPath()); + } + } + } catch (Exception e) { + Log.e(LOG_TAG, "## removeMediasBefore() : failed " + e.getMessage(), e); + } + } + } + } + + AsyncTask task = new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + long length = getMediasCache().removeMediasBefore(timestamp, filesToKeep); + + // delete also the log files + // they might be large + File logsDir = Log.getLogDirectory(); + + if (null != logsDir) { + File[] logFiles = logsDir.listFiles(); + + if (null != logFiles) { + for (File file : logFiles) { + if (ContentUtils.getLastAccessTime(file) < timestamp) { + length += file.length(); + file.delete(); + } + } + } + } + + if (0 != length) { + Log.d(LOG_TAG, "## removeMediasBefore() : save " + android.text.format.Formatter.formatFileSize(context, length)); + } else { + Log.d(LOG_TAG, "## removeMediasBefore() : useless"); + } + + return null; + } + }; + try { + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } catch (Exception e) { + Log.e(LOG_TAG, "## removeMediasBefore() : failed " + e.getMessage(), e); + task.cancel(true); + } + } + + /** + * @return true if the session is active i.e. has not been cleared after a logout. + */ + public boolean isAlive() { + synchronized (this) { + return mIsAliveSession; + } + } + + /** + * Get the content manager (for uploading and downloading content) associated with the session. + * + * @return the content manager + */ + public ContentManager getContentManager() { + checkIfAlive(); + return mContentManager; + } + + /** + * Get the session's current user. The MyUser object provides methods for updating user properties which are not possible for other users. + * + * @return the session's MyUser object + */ + public MyUser getMyUser() { + checkIfAlive(); + + return mDataHandler.getMyUser(); + } + + + /** + * Get the session's current userid. + * + * @return the session's MyUser id + */ + public String getMyUserId() { + checkIfAlive(); + + if (null != mDataHandler.getMyUser()) { + return mDataHandler.getMyUser().user_id; + } + return null; + } + + /** + * Start the event stream (events thread that listens for events) with an event listener. + * + * @param anEventsListener the event listener or null if using a DataHandler + * @param networkConnectivityReceiver the network connectivity listener. + * @param initialToken the initial sync token (null to start from scratch) + */ + public void startEventStream(final EventsThreadListener anEventsListener, + final NetworkConnectivityReceiver networkConnectivityReceiver, + final String initialToken) { + checkIfAlive(); + + // reported by a rageshake issue + // startEventStream might be called several times + // when the service is killed and automatically restarted. + // It might be restarted by itself and by android at the same time. + synchronized (LOG_TAG) { + if (mEventsThread != null) { + if (!mEventsThread.isAlive()) { + mEventsThread = null; + Log.e(LOG_TAG, "startEventStream() : create a new EventsThread"); + } else { + // https://github.com/vector-im/riot-android/issues/1331 + mEventsThread.cancelKill(); + Log.e(LOG_TAG, "Ignoring startEventStream() : Thread already created."); + return; + } + } + + if (mDataHandler == null) { + Log.e(LOG_TAG, "Error starting the event stream: No data handler is defined"); + return; + } + + Log.d(LOG_TAG, "startEventStream : create the event stream"); + + final EventsThreadListener fEventsListener = (null == anEventsListener) ? new DefaultEventsThreadListener(mDataHandler) : anEventsListener; + + mEventsThread = new EventsThread(mAppContent, mEventsRestClient, fEventsListener, initialToken); + setSyncFilter(mCurrentFilter); + mEventsThread.setMetricsListener(mMetricsListener); + mEventsThread.setNetworkConnectivityReceiver(networkConnectivityReceiver); + mEventsThread.setIsOnline(mIsOnline); + mEventsThread.setServerLongPollTimeout(mSyncTimeout); + mEventsThread.setSyncDelay(mSyncDelay); + + if (mFailureCallback != null) { + mEventsThread.setFailureCallback(mFailureCallback); + } + + if (mCredentials.accessToken != null && !mEventsThread.isAlive()) { + // GA issue + try { + mEventsThread.start(); + } catch (Exception e) { + Log.e(LOG_TAG, "## startEventStream() : mEventsThread.start failed " + e.getMessage(), e); + } + + if (mIsBgCatchupPending) { + Log.d(LOG_TAG, "startEventStream : start a catchup"); + mIsBgCatchupPending = false; + // catchup retrieve any available messages before stop the sync + mEventsThread.catchup(); + } + } + } + } + + /** + * Refresh the access token + */ + public void refreshToken() { + checkIfAlive(); + + mProfileRestClient.refreshTokens(new ApiCallback() { + @Override + public void onSuccess(Credentials info) { + Log.d(LOG_TAG, "refreshToken : succeeds."); + } + + @Override + public void onNetworkError(Exception e) { + Log.e(LOG_TAG, "refreshToken : onNetworkError " + e.getMessage(), e); + } + + @Override + public void onMatrixError(MatrixError e) { + Log.e(LOG_TAG, "refreshToken : onMatrixError " + e.getMessage()); + } + + @Override + public void onUnexpectedError(Exception e) { + Log.e(LOG_TAG, "refreshToken : onMatrixError " + e.getMessage(), e); + } + }); + } + + /** + * Update the online status + * + * @param isOnline true if the client must be seen as online + */ + public void setIsOnline(boolean isOnline) { + if (isOnline != mIsOnline) { + mIsOnline = isOnline; + + if (null != mEventsThread) { + mEventsThread.setIsOnline(isOnline); + } + } + } + + /** + * @return true if the client is seen as "online" + */ + public boolean isOnline() { + return mIsOnline; + } + + /** + * Update the heartbeat request timeout. + * + * @param ms the delay in ms + */ + public void setSyncTimeout(int ms) { + mSyncTimeout = ms; + if (null != mEventsThread) { + mEventsThread.setServerLongPollTimeout(ms); + } + } + + /** + * @return the heartbeat request timeout + */ + public int getSyncTimeout() { + return mSyncTimeout; + } + + /** + * Set a delay between two sync requests. + * + * @param ms the delay in ms + */ + public void setSyncDelay(int ms) { + mSyncDelay = ms; + if (null != mEventsThread) { + mEventsThread.setSyncDelay(ms); + } + } + + /** + * @return the delay between two sync requests. + */ + public int getSyncDelay() { + return mSyncDelay; + } + + /** + * Update the data save mode. + * + * @param enabled true to enable the data save mode + */ + public void setUseDataSaveMode(boolean enabled) { + mUseDataSaveMode = enabled; + + if (mEventsThread != null) { + setSyncFilter(mCurrentFilter); + } + } + + /** + * Allows setting the filter used by the EventsThread + * + * @param filter the content of the filter param on sync requests + */ + public synchronized void setSyncFilter(FilterBody filter) { + Log.d(LOG_TAG, "setSyncFilter ## " + filter); + mCurrentFilter = filter; + + // Enable Data save mode and/or LazyLoading + FilterUtil.enableDataSaveMode(mCurrentFilter, mUseDataSaveMode); + FilterUtil.enableLazyLoading(mCurrentFilter, mDataHandler.isLazyLoadingEnabled()); + + convertFilterToFilterId(); + } + + /** + * Convert a filter to a filterId + * Either it is already known to the server, or send the filter to the server to get a filterId + */ + private void convertFilterToFilterId() { + // Ensure mCurrentFilter has not been updated in the same time + final String wantedJsonFilter = mCurrentFilter.toJSONString(); + + // Check if the current filter is known by the server, to directly use the filterId + String filterId = getDataHandler().getStore().getFilters().get(wantedJsonFilter); + + if (TextUtils.isEmpty(filterId)) { + // enable the filter in JSON representation so do not block sync until the filter response is there + mEventsThread.setFilterOrFilterId(wantedJsonFilter); + + // Send the filter to the server + mFilterRestClient.uploadFilter(getMyUserId(), mCurrentFilter, new SimpleApiCallback() { + @Override + public void onSuccess(FilterResponse filter) { + // Store the couple filter/filterId + getDataHandler().getStore().addFilter(wantedJsonFilter, filter.filterId); + + // Ensure the filter is still corresponding to the current filter + if (TextUtils.equals(wantedJsonFilter, mCurrentFilter.toJSONString())) { + // Tell the event thread to use the id now + mEventsThread.setFilterOrFilterId(filter.filterId); + } + } + }); + } else { + // Tell the event thread to use the id now + mEventsThread.setFilterOrFilterId(filterId); + } + } + + /** + * Refresh the network connection information. + * On android version older than 6.0, the doze mode might have killed the network connection. + */ + public void refreshNetworkConnection() { + if (null != mNetworkConnectivityReceiver) { + // mNetworkConnectivityReceiver is a broadcastReceiver + // but some users reported that the network updates were not dispatched + mNetworkConnectivityReceiver.checkNetworkConnection(mAppContent); + } + } + + /** + * Shorthand for {@link #startEventStream(EventsThreadListener, NetworkConnectivityReceiver, String)} with no eventListener + * using a DataHandler and no specific failure callback. + * + * @param initialToken the initial sync token (null to sync from scratch). + */ + public void startEventStream(String initialToken) { + checkIfAlive(); + startEventStream(null, mNetworkConnectivityReceiver, initialToken); + } + + /** + * Gracefully stop the event stream. + */ + public void stopEventStream() { + if (null != mCallsManager) { + mCallsManager.stopTurnServerRefresh(); + } + + if (null != mEventsThread) { + Log.d(LOG_TAG, "stopEventStream"); + + mEventsThread.kill(); + mEventsThread = null; + } else { + Log.e(LOG_TAG, "stopEventStream : mEventsThread is already null"); + } + } + + /** + * Pause the event stream + */ + public void pauseEventStream() { + checkIfAlive(); + + if (null != mCallsManager) { + mCallsManager.pauseTurnServerRefresh(); + } + + if (null != mEventsThread) { + Log.d(LOG_TAG, "pauseEventStream"); + mEventsThread.pause(); + } else { + Log.e(LOG_TAG, "pauseEventStream : mEventsThread is null"); + } + + if (null != getMediasCache()) { + getMediasCache().clearTmpDecryptedMediaCache(); + } + + if (null != mGroupsManager) { + mGroupsManager.onSessionPaused(); + } + } + + /** + * @return the current sync token + */ + public String getCurrentSyncToken() { + return (null != mEventsThread) ? mEventsThread.getCurrentSyncToken() : null; + } + + /** + * Resume the event stream + */ + public void resumeEventStream() { + checkIfAlive(); + + if (null != mNetworkConnectivityReceiver) { + // mNetworkConnectivityReceiver is a broadcastReceiver + // but some users reported that the network updates were not dispatched + mNetworkConnectivityReceiver.checkNetworkConnection(mAppContent); + } + + if (null != mCallsManager) { + mCallsManager.unpauseTurnServerRefresh(); + } + + if (null != mEventsThread) { + Log.d(LOG_TAG, "## resumeEventStream() : unpause"); + mEventsThread.unpause(); + } else { + Log.e(LOG_TAG, "resumeEventStream : mEventsThread is null"); + } + + if (mIsBgCatchupPending) { + mIsBgCatchupPending = false; + Log.d(LOG_TAG, "## resumeEventStream() : cancel bg sync"); + } + + if (null != getMediasCache()) { + getMediasCache().clearShareDecryptedMediaCache(); + } + + if (null != mGroupsManager) { + mGroupsManager.onSessionResumed(); + } + } + + /** + * Trigger a catchup + */ + public void catchupEventStream() { + checkIfAlive(); + + if (null != mEventsThread) { + Log.d(LOG_TAG, "catchupEventStream"); + mEventsThread.catchup(); + } else { + Log.e(LOG_TAG, "catchupEventStream : mEventsThread is null so catchup when the thread will be created"); + mIsBgCatchupPending = true; + } + } + + /** + * Set a global failure callback implementation. + * + * @param failureCallback the failure callback + */ + public void setFailureCallback(ApiFailureCallback failureCallback) { + checkIfAlive(); + + mFailureCallback = failureCallback; + if (mEventsThread != null) { + mEventsThread.setFailureCallback(failureCallback); + } + } + + /** + * Create a new room. + * + * @param callback the async callback once the room is ready + */ + public void createRoom(final ApiCallback callback) { + createRoom(null, null, null, callback); + } + + /** + * Create a new room with given properties. Needs the data handler. + * + * @param name the room name + * @param topic the room topic + * @param alias the room alias + * @param callback the async callback once the room is ready + */ + public void createRoom(String name, String topic, String alias, final ApiCallback callback) { + createRoom(name, topic, RoomDirectoryVisibility.DIRECTORY_VISIBILITY_PRIVATE, alias, null, callback); + } + + /** + * Create a new room with given properties. Needs the data handler. + * + * @param name the room name + * @param topic the room topic + * @param visibility the room visibility + * @param alias the room alias + * @param algorithm the crypto algorithm (null to create an unencrypted room) + * @param callback the async callback once the room is ready + */ + public void createRoom(String name, + String topic, + String visibility, + String alias, + String algorithm, + final ApiCallback callback) { + checkIfAlive(); + + CreateRoomParams params = new CreateRoomParams(); + params.name = !TextUtils.isEmpty(name) ? name : null; + params.topic = !TextUtils.isEmpty(topic) ? topic : null; + params.visibility = !TextUtils.isEmpty(visibility) ? visibility : null; + params.roomAliasName = !TextUtils.isEmpty(alias) ? alias : null; + params.addCryptoAlgorithm(algorithm); + + createRoom(params, callback); + } + + /** + * Create an encrypted room. + * + * @param algorithm the encryption algorithm. + * @param callback the async callback once the room is ready + */ + public void createEncryptedRoom(String algorithm, final ApiCallback callback) { + CreateRoomParams params = new CreateRoomParams(); + params.addCryptoAlgorithm(algorithm); + createRoom(params, callback); + } + + /** + * Create a direct message room with one participant.
+ * The participant can be a user ID or mail address. Once the room is created, on success, the room + * is set as a "direct message" with the participant. + * + * @param aParticipantUserId user ID (or user mail) to be invited in the direct message room + * @param aCreateRoomCallBack async call back response + * @return true if the invite was performed, false otherwise + */ + public boolean createDirectMessageRoom(final String aParticipantUserId, final ApiCallback aCreateRoomCallBack) { + return createDirectMessageRoom(aParticipantUserId, null, aCreateRoomCallBack); + } + + /** + * Create a direct message room with one participant.
+ * The participant can be a user ID or mail address. Once the room is created, on success, the room + * is set as a "direct message" with the participant. + * + * @param aParticipantUserId user ID (or user mail) to be invited in the direct message room + * @param algorithm the crypto algorithm (null to create an unencrypted room) + * @param aCreateRoomCallBack async call back response + * @return true if the invite was performed, false otherwise + */ + public boolean createDirectMessageRoom(final String aParticipantUserId, final String algorithm, final ApiCallback aCreateRoomCallBack) { + boolean retCode = false; + + if (!TextUtils.isEmpty(aParticipantUserId)) { + retCode = true; + CreateRoomParams params = new CreateRoomParams(); + + params.addCryptoAlgorithm(algorithm); + params.setDirectMessage(); + params.addParticipantIds(mHsConfig, Arrays.asList(aParticipantUserId)); + + createRoom(params, aCreateRoomCallBack); + } + + return retCode; + } + + /** + * Finalise the created room as a direct chat one. + * + * @param roomId the room id + * @param userId the user id + * @param callback the asynchronous callback + */ + private void finalizeDMRoomCreation(final String roomId, String userId, final ApiCallback callback) { + final String fRoomId = roomId; + + toggleDirectChatRoom(roomId, userId, new SimpleApiCallback(callback) { + @Override + public void onSuccess(Void info) { + Room room = getDataHandler().getRoom(fRoomId); + + if (null != room) { + room.markAllAsRead(null); + } + + if (null != callback) { + callback.onSuccess(fRoomId); + } + } + }); + } + + /** + * Create a new room with given properties. + * + * @param params the creation parameters. + * @param callback the async callback once the room is ready + */ + public void createRoom(final CreateRoomParams params, final ApiCallback callback) { + mRoomsRestClient.createRoom(params, new SimpleApiCallback(callback) { + @Override + public void onSuccess(CreateRoomResponse info) { + final String roomId = info.roomId; + final Room createdRoom = mDataHandler.getRoom(roomId); + + // the creation events are not be called during the creation + if (!createdRoom.isJoined()) { + createdRoom.setOnInitialSyncCallback(new SimpleApiCallback(callback) { + @Override + public void onSuccess(Void info) { + createdRoom.markAllAsRead(null); + + if (params.isDirect()) { + finalizeDMRoomCreation(roomId, params.getFirstInvitedUserId(), callback); + } else { + callback.onSuccess(roomId); + } + } + }); + } else { + createdRoom.markAllAsRead(null); + + if (params.isDirect()) { + finalizeDMRoomCreation(roomId, params.getFirstInvitedUserId(), callback); + } else { + callback.onSuccess(roomId); + } + } + } + }); + } + + /** + * Join a room by its roomAlias + * + * @param roomIdOrAlias the room alias + * @param callback the async callback once the room is joined. The RoomId is provided. + */ + public void joinRoom(String roomIdOrAlias, final ApiCallback callback) { + checkIfAlive(); + + // sanity check + if ((null != mDataHandler) && (null != roomIdOrAlias)) { + mDataRetriever.getRoomsRestClient().joinRoom(roomIdOrAlias, new SimpleApiCallback(callback) { + @Override + public void onSuccess(final RoomResponse roomResponse) { + final String roomId = roomResponse.roomId; + Room joinedRoom = mDataHandler.getRoom(roomId); + + // wait until the initial sync is done + if (!joinedRoom.isJoined()) { + joinedRoom.setOnInitialSyncCallback(new SimpleApiCallback(callback) { + @Override + public void onSuccess(Void info) { + callback.onSuccess(roomId); + } + }); + } else { + // to initialise the notification counters + joinedRoom.markAllAsRead(null); + callback.onSuccess(roomId); + } + } + }); + } + } + + /** + * Send the read receipts to the latest room messages. + * + * @param rooms the rooms list + * @param callback the asynchronous callback + */ + public void markRoomsAsRead(final Collection rooms, final ApiCallback callback) { + if ((null == rooms) || (0 == rooms.size())) { + if (null != callback) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + callback.onSuccess(null); + } + }); + } + return; + } + + markRoomsAsRead(rooms.iterator(), callback); + } + + /** + * Send the read receipts to the latest room messages. + * + * @param roomsIterator the rooms list iterator + * @param callback the asynchronous callback + */ + private void markRoomsAsRead(final Iterator roomsIterator, final ApiCallback callback) { + if (roomsIterator.hasNext()) { + Room room = (Room) roomsIterator.next(); + boolean isRequestSent = false; + + if (mNetworkConnectivityReceiver.isConnected()) { + isRequestSent = room.markAllAsRead(new SimpleApiCallback(callback) { + @Override + public void onSuccess(Void anything) { + markRoomsAsRead(roomsIterator, callback); + } + }); + } else { + // update the local data + room.sendReadReceipt(); + } + + if (!isRequestSent) { + markRoomsAsRead(roomsIterator, callback); + } + + } else { + if (null != callback) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + callback.onSuccess(null); + } + }); + } + } + } + + /** + * Retrieve user matrix id from a 3rd party id. + * + * @param address the user id. + * @param media the media. + * @param callback the 3rd party callback + */ + public void lookup3Pid(String address, String media, final ApiCallback callback) { + checkIfAlive(); + + mThirdPidRestClient.lookup3Pid(address, media, callback); + } + + /** + * Retrieve user matrix id from a 3rd party id. + * + * @param addresses 3rd party ids + * @param mediums the medias. + * @param callback the 3rd parties callback + */ + public void lookup3Pids(List addresses, List mediums, ApiCallback> callback) { + checkIfAlive(); + + mThirdPidRestClient.lookup3Pids(addresses, mediums, callback); + } + + /** + * Perform a remote text search. + * + * @param text the text to search for. + * @param rooms a list of rooms to search in. nil means all rooms the user is in. + * @param beforeLimit the number of events to get before the matching results. + * @param afterLimit the number of events to get after the matching results. + * @param nextBatch the token to pass for doing pagination from a previous response. + * @param callback the request callback + */ + public void searchMessageText(String text, + List rooms, + int beforeLimit, + int afterLimit, + String nextBatch, + final ApiCallback callback) { + checkIfAlive(); + if (null != callback) { + mEventsRestClient.searchMessagesByText(text, rooms, beforeLimit, afterLimit, nextBatch, callback); + } + } + + /** + * Perform a remote text search. + * + * @param text the text to search for. + * @param rooms a list of rooms to search in. nil means all rooms the user is in. + * @param nextBatch the token to pass for doing pagination from a previous response. + * @param callback the request callback + */ + public void searchMessagesByText(String text, List rooms, String nextBatch, final ApiCallback callback) { + checkIfAlive(); + if (null != callback) { + mEventsRestClient.searchMessagesByText(text, rooms, 0, 0, nextBatch, callback); + } + } + + /** + * Perform a remote text search. + * + * @param text the text to search for. + * @param nextBatch the token to pass for doing pagination from a previous response. + * @param callback the request callback + */ + public void searchMessagesByText(String text, String nextBatch, final ApiCallback callback) { + checkIfAlive(); + if (null != callback) { + mEventsRestClient.searchMessagesByText(text, null, 0, 0, nextBatch, callback); + } + } + + /** + * Cancel any pending search request + */ + public void cancelSearchMessagesByText() { + checkIfAlive(); + mEventsRestClient.cancelSearchMessagesByText(); + } + + /** + * Perform a remote text search for a dedicated media types list + * + * @param name the text to search for. + * @param rooms a list of rooms to search in. nil means all rooms the user is in. + * @param nextBatch the token to pass for doing pagination from a previous response. + * @param callback the request callback + */ + public void searchMediasByName(String name, List rooms, String nextBatch, final ApiCallback callback) { + checkIfAlive(); + + if (null != callback) { + mEventsRestClient.searchMediasByText(name, rooms, 0, 0, nextBatch, callback); + } + } + + /** + * Cancel any pending file search request + */ + public void cancelSearchMediasByText() { + checkIfAlive(); + mEventsRestClient.cancelSearchMediasByText(); + } + + /** + * Perform a remote users search by name / user id. + * + * @param name the text to search for. + * @param limit the maximum number of items to retrieve (can be null) + * @param userIdsFilter the user ids filter (can be null) + * @param callback the callback + */ + public void searchUsers(String name, Integer limit, Set userIdsFilter, final ApiCallback callback) { + checkIfAlive(); + + if (null != callback) { + mEventsRestClient.searchUsers(name, limit, userIdsFilter, callback); + } + } + + /** + * Cancel any pending user search + */ + public void cancelUsersSearch() { + checkIfAlive(); + mEventsRestClient.cancelUsersSearch(); + } + + + /** + * Return the fulfilled active BingRule for the event. + * + * @param event the event + * @return the fulfilled bingRule + */ + public BingRule fulfillRule(Event event) { + checkIfAlive(); + return mBingRulesManager.fulfilledBingRule(event); + } + + /** + * @return true if the calls are supported + */ + public boolean isVoipCallSupported() { + if (null != mCallsManager) { + return mCallsManager.isSupported(); + } else { + return false; + } + } + + /** + * Get the list of rooms that are tagged the specified tag. + * The returned array is ordered according to the room tag order. + * + * @param tag RoomTag.ROOM_TAG_XXX values + * @return the rooms list. + */ + public List roomsWithTag(final String tag) { + final List taggedRooms = new ArrayList<>(); + + // sanity check + if (null == mDataHandler.getStore()) { + return taggedRooms; + } + + if (!TextUtils.equals(tag, RoomTag.ROOM_TAG_NO_TAG)) { + final Collection rooms = mDataHandler.getStore().getRooms(); + for (Room room : rooms) { + if (null != room.getAccountData().roomTag(tag)) { + taggedRooms.add(room); + } + } + if (taggedRooms.size() > 0) { + Collections.sort(taggedRooms, new RoomComparatorWithTag(tag)); + } + } else { + final Collection rooms = mDataHandler.getStore().getRooms(); + for (Room room : rooms) { + if (!room.getAccountData().hasTags()) { + taggedRooms.add(room); + } + } + } + + return taggedRooms; + } + + /** + * Get the list of roomIds that are tagged the specified tag. + * The returned array is ordered according to the room tag order. + * + * @param tag RoomTag.ROOM_TAG_XXX values + * @return the room IDs list. + */ + public List roomIdsWithTag(final String tag) { + List roomsWithTag = roomsWithTag(tag); + + List roomIdsList = new ArrayList<>(); + + for (Room room : roomsWithTag) { + roomIdsList.add(room.getRoomId()); + } + + return roomIdsList; + } + + /** + * Compute the tag order to use for a room tag so that the room will appear in the expected position + * in the list of rooms stamped with this tag. + * + * @param index the targeted index of the room in the list of rooms with the tag `tag`. + * @param originIndex the origin index. Integer.MAX_VALUE if there is none. + * @param tag the tag + * @return the tag order to apply to get the expected position. + */ + public Double tagOrderToBeAtIndex(int index, int originIndex, String tag) { + // Algo (and the [0.0, 1.0] assumption) inspired from matrix-react-sdk: + // We sort rooms by the lexicographic ordering of the 'order' metadata on their tags. + // For convenience, we calculate this for now a floating point number between 0.0 and 1.0. + + Double orderA = 0.0; // by default we're next to the beginning of the list + Double orderB = 1.0; // by default we're next to the end of the list too + + List roomsWithTag = roomsWithTag(tag); + + if (roomsWithTag.size() > 0) { + // when an object is moved down, the index must be incremented + // because the object will be removed from the list to be inserted after its destination + if ((originIndex != Integer.MAX_VALUE) && (originIndex < index)) { + index++; + } + + if (index > 0) { + // Bound max index to the array size + int prevIndex = (index < roomsWithTag.size()) ? index : roomsWithTag.size(); + + RoomTag prevTag = roomsWithTag.get(prevIndex - 1).getAccountData().roomTag(tag); + + if (null == prevTag.mOrder) { + Log.e(LOG_TAG, "computeTagOrderForRoom: Previous room in sublist has no ordering metadata. This should never happen."); + } else { + orderA = prevTag.mOrder; + } + } + + if (index <= roomsWithTag.size() - 1) { + RoomTag nextTag = roomsWithTag.get(index).getAccountData().roomTag(tag); + + if (null == nextTag.mOrder) { + Log.e(LOG_TAG, "computeTagOrderForRoom: Next room in sublist has no ordering metadata. This should never happen."); + } else { + orderB = nextTag.mOrder; + } + } + } + + return (orderA + orderB) / 2.0; + } + + /** + * Toggles the direct chat status of a room.
+ * Create a new direct chat room in the account data section if the room does not exist, + * otherwise the room is removed from the account data section. + * Direct chat room user ID choice algorithm:
+ * 1- oldest joined room member + * 2- oldest invited room member + * 3- the user himself + * + * @param roomId the room roomId + * @param aParticipantUserId the participant user id + * @param callback the asynchronous callback + */ + public void toggleDirectChatRoom(final String roomId, + @Nullable final String aParticipantUserId, + final ApiCallback callback) { + IMXStore store = getDataHandler().getStore(); + Room room = store.getRoom(roomId); + + if (null != room) { + if (getDataHandler().getDirectChatRoomIdsList().contains(roomId)) { + // The room is already seen as direct chat + removeDirectChatRoomFromAccountData(roomId, callback); + } else { + // The room was not yet seen as direct chat + if (null == aParticipantUserId) { + searchOtherUserInRoomToCreateDirectChat(room, new SimpleApiCallback(callback) { + @Override + public void onSuccess(String info) { + addDirectChatRoomToAccountData(roomId, info, callback); + } + }); + } else { + addDirectChatRoomToAccountData(roomId, aParticipantUserId, callback); + } + } + } else { + if (callback != null) { + callback.onUnexpectedError(new Exception("Unknown room")); + } + } + } + + /** + * Search another user in the room to create a direct chat + * + * @param room the room to search in + * @param callback the callback to get the selected user id + */ + private void searchOtherUserInRoomToCreateDirectChat(@NonNull final Room room, + @NonNull final ApiCallback callback) { + room.getActiveMembersAsync(new SimpleApiCallback>(callback) { + @Override + public void onSuccess(List members) { + // should never happen but it was reported by a GA issue + if (members.isEmpty()) { + callback.onUnexpectedError(new Exception("Error")); + + return; + } + + RoomMember directChatMember = null; + + if (members.size() > 1) { + // sort algo: oldest join first, then oldest invited + Collections.sort(members, new Comparator() { + @Override + public int compare(RoomMember r1, RoomMember r2) { + int res; + long diff; + + if (RoomMember.MEMBERSHIP_JOIN.equals(r2.membership) && RoomMember.MEMBERSHIP_INVITE.equals(r1.membership)) { + res = 1; + } else if (r2.membership.equals(r1.membership)) { + diff = r1.getOriginServerTs() - r2.getOriginServerTs(); + res = (0 == diff) ? 0 : ((diff > 0) ? 1 : -1); + } else { + res = -1; + } + return res; + } + }); + + int nextIndexSearch = 0; + + // take the oldest join member + if (!TextUtils.equals(members.get(0).getUserId(), getMyUserId())) { + if (RoomMember.MEMBERSHIP_JOIN.equals(members.get(0).membership)) { + directChatMember = members.get(0); + } + } else { + nextIndexSearch = 1; + if (RoomMember.MEMBERSHIP_JOIN.equals(members.get(1).membership)) { + directChatMember = members.get(1); + } + } + + // no join member found, test the oldest join member + if (null == directChatMember) { + if (RoomMember.MEMBERSHIP_INVITE.equals(members.get(nextIndexSearch).membership)) { + directChatMember = members.get(nextIndexSearch); + } + } + } + + // last option: get the logged user + if (null == directChatMember) { + directChatMember = members.get(0); + } + + callback.onSuccess(directChatMember.getUserId()); + } + }); + } + + /** + * Add the room to the direct chat room list in AccountData + * + * @param roomId the room roomId + * @param chosenUserId userId of the direct chat room + * @param callback the asynchronous callback + */ + private void addDirectChatRoomToAccountData(String roomId, + @NonNull String chosenUserId, + ApiCallback callback) { + IMXStore store = getDataHandler().getStore(); + Map> params; + + if (null != store.getDirectChatRoomsDict()) { + params = new HashMap<>(store.getDirectChatRoomsDict()); + } else { + params = new HashMap<>(); + } + + List roomIdsList = new ArrayList<>(); + + // search if there is an entry with the same user + if (params.containsKey(chosenUserId)) { + roomIdsList = new ArrayList<>(params.get(chosenUserId)); + } + + roomIdsList.add(roomId); // update room list with the new room + params.put(chosenUserId, roomIdsList); + + // Store and upload the updated map + getDataHandler().setDirectChatRoomsMap(params, callback); + } + + /** + * Remove the room to the direct chat room list in AccountData + * + * @param roomId the room roomId + * @param callback the asynchronous callback + */ + private void removeDirectChatRoomFromAccountData(String roomId, + ApiCallback callback) { + IMXStore store = getDataHandler().getStore(); + + Map> params; + + if (null != store.getDirectChatRoomsDict()) { + params = new HashMap<>(store.getDirectChatRoomsDict()); + } else { + params = new HashMap<>(); + } + + // remove the current room from the direct chat list rooms + if (null != store.getDirectChatRoomsDict()) { + List keysList = new ArrayList<>(params.keySet()); + + for (String key : keysList) { + List roomIdsList = params.get(key); + if (roomIdsList.contains(roomId)) { + roomIdsList.remove(roomId); + + if (roomIdsList.isEmpty()) { + // Remove this entry + params.remove(key); + } + } + } + } else { + // should not happen: if the room has to be removed, it means the room has been + // previously detected as being part of the listOfList + Log.e(LOG_TAG, "## removeDirectChatRoomFromAccountData(): failed to remove a direct chat room (not seen as direct chat room)"); + if (callback != null) { + callback.onUnexpectedError(new Exception("Error")); + } + return; + } + + // Store and upload the updated map + getDataHandler().setDirectChatRoomsMap(params, callback); + } + + /** + * Update the account password + * + * @param oldPassword the former account password + * @param newPassword the new account password + * @param callback the callback + */ + public void updatePassword(String oldPassword, String newPassword, ApiCallback callback) { + mProfileRestClient.updatePassword(getMyUserId(), oldPassword, newPassword, callback); + } + + /** + * Reset the password to a new one. + * + * @param newPassword the new password + * @param threepid_creds the three pids. + * @param callback the callback + */ + public void resetPassword(final String newPassword, final Map threepid_creds, final ApiCallback callback) { + mProfileRestClient.resetPassword(newPassword, threepid_creds, callback); + } + + /** + * Triggers a request to update the userIds to ignore + * + * @param userIds the userIds to ignore + * @param callback the callback + */ + private void updateUsers(List userIds, ApiCallback callback) { + Map ignoredUsersDict = new HashMap<>(); + + for (String userId : userIds) { + ignoredUsersDict.put(userId, new HashMap<>()); + } + + Map params = new HashMap<>(); + params.put(AccountDataRestClient.ACCOUNT_DATA_KEY_IGNORED_USERS, ignoredUsersDict); + + mAccountDataRestClient.setAccountData(getMyUserId(), AccountDataRestClient.ACCOUNT_DATA_TYPE_IGNORED_USER_LIST, params, callback); + } + + /** + * Tells if an user is in the ignored user ids list + * + * @param userId the user id to test + * @return true if the user is ignored + */ + public boolean isUserIgnored(String userId) { + if (null != userId) { + return getDataHandler().getIgnoredUserIds().indexOf(userId) >= 0; + } + + return false; + } + + /** + * Ignore a list of users. + * + * @param userIds the user ids list to ignore + * @param callback the result callback + */ + public void ignoreUsers(List userIds, ApiCallback callback) { + List curUserIdsToIgnore = getDataHandler().getIgnoredUserIds(); + List userIdsToIgnore = new ArrayList<>(getDataHandler().getIgnoredUserIds()); + + // something to add + if ((null != userIds) && (userIds.size() > 0)) { + // add the new one + for (String userId : userIds) { + if (userIdsToIgnore.indexOf(userId) < 0) { + userIdsToIgnore.add(userId); + } + } + + // some items have been added + if (curUserIdsToIgnore.size() != userIdsToIgnore.size()) { + updateUsers(userIdsToIgnore, callback); + } + } + } + + /** + * Unignore a list of users. + * + * @param userIds the user ids list to unignore + * @param callback the result callback + */ + public void unIgnoreUsers(List userIds, ApiCallback callback) { + List curUserIdsToIgnore = getDataHandler().getIgnoredUserIds(); + List userIdsToIgnore = new ArrayList<>(getDataHandler().getIgnoredUserIds()); + + // something to add + if ((null != userIds) && (userIds.size() > 0)) { + // add the new one + for (String userId : userIds) { + userIdsToIgnore.remove(userId); + } + + // some items have been added + if (curUserIdsToIgnore.size() != userIdsToIgnore.size()) { + updateUsers(userIdsToIgnore, callback); + } + } + } + + /** + * @return the network receiver. + */ + public NetworkConnectivityReceiver getNetworkConnectivityReceiver() { + return mNetworkConnectivityReceiver; + } + + + /** + * Ask the home server if the lazy loading of room members is supported. + * + * @param callback the callback, to be notified if the server actually support the lazy loading. True if supported + */ + public void canEnableLazyLoading(final ApiCallback callback) { + // Check that the server support the lazy loading + mLoginRestClient.getVersions(new SimpleApiCallback(callback) { + @Override + public void onSuccess(Versions info) { + // Check if we can enable lazyLoading + callback.onSuccess(VersionsUtil.supportLazyLoadMembers(info)); + } + }); + } + + /** + * Invalidate the access token, so that it can no longer be used for authorization. + * + * @param context the application context + * @param callback the callback success and failure callback + */ + public void logout(final Context context, final ApiCallback callback) { + synchronized (this) { + if (!mIsAliveSession) { + Log.e(LOG_TAG, "## logout() was already called"); + return; + } + + mIsAliveSession = false; + } + + // Clear crypto data + // For security and because it will be no more useful as we will get a new device id + // on the next log in + enableCrypto(false, null); + + mLoginRestClient.logout(new ApiCallback() { + + private void clearData() { + // required else the clear won't be done + mIsAliveSession = true; + + clear(context, new SimpleApiCallback() { + @Override + public void onSuccess(Void info) { + if (null != callback) { + callback.onSuccess(null); + } + } + }); + } + + @Override + public void onSuccess(JsonObject info) { + Log.d(LOG_TAG, "## logout() : succeed -> clearing the application data "); + clearData(); + } + + private void onError(String errorMessage) { + Log.e(LOG_TAG, "## logout() : failed " + errorMessage); + clearData(); + } + + @Override + public void onNetworkError(Exception e) { + onError(e.getMessage()); + } + + @Override + public void onMatrixError(MatrixError e) { + onError(e.getMessage()); + } + + @Override + public void onUnexpectedError(Exception e) { + onError(e.getMessage()); + } + }); + } + + /** + * Deactivate the account. + * + * @param context the application context + * @param type type of authentication + * @param userPassword current password + * @param eraseUserData true to also erase all the user data + * @param callback the success and failure callback + */ + public void deactivateAccount(final Context context, + final String type, + final String userPassword, + final boolean eraseUserData, + final ApiCallback callback) { + mProfileRestClient.deactivateAccount(type, getMyUserId(), userPassword, eraseUserData, new SimpleApiCallback(callback) { + + @Override + public void onSuccess(Void info) { + Log.d(LOG_TAG, "## deactivateAccount() : succeed -> clearing the application data "); + + // Clear crypto data + // For security and because it will be no more useful as we will get a new device id + // on the next log in + enableCrypto(false, null); + + clear(context, new SimpleApiCallback(callback) { + @Override + public void onSuccess(Void info) { + if (null != callback) { + callback.onSuccess(null); + } + } + }); + } + }); + } + + /** + * Update the URL preview status by default + * + * @param status the status + * @param callback + */ + public void setURLPreviewStatus(final boolean status, final ApiCallback callback) { + Map params = new HashMap<>(); + params.put(AccountDataRestClient.ACCOUNT_DATA_KEY_URL_PREVIEW_DISABLE, !status); + + Log.d(LOG_TAG, "## setURLPreviewStatus() : status " + status); + mAccountDataRestClient.setAccountData(getMyUserId(), AccountDataRestClient.ACCOUNT_DATA_TYPE_PREVIEW_URLS, params, new ApiCallback() { + @Override + public void onSuccess(Void info) { + Log.d(LOG_TAG, "## setURLPreviewStatus() : succeeds"); + + getDataHandler().getStore().setURLPreviewEnabled(status); + if (null != callback) { + callback.onSuccess(null); + } + } + + @Override + public void onNetworkError(Exception e) { + Log.e(LOG_TAG, "## setURLPreviewStatus() : failed " + e.getMessage(), e); + callback.onNetworkError(e); + } + + @Override + public void onMatrixError(MatrixError e) { + Log.e(LOG_TAG, "## setURLPreviewStatus() : failed " + e.getMessage()); + callback.onMatrixError(e); + } + + @Override + public void onUnexpectedError(Exception e) { + Log.e(LOG_TAG, "## setURLPreviewStatus() : failed " + e.getMessage(), e); + callback.onUnexpectedError(e); + } + }); + } + + /** + * Add user widget to the user Account Data + * + * @param params + * @param callback + */ + public void addUserWidget(final Map params, final ApiCallback callback) { + Log.d(LOG_TAG, "## addUserWidget()"); + + mAccountDataRestClient.setAccountData(getMyUserId(), AccountDataRestClient.ACCOUNT_DATA_TYPE_WIDGETS, params, new ApiCallback() { + @Override + public void onSuccess(Void info) { + Log.d(LOG_TAG, "## addUserWidget() : succeeds"); + + getDataHandler().getStore().setUserWidgets(params); + if (null != callback) { + callback.onSuccess(null); + } + } + + @Override + public void onNetworkError(Exception e) { + Log.e(LOG_TAG, "## addUserWidget() : failed " + e.getMessage(), e); + callback.onNetworkError(e); + } + + @Override + public void onMatrixError(MatrixError e) { + Log.e(LOG_TAG, "## addUserWidget() : failed " + e.getMessage()); + callback.onMatrixError(e); + } + + @Override + public void onUnexpectedError(Exception e) { + Log.e(LOG_TAG, "## addUserWidget() : failed " + e.getMessage(), e); + callback.onUnexpectedError(e); + } + }); + } + + /** + * Tells if the global URL preview settings is enabled + * + * @return true if it is enabled. + */ + public boolean isURLPreviewEnabled() { + return getDataHandler().getStore().isURLPreviewEnabled(); + } + + /** + * Get user widget from user AccountData + * + * @return + */ + public Map getUserWidgets() { + return getDataHandler().getStore().getUserWidgets(); + } + + //============================================================================================================== + // Crypto + //============================================================================================================== + + /** + * The module that manages E2E encryption. + * Null if the feature is not enabled + */ + private MXCrypto mCrypto; + + /** + * @return the crypto instance + */ + public MXCrypto getCrypto() { + return mCrypto; + } + + /** + * @return true if the crypto is enabled + */ + public boolean isCryptoEnabled() { + return null != mCrypto; + } + + /** + * enable encryption by default when launching the session + */ + private boolean mEnableCryptoWhenStartingMXSession = false; + + /** + * Enable the crypto when initializing a new session. + */ + public void enableCryptoWhenStarting() { + mEnableCryptoWhenStartingMXSession = true; + } + + /** + * Optional set of parameters used to configure/customize the e2e encryption + */ + @Nullable + private static MXCryptoConfig sCryptoConfig; + + /** + * Define the set of parameters used to configure/customize the e2e encryption + * This configuration must be set before instantiating the session + */ + public static void setCryptoConfig(@Nullable MXCryptoConfig cryptoConfig) { + sCryptoConfig = cryptoConfig; + } + + /** + * When the encryption is toogled, the room summaries must be updated + * to display the right messages. + */ + private void decryptRoomSummaries() { + if (null != getDataHandler().getStore()) { + Collection summaries = getDataHandler().getStore().getSummaries(); + + for (RoomSummary summary : summaries) { + mDataHandler.decryptEvent(summary.getLatestReceivedEvent(), null); + } + } + } + + /** + * Check if the crypto engine is properly initialized. + * Launch it it is was not yet done. + */ + public void checkCrypto() { + MXFileCryptoStore fileCryptoStore = new MXFileCryptoStore(mEnableFileEncryption); + fileCryptoStore.initWithCredentials(mAppContent, mCredentials); + + if ((fileCryptoStore.hasData() || mEnableCryptoWhenStartingMXSession) && (null == mCrypto)) { + boolean isStoreLoaded = false; + try { + // open the store + fileCryptoStore.open(); + isStoreLoaded = true; + } catch (UnsatisfiedLinkError e) { + Log.e(LOG_TAG, "## checkCrypto() failed " + e.getMessage(), e); + } + + if (!isStoreLoaded) { + // load again the olm manager + // reported by rageshake, it seems that the olm lib is unloaded. + mOlmManager = new OlmManager(); + + try { + // open the store + fileCryptoStore.open(); + isStoreLoaded = true; + } catch (UnsatisfiedLinkError e) { + Log.e(LOG_TAG, "## checkCrypto() failed 2 " + e.getMessage(), e); + } + } + + if (!isStoreLoaded) { + Log.e(LOG_TAG, "## checkCrypto() : cannot enable the crypto because of olm lib"); + return; + } + + mCrypto = new MXCrypto(MXSession.this, fileCryptoStore, sCryptoConfig); + mDataHandler.setCrypto(mCrypto); + // the room summaries are not stored with decrypted content + decryptRoomSummaries(); + + Log.d(LOG_TAG, "## checkCrypto() : the crypto engine is ready"); + } else if (mDataHandler.getCrypto() != mCrypto) { + Log.e(LOG_TAG, "## checkCrypto() : the data handler crypto was not initialized"); + mDataHandler.setCrypto(mCrypto); + } + } + + /** + * Enable / disable the crypto. + * + * @param cryptoEnabled true to enable the crypto + * @param callback the asynchronous callback called when the action has been done + */ + public void enableCrypto(boolean cryptoEnabled, final ApiCallback callback) { + if (cryptoEnabled != isCryptoEnabled()) { + if (cryptoEnabled) { + Log.d(LOG_TAG, "Crypto is enabled"); + MXFileCryptoStore fileCryptoStore = new MXFileCryptoStore(mEnableFileEncryption); + fileCryptoStore.initWithCredentials(mAppContent, mCredentials); + fileCryptoStore.open(); + mCrypto = new MXCrypto(this, fileCryptoStore, sCryptoConfig); + mCrypto.start(true, new SimpleApiCallback(callback) { + @Override + public void onSuccess(Void info) { + decryptRoomSummaries(); + if (null != callback) { + callback.onSuccess(null); + } + } + }); + } else if (null != mCrypto) { + Log.d(LOG_TAG, "Crypto is disabled"); + IMXCryptoStore store = mCrypto.mCryptoStore; + mCrypto.close(); + store.deleteStore(); + mCrypto = null; + mDataHandler.setCrypto(null); + + decryptRoomSummaries(); + + if (null != callback) { + callback.onSuccess(null); + } + } + + mDataHandler.setCrypto(mCrypto); + } else { + if (null != callback) { + callback.onSuccess(null); + } + } + } + + /** + * Retrieves the devices list + * + * @param callback the asynchronous callback + */ + public void getDevicesList(ApiCallback callback) { + mCryptoRestClient.getDevices(callback); + } + + /** + * Set a device name. + * + * @param deviceId the device id + * @param deviceName the device name + * @param callback the asynchronous callback + */ + public void setDeviceName(final String deviceId, final String deviceName, final ApiCallback callback) { + mCryptoRestClient.setDeviceName(deviceId, deviceName, callback); + } + + /** + * Delete a device + * + * @param deviceId the device id + * @param password the passwoerd + * @param callback the asynchronous callback. + */ + public void deleteDevice(final String deviceId, final String password, final ApiCallback callback) { + mCryptoRestClient.deleteDevice(deviceId, new DeleteDeviceParams(), new SimpleApiCallback(callback) { + @Override + public void onSuccess(Void info) { + // should never happen + if (null != callback) { + callback.onSuccess(null); + } + } + + @Override + public void onMatrixError(MatrixError matrixError) { + Log.d(LOG_TAG, "## deleteDevice() : onMatrixError " + matrixError.getMessage()); + RegistrationFlowResponse registrationFlowResponse = null; + + // expected status code is 401 + if ((null != matrixError.mStatus) && (matrixError.mStatus == 401)) { + try { + registrationFlowResponse = JsonUtils.toRegistrationFlowResponse(matrixError.mErrorBodyAsString); + } catch (Exception castExcept) { + Log.e(LOG_TAG, "## deleteDevice(): Received status 401 - Exception - JsonUtils.toRegistrationFlowResponse()", castExcept); + } + } else { + Log.d(LOG_TAG, "## deleteDevice(): Received not expected status 401 =" + matrixError.mStatus); + } + + List stages = new ArrayList<>(); + + // check if the server response can be casted + if ((null != registrationFlowResponse) + && (null != registrationFlowResponse.flows) + && !registrationFlowResponse.flows.isEmpty()) { + for (LoginFlow flow : registrationFlowResponse.flows) { + if (null != flow.stages) { + stages.addAll(flow.stages); + } + } + } + + if (!stages.isEmpty()) { + DeleteDeviceParams params = new DeleteDeviceParams(); + params.auth = new DeleteDeviceAuth(); + params.auth.session = registrationFlowResponse.session; + params.auth.user = mCredentials.userId; + params.auth.password = password; + + Log.d(LOG_TAG, "## deleteDevice() : supported stages " + stages); + + deleteDevice(deviceId, params, stages, callback); + } else { + if (null != callback) { + callback.onMatrixError(matrixError); + } + } + } + }); + } + + /** + * Delete a device. + * + * @param deviceId the device id. + * @param params the delete device params + * @param stages the supported stages + * @param callback the asynchronous callback + */ + private void deleteDevice(final String deviceId, final DeleteDeviceParams params, final List stages, final ApiCallback callback) { + // test the first one + params.auth.type = stages.get(0); + stages.remove(0); + + mCryptoRestClient.deleteDevice(deviceId, params, new SimpleApiCallback(callback) { + @Override + public void onSuccess(Void info) { + if (null != callback) { + callback.onSuccess(null); + } + } + + @Override + public void onMatrixError(MatrixError matrixError) { + boolean has401Error = (null != matrixError.mStatus) && (matrixError.mStatus == 401); + + // failed, try next flow type + if ((has401Error || TextUtils.equals(matrixError.errcode, MatrixError.FORBIDDEN) || TextUtils.equals(matrixError.errcode, MatrixError.UNKNOWN)) + && !stages.isEmpty()) { + deleteDevice(deviceId, params, stages, callback); + } else { + if (null != callback) { + callback.onMatrixError(matrixError); + } + } + } + }); + } + + /** + * Gets a bearer token from the homeserver that the user can + * present to a third party in order to prove their ownership + * of the Matrix account they are logged into. + * + * @param callback the asynchronous callback called when finished + */ + public void openIdToken(final ApiCallback> callback) { + mAccountDataRestClient.openIdToken(getMyUserId(), callback); + } + + /** + * @return the groups manager + */ + public GroupsManager getGroupsManager() { + return mGroupsManager; + } + + /* ========================================================================================== + * Builder + * ========================================================================================== */ + + public static class Builder { + private MXSession mxSession; + + public Builder(HomeServerConnectionConfig hsConfig, MXDataHandler dataHandler, Context context) { + mxSession = new MXSession(hsConfig, dataHandler, context); + } + + public Builder withFileEncryption(boolean enableFileEncryption) { + mxSession.mEnableFileEncryption = enableFileEncryption; + return this; + } + + /** + * Create a pusher rest client, overriding the push server url if necessary + * + * @param pushServerUrl the push server url, or null or empty to use the default PushersRestClient + * @return this builder, to chain calls + */ + public Builder withPushServerUrl(@Nullable String pushServerUrl) { + // If not empty, create a special PushersRestClient + PushersRestClient pushersRestClient = null; + + if (!TextUtils.isEmpty(pushServerUrl)) { + // pusher uses a custom server + try { + HomeServerConnectionConfig alteredHsConfig = new HomeServerConnectionConfig.Builder() + .withHomeServerUri(Uri.parse(pushServerUrl)) + .withCredentials(mxSession.mHsConfig.getCredentials()) + .build(); + pushersRestClient = new PushersRestClient(alteredHsConfig); + } catch (Exception e) { + Log.e(LOG_TAG, "## withPushServerUrl() failed " + e.getMessage(), e); + } + } + + if (null != pushersRestClient) { + // Replace the existing client + mxSession.mPushersRestClient = pushersRestClient; + } + + return this; + } + + /** + * Set the metrics listener of this session + * + * @param metricsListener the metrics listener + * @return this builder, to chain calls + */ + public Builder withMetricsListener(@Nullable MetricsListener metricsListener) { + mxSession.mMetricsListener = metricsListener; + + return this; + } + + /** + * Build the session + * + * @return the build session + */ + public MXSession build() { + return mxSession; + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/MxEventDispatcher.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/MxEventDispatcher.java new file mode 100644 index 0000000000..43a46f493b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/MxEventDispatcher.java @@ -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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 senderIds, boolean ignoreEvent) { + if (ignoreEvent) { + return; + } + + final List 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 getListenersSnapshot() { + List eventListeners; + + synchronized (this) { + eventListeners = new ArrayList<>(mEventListeners); + } + + return eventListeners; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/RestClient.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/RestClient.java new file mode 100644 index 0000000000..70b8b3a200 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/RestClient.java @@ -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 { + 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 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 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 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 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; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/CallSoundsManager.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/CallSoundsManager.java new file mode 100644 index 0000000000..9f93f54c7e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/CallSoundsManager.java @@ -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 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 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 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(); + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/HeadsetConnectionReceiver.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/HeadsetConnectionReceiver.java new file mode 100644 index 0000000000..3d33ddd2e9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/HeadsetConnectionReceiver.java @@ -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 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)); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/IMXCall.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/IMXCall.java new file mode 100644 index 0000000000..a0a2c41931 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/IMXCall.java @@ -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). + *

+ *
See {@link #switchRearFrontCamera()}. + * + * @return true if camera was switched, false otherwise + */ + boolean isCameraSwitched(); + + /** + * Indicate if the device supports camera switching. + *

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. + *

+ *
See {@link #muteVideoRecording(boolean)}. + * + * @return true if video recording is muted, false otherwise + */ + boolean isVideoRecordingMuted(); +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/IMXCallListener.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/IMXCallListener.java new file mode 100644 index 0000000000..fd88bad0f1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/IMXCallListener.java @@ -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); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/IMXCallsManagerListener.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/IMXCallsManagerListener.java new file mode 100644 index 0000000000..72298f3ac8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/IMXCallsManagerListener.java @@ -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 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); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/MXCall.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/MXCall.java new file mode 100644 index 0000000000..c1ad66c769 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/MXCall.java @@ -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 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 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 getCallListeners() { + Collection 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 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 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 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 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 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 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() { + @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 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() { + @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; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/MXCallListener.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/MXCallListener.java new file mode 100644 index 0000000000..53dd5f37be --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/MXCallListener.java @@ -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) { + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/MXCallsManager.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/MXCallsManager.java new file mode 100644 index 0000000000..1d99f69344 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/MXCallsManager.java @@ -0,0 +1,1295 @@ +/* + * 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.call; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; +import android.util.Base64; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import im.vector.matrix.android.internal.legacy.MXPatterns; +import im.vector.matrix.android.internal.legacy.MXSession; +import im.vector.matrix.android.internal.legacy.crypto.MXCryptoError; +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.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.listeners.MXEventListener; +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.CallRestClient; +import im.vector.matrix.android.internal.legacy.rest.model.CreateRoomParams; +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.MatrixError; +import im.vector.matrix.android.internal.legacy.rest.model.RoomMember; +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.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; + +public class MXCallsManager { + private static final String LOG_TAG = MXCallsManager.class.getSimpleName(); + + /** + * Defines the call classes. + */ + public enum CallClass { + // disabled because of https://github.com/vector-im/riot-android/issues/1660 + //CHROME_CLASS, + WEBRTC_CLASS, + DEFAULT_CLASS + } + + private MXSession mSession = null; + private Context mContext = null; + + private CallRestClient mCallResClient = null; + private JsonElement mTurnServer = null; + private Timer mTurnServerTimer = null; + private boolean mSuspendTurnServerRefresh = false; + + private CallClass mPreferredCallClass = CallClass.WEBRTC_CLASS; + + // active calls + private final Map mCallsByCallId = new HashMap<>(); + + // listeners + private final Set mListeners = new HashSet<>(); + + // incoming calls + private final Set mxPendingIncomingCallId = new HashSet<>(); + + // UI handler + private final Handler mUIThreadHandler; + + /** + * To create an outgoing call + * 1- CallsManager.createCallInRoom() + * 2- on success, IMXCall.createCallView + * 3- IMXCallListener.onCallViewCreated(callview) -> insert the callview + * 4- IMXCallListener.onCallReady() -> IMXCall.placeCall() + * 5- the call states should follow theses steps + * CALL_STATE_WAIT_LOCAL_MEDIA + * CALL_STATE_WAIT_CREATE_OFFER + * CALL_STATE_INVITE_SENT + * CALL_STATE_RINGING + * 6- the callee accepts the call + * CALL_STATE_CONNECTING + * CALL_STATE_CONNECTED + * + * To manage an incoming call + * 1- IMXCall.createCallView + * 2- IMXCallListener.onCallViewCreated(callview) -> insert the callview + * 3- IMXCallListener.onCallReady(), IMXCall.launchIncomingCall() + * 4- the call states should follow theses steps + * CALL_STATE_WAIT_LOCAL_MEDIA + * CALL_STATE_RINGING + * 5- The user accepts the call, IMXCall.answer() + * 6- the states should be + * CALL_STATE_CREATE_ANSWER + * CALL_STATE_CONNECTING + * CALL_STATE_CONNECTED + */ + + /** + * Constructor + * + * @param session the session + * @param context the context + */ + public MXCallsManager(MXSession session, Context context) { + mSession = session; + mContext = context; + + mUIThreadHandler = new Handler(Looper.getMainLooper()); + + mCallResClient = mSession.getCallRestClient(); + + mSession.getDataHandler().addListener(new MXEventListener() { + @Override + public void onLiveEvent(Event event, RoomState roomState) { + if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_STATE_ROOM_MEMBER)) { + // Listen to the membership join/leave events to detect the conference user activity. + // This mechanism detects the presence of an established conf call + if (TextUtils.equals(event.sender, MXCallsManager.getConferenceUserId(event.roomId))) { + EventContent eventContent = JsonUtils.toEventContent(event.getContentAsJsonObject()); + + if (TextUtils.equals(eventContent.membership, RoomMember.MEMBERSHIP_LEAVE)) { + dispatchOnVoipConferenceFinished(event.roomId); + } + if (TextUtils.equals(eventContent.membership, RoomMember.MEMBERSHIP_JOIN)) { + dispatchOnVoipConferenceStarted(event.roomId); + } + } + } + } + }); + + refreshTurnServer(); + } + + /** + * @return true if the call feature is supported + */ + public boolean isSupported() { + return /*MXChromeCall.isSupported() || */ MXWebRtcCall.isSupported(mContext); + } + + /** + * @return the list of supported classes + */ + public Collection supportedClass() { + List list = new ArrayList<>(); + + /*if (MXChromeCall.isSupported()) { + list.add(CallClass.CHROME_CLASS); + }*/ + + if (MXWebRtcCall.isSupported(mContext)) { + list.add(CallClass.WEBRTC_CLASS); + } + + Log.d(LOG_TAG, "supportedClass " + list); + + return list; + } + + /** + * @param callClass set the default callClass + */ + public void setDefaultCallClass(CallClass callClass) { + Log.d(LOG_TAG, "setDefaultCallClass " + callClass); + + boolean isUpdatable = false; + + /*if (callClass == CallClass.CHROME_CLASS) { + isUpdatable = MXChromeCall.isSupported(); + }*/ + + if (callClass == CallClass.WEBRTC_CLASS) { + isUpdatable = MXWebRtcCall.isSupported(mContext); + } + + if (isUpdatable) { + mPreferredCallClass = callClass; + } + } + + /** + * create a new call + * + * @param callId the call Id (null to use a default value) + * @return the IMXCall + */ + private IMXCall createCall(String callId) { + Log.d(LOG_TAG, "createCall " + callId); + + IMXCall call = null; + + // default + /*if (((CallClass.CHROME_CLASS == mPreferredCallClass) || (CallClass.DEFAULT_CLASS == mPreferredCallClass)) && MXChromeCall.isSupported()) { + call = new MXChromeCall(mSession, mContext, getTurnServer()); + }*/ + + // webrtc + if (null == call) { + try { + call = new MXWebRtcCall(mSession, mContext, getTurnServer()); + } catch (Exception e) { + Log.e(LOG_TAG, "createCall " + e.getMessage(), e); + } + } + + // a valid callid is provided + if (null != callId) { + call.setCallId(callId); + } + + return call; + } + + /** + * Search a call from its dedicated room id. + * + * @param roomId the room id + * @return the IMXCall if it exists + */ + public IMXCall getCallWithRoomId(String roomId) { + List calls; + + synchronized (this) { + calls = new ArrayList<>(mCallsByCallId.values()); + } + + for (IMXCall call : calls) { + if (TextUtils.equals(roomId, call.getRoom().getRoomId())) { + if (TextUtils.equals(call.getCallState(), IMXCall.CALL_STATE_ENDED)) { + Log.d(LOG_TAG, "## getCallWithRoomId() : the call " + call.getCallId() + " has been stopped"); + synchronized (this) { + mCallsByCallId.remove(call.getCallId()); + } + } else { + return call; + } + } + } + + return null; + } + + /** + * Returns the IMXCall from its callId. + * + * @param callId the call Id + * @return the IMXCall if it exists + */ + public IMXCall getCallWithCallId(String callId) { + return getCallWithCallId(callId, false); + } + + /** + * Returns the IMXCall from its callId. + * + * @param callId the call Id + * @param create create the IMXCall if it does not exist + * @return the IMXCall if it exists + */ + private IMXCall getCallWithCallId(String callId, boolean create) { + IMXCall call = null; + + // check if the call exists + if (null != callId) { + synchronized (this) { + call = mCallsByCallId.get(callId); + } + } + + // test if the call has been stopped + if ((null != call) && TextUtils.equals(call.getCallState(), IMXCall.CALL_STATE_ENDED)) { + Log.d(LOG_TAG, "## getCallWithCallId() : the call " + callId + " has been stopped"); + synchronized (this) { + mCallsByCallId.remove(call.getCallId()); + } + + call = null; + } + + // the call does not exist but request to create it + if ((null == call) && create) { + call = createCall(callId); + synchronized (this) { + mCallsByCallId.put(call.getCallId(), call); + } + } + + Log.d(LOG_TAG, "getCallWithCallId " + callId + " " + call); + + return call; + } + + /** + * Tell if a call is in progress. + * + * @param call the call + * @return true if the call is in progress + */ + public static boolean isCallInProgress(IMXCall call) { + boolean res = false; + + if (null != call) { + String callState = call.getCallState(); + res = TextUtils.equals(callState, IMXCall.CALL_STATE_CREATED) + || TextUtils.equals(callState, IMXCall.CALL_STATE_CREATING_CALL_VIEW) + || TextUtils.equals(callState, IMXCall.CALL_STATE_READY) + || TextUtils.equals(callState, IMXCall.CALL_STATE_WAIT_LOCAL_MEDIA) + || TextUtils.equals(callState, IMXCall.CALL_STATE_WAIT_CREATE_OFFER) + || TextUtils.equals(callState, IMXCall.CALL_STATE_INVITE_SENT) + || TextUtils.equals(callState, IMXCall.CALL_STATE_RINGING) + || TextUtils.equals(callState, IMXCall.CALL_STATE_CREATE_ANSWER) + || TextUtils.equals(callState, IMXCall.CALL_STATE_CONNECTING) + || TextUtils.equals(callState, IMXCall.CALL_STATE_CONNECTED); + } + + return res; + } + + /** + * @return true if there are some active calls. + */ + public boolean hasActiveCalls() { + synchronized (this) { + List callIdsToRemove = new ArrayList<>(); + + Set callIds = mCallsByCallId.keySet(); + + for (String callId : callIds) { + IMXCall call = mCallsByCallId.get(callId); + + if (TextUtils.equals(call.getCallState(), IMXCall.CALL_STATE_ENDED)) { + Log.d(LOG_TAG, "# hasActiveCalls() : the call " + callId + " is not anymore valid"); + callIdsToRemove.add(callId); + } else { + Log.d(LOG_TAG, "# hasActiveCalls() : the call " + callId + " is active"); + return true; + } + } + + for (String callIdToRemove : callIdsToRemove) { + mCallsByCallId.remove(callIdToRemove); + } + } + + Log.d(LOG_TAG, "# hasActiveCalls() : no active call"); + return false; + } + + /** + * Manage the call events. + * + * @param store the dedicated store + * @param event the call event. + */ + public void handleCallEvent(final IMXStore store, final Event event) { + if (event.isCallEvent() && isSupported()) { + Log.d(LOG_TAG, "handleCallEvent " + event.getType()); + + // always run the call event in the UI thread + // MXChromeCall does not work properly in other thread (because of the webview) + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + boolean isMyEvent = TextUtils.equals(event.getSender(), mSession.getMyUserId()); + Room room = mSession.getDataHandler().getRoom(store, event.roomId, true); + + String callId = null; + JsonObject eventContent = null; + + try { + eventContent = event.getContentAsJsonObject(); + callId = eventContent.getAsJsonPrimitive("call_id").getAsString(); + } catch (Exception e) { + Log.e(LOG_TAG, "handleCallEvent : fail to retrieve call_id " + e.getMessage(), e); + } + // sanity check + if ((null != callId) && (null != room)) { + // receive an invitation + if (Event.EVENT_TYPE_CALL_INVITE.equals(event.getType())) { + long lifeTime = event.getAge(); + + if (Long.MAX_VALUE == lifeTime) { + lifeTime = System.currentTimeMillis() - event.getOriginServerTs(); + } + + // ignore older call messages + if (lifeTime < MXCall.CALL_TIMEOUT_MS) { + // create the call only it is triggered from someone else + IMXCall call = getCallWithCallId(callId, !isMyEvent); + + // sanity check + if (null != call) { + // init the information + if (null == call.getRoom()) { + call.setRooms(room, room); + } + + if (!isMyEvent) { + call.prepareIncomingCall(eventContent, callId, null); + mxPendingIncomingCallId.add(callId); + } else { + call.handleCallEvent(event); + } + } + } else { + Log.d(LOG_TAG, "## handleCallEvent() : " + Event.EVENT_TYPE_CALL_INVITE + " is ignored because it is too old"); + } + } else if (Event.EVENT_TYPE_CALL_CANDIDATES.equals(event.getType())) { + if (!isMyEvent) { + IMXCall call = getCallWithCallId(callId); + + if (null != call) { + if (null == call.getRoom()) { + call.setRooms(room, room); + } + call.handleCallEvent(event); + } + } + } else if (Event.EVENT_TYPE_CALL_ANSWER.equals(event.getType())) { + IMXCall call = getCallWithCallId(callId); + + if (null != call) { + // assume it is a catch up call. + // the creation / candidates / + // the call has been answered on another device + if (IMXCall.CALL_STATE_CREATED.equals(call.getCallState())) { + call.onAnsweredElsewhere(); + synchronized (this) { + mCallsByCallId.remove(callId); + } + } else { + if (null == call.getRoom()) { + call.setRooms(room, room); + } + call.handleCallEvent(event); + } + } + } else if (Event.EVENT_TYPE_CALL_HANGUP.equals(event.getType())) { + final IMXCall call = getCallWithCallId(callId); + if (null != call) { + // trigger call events only if the call is active + final boolean isActiveCall = !IMXCall.CALL_STATE_CREATED.equals(call.getCallState()); + + if (null == call.getRoom()) { + call.setRooms(room, room); + } + + if (isActiveCall) { + call.handleCallEvent(event); + } + + synchronized (this) { + mCallsByCallId.remove(callId); + } + + // warn that a call has been hung up + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + // must warn anyway any listener that the call has been killed + // for example, when the device is in locked screen + // the callview is not created but the device is ringing + // if the other participant ends the call, the ring should stop + dispatchOnCallHangUp(call); + } + }); + } + } + } + } + }); + } + } + + /** + * check if there is a pending incoming call + */ + public void checkPendingIncomingCalls() { + //Log.d(LOG_TAG, "checkPendingIncomingCalls"); + + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + if (mxPendingIncomingCallId.size() > 0) { + for (String callId : mxPendingIncomingCallId) { + final IMXCall call = getCallWithCallId(callId); + + if (null != call) { + final Room room = call.getRoom(); + + // for encrypted rooms with 2 members + // check if there are some unknown devices before warning + // of the incoming call. + // If there are some unknown devices, the answer event would not be encrypted. + if ((null != room) + && room.isEncrypted() + && mSession.getCrypto().warnOnUnknownDevices() + && room.getNumberOfJoinedMembers() == 2) { + + // test if the encrypted events are sent only to the verified devices (any room) + mSession.getCrypto().getGlobalBlacklistUnverifiedDevices(new SimpleApiCallback() { + @Override + public void onSuccess(Boolean sendToVerifiedDevicesOnly) { + if (sendToVerifiedDevicesOnly) { + dispatchOnIncomingCall(call, null); + } else { + // test if the encrypted events are sent only to the verified devices (only this room) + mSession.getCrypto().isRoomBlacklistUnverifiedDevices(room.getRoomId(), new SimpleApiCallback() { + @Override + public void onSuccess(Boolean sendToVerifiedDevicesOnly) { + if (sendToVerifiedDevicesOnly) { + dispatchOnIncomingCall(call, null); + } else { + room.getJoinedMembersAsync(new ApiCallback>() { + + @Override + public void onNetworkError(Exception e) { + dispatchOnIncomingCall(call, null); + } + + @Override + public void onMatrixError(MatrixError e) { + dispatchOnIncomingCall(call, null); + } + + @Override + public void onUnexpectedError(Exception e) { + dispatchOnIncomingCall(call, null); + } + + @Override + public void onSuccess(List members) { + String userId1 = members.get(0).getUserId(); + String userId2 = members.get(1).getUserId(); + + Log.d(LOG_TAG, "## checkPendingIncomingCalls() : check the unknown devices"); + + // + mSession.getCrypto() + .checkUnknownDevices(Arrays.asList(userId1, userId2), new ApiCallback() { + @Override + public void onSuccess(Void anything) { + Log.d(LOG_TAG, "## checkPendingIncomingCalls() : no unknown device"); + dispatchOnIncomingCall(call, null); + } + + @Override + public void onNetworkError(Exception e) { + Log.e(LOG_TAG, + "## checkPendingIncomingCalls() : checkUnknownDevices failed " + + e.getMessage(), e); + dispatchOnIncomingCall(call, null); + } + + @Override + public void onMatrixError(MatrixError e) { + MXUsersDevicesMap unknownDevices = null; + + if (e instanceof MXCryptoError) { + MXCryptoError cryptoError = (MXCryptoError) e; + + if (MXCryptoError.UNKNOWN_DEVICES_CODE.equals(cryptoError.errcode)) { + unknownDevices = + (MXUsersDevicesMap) cryptoError.mExceptionData; + } + } + + if (null != unknownDevices) { + Log.d(LOG_TAG, "## checkPendingIncomingCalls() :" + + " checkUnknownDevices found some unknown devices"); + } else { + Log.e(LOG_TAG, "## checkPendingIncomingCalls() :" + + " checkUnknownDevices failed " + e.getMessage()); + } + + dispatchOnIncomingCall(call, unknownDevices); + } + + @Override + public void onUnexpectedError(Exception e) { + Log.e(LOG_TAG, "## checkPendingIncomingCalls() :" + + " checkUnknownDevices failed " + e.getMessage(), e); + dispatchOnIncomingCall(call, null); + } + }); + } + }); + } + } + }); + } + } + }); + } else { + dispatchOnIncomingCall(call, null); + } + } + } + } + mxPendingIncomingCallId.clear(); + } + }); + } + + /** + * Create an IMXCall in the room defines by its room Id. + * -> for a 1:1 call, it is a standard call. + * -> for a conference call, + * ----> the conference user is invited to the room (if it was not yet invited) + * ----> the call signaling room is created (or retrieved) with the conference + * ----> and the call is started + * + * @param roomId the room roomId + * @param isVideo true to start a video call + * @param callback the async callback + */ + public void createCallInRoom(final String roomId, final boolean isVideo, final ApiCallback callback) { + Log.d(LOG_TAG, "createCallInRoom in " + roomId); + + final Room room = mSession.getDataHandler().getRoom(roomId); + + // sanity check + if (null != room) { + if (isSupported()) { + int joinedMembers = room.getNumberOfJoinedMembers(); + + Log.d(LOG_TAG, "createCallInRoom : the room has " + joinedMembers + " joined members"); + + if (joinedMembers > 1) { + if (joinedMembers == 2) { + // when a room is encrypted, test first there is no unknown device + // else the call will fail. + // So it seems safer to reject the call creation it it will fail. + if (room.isEncrypted() && mSession.getCrypto().warnOnUnknownDevices()) { + room.getJoinedMembersAsync(new SimpleApiCallback>(callback) { + @Override + public void onSuccess(List members) { + if (members.size() != 2) { + // Safety check + callback.onUnexpectedError(new Exception("Wrong number of members")); + return; + } + + String userId1 = members.get(0).getUserId(); + String userId2 = members.get(1).getUserId(); + + // force the refresh to ensure that the devices list is up-to-date + mSession.getCrypto().checkUnknownDevices(Arrays.asList(userId1, userId2), new SimpleApiCallback(callback) { + @Override + public void onSuccess(Void anything) { + final IMXCall call = getCallWithCallId(null, true); + call.setRooms(room, room); + call.setIsVideo(isVideo); + dispatchOnOutgoingCall(call); + + if (null != callback) { + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + callback.onSuccess(call); + } + }); + } + } + }); + } + }); + } else { + final IMXCall call = getCallWithCallId(null, true); + call.setIsVideo(isVideo); + dispatchOnOutgoingCall(call); + call.setRooms(room, room); + + if (null != callback) { + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + callback.onSuccess(call); + } + }); + } + } + } else { + Log.d(LOG_TAG, "createCallInRoom : inviteConferenceUser"); + + inviteConferenceUser(room, new ApiCallback() { + @Override + public void onSuccess(Void info) { + Log.d(LOG_TAG, "createCallInRoom : inviteConferenceUser succeeds"); + + getConferenceUserRoom(room.getRoomId(), new ApiCallback() { + @Override + public void onSuccess(Room conferenceRoom) { + + Log.d(LOG_TAG, "createCallInRoom : getConferenceUserRoom succeeds"); + + final IMXCall call = getCallWithCallId(null, true); + call.setRooms(room, conferenceRoom); + call.setIsConference(true); + call.setIsVideo(isVideo); + dispatchOnOutgoingCall(call); + + if (null != callback) { + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + callback.onSuccess(call); + } + }); + } + } + + @Override + public void onNetworkError(Exception e) { + Log.e(LOG_TAG, "createCallInRoom : getConferenceUserRoom failed " + e.getMessage(), e); + + if (null != callback) { + callback.onNetworkError(e); + } + } + + @Override + public void onMatrixError(MatrixError e) { + Log.e(LOG_TAG, "createCallInRoom : getConferenceUserRoom failed " + e.getMessage()); + + + if (null != callback) { + callback.onMatrixError(e); + } + } + + @Override + public void onUnexpectedError(Exception e) { + Log.e(LOG_TAG, "createCallInRoom : getConferenceUserRoom failed " + e.getMessage(), e); + + if (null != callback) { + callback.onUnexpectedError(e); + } + } + }); + } + + @Override + public void onNetworkError(Exception e) { + Log.e(LOG_TAG, "createCallInRoom : inviteConferenceUser fails " + e.getMessage(), e); + + if (null != callback) { + callback.onNetworkError(e); + } + } + + @Override + public void onMatrixError(MatrixError e) { + Log.e(LOG_TAG, "createCallInRoom : inviteConferenceUser fails " + e.getMessage()); + + if (null != callback) { + callback.onMatrixError(e); + } + } + + @Override + public void onUnexpectedError(Exception e) { + Log.e(LOG_TAG, "createCallInRoom : inviteConferenceUser fails " + e.getMessage(), e); + + if (null != callback) { + callback.onUnexpectedError(e); + } + } + }); + } + } else { + if (null != callback) { + callback.onMatrixError(new MatrixError(MatrixError.NOT_SUPPORTED, "too few users")); + } + } + } else { + if (null != callback) { + callback.onMatrixError(new MatrixError(MatrixError.NOT_SUPPORTED, "VOIP is not supported")); + } + } + } else { + if (null != callback) { + callback.onMatrixError(new MatrixError(MatrixError.NOT_FOUND, "room not found")); + } + } + } + + //============================================================================================================== + // Turn servers management + //============================================================================================================== + + /** + * Suspend the turn server refresh + */ + public void pauseTurnServerRefresh() { + mSuspendTurnServerRefresh = true; + } + + /** + * Refresh the turn servers until it succeeds. + */ + public void unpauseTurnServerRefresh() { + Log.d(LOG_TAG, "unpauseTurnServerRefresh"); + + mSuspendTurnServerRefresh = false; + if (null != mTurnServerTimer) { + mTurnServerTimer.cancel(); + mTurnServerTimer = null; + } + refreshTurnServer(); + } + + /** + * Stop the turn servers refresh. + */ + public void stopTurnServerRefresh() { + Log.d(LOG_TAG, "stopTurnServerRefresh"); + + mSuspendTurnServerRefresh = true; + if (null != mTurnServerTimer) { + mTurnServerTimer.cancel(); + mTurnServerTimer = null; + } + } + + /** + * @return the turn server + */ + private JsonElement getTurnServer() { + JsonElement res; + + synchronized (LOG_TAG) { + res = mTurnServer; + } + + // privacy logs + //Log.d(LOG_TAG, "getTurnServer " + res); + Log.d(LOG_TAG, "getTurnServer "); + + return res; + } + + /** + * Refresh the turn servers. + */ + private void refreshTurnServer() { + if (mSuspendTurnServerRefresh) { + return; + } + + Log.d(LOG_TAG, "## refreshTurnServer () starts"); + + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + mCallResClient.getTurnServer(new ApiCallback() { + private void restartAfter(int msDelay) { + // reported by GA + // "ttl" seems invalid + if (msDelay <= 0) { + Log.e(LOG_TAG, "## refreshTurnServer() : invalid delay " + msDelay); + } else { + if (null != mTurnServerTimer) { + mTurnServerTimer.cancel(); + } + + try { + mTurnServerTimer = new Timer(); + mTurnServerTimer.schedule(new TimerTask() { + @Override + public void run() { + if (mTurnServerTimer != null) { + mTurnServerTimer.cancel(); + mTurnServerTimer = null; + } + + refreshTurnServer(); + } + }, msDelay); + } catch (Throwable e) { + Log.e(LOG_TAG, "## refreshTurnServer() failed to start the timer", e); + + if (null != mTurnServerTimer) { + mTurnServerTimer.cancel(); + mTurnServerTimer = null; + } + refreshTurnServer(); + } + } + } + + @Override + public void onSuccess(JsonObject info) { + // privacy + Log.d(LOG_TAG, "## refreshTurnServer () : onSuccess"); + //Log.d(LOG_TAG, "onSuccess " + info); + + if (null != info) { + if (info.has("uris")) { + synchronized (LOG_TAG) { + mTurnServer = info; + } + } + + if (info.has("ttl")) { + int ttl = 60000; + + try { + ttl = info.get("ttl").getAsInt(); + // restart a 90 % before ttl expires + ttl = ttl * 9 / 10; + } catch (Exception e) { + Log.e(LOG_TAG, "Fail to retrieve ttl " + e.getMessage(), e); + } + + Log.d(LOG_TAG, "## refreshTurnServer () : onSuccess : retry after " + ttl + " seconds"); + restartAfter(ttl * 1000); + } + } + } + + @Override + public void onNetworkError(Exception e) { + Log.e(LOG_TAG, "## refreshTurnServer () : onNetworkError", e); + restartAfter(60000); + } + + @Override + public void onMatrixError(MatrixError e) { + Log.e(LOG_TAG, "## refreshTurnServer () : onMatrixError() : " + e.errcode); + + if (TextUtils.equals(e.errcode, MatrixError.LIMIT_EXCEEDED) && (null != e.retry_after_ms)) { + Log.e(LOG_TAG, "## refreshTurnServer () : onMatrixError() : retry after " + e.retry_after_ms + " ms"); + restartAfter(e.retry_after_ms); + } + } + + @Override + public void onUnexpectedError(Exception e) { + // should never happen + Log.e(LOG_TAG, "## refreshTurnServer () : onUnexpectedError()", e); + } + }); + } + }); + } + + //============================================================================================================== + // Conference call + //============================================================================================================== + + + // Copied from vector-web: + // FIXME: This currently forces Vector to try to hit the matrix.org AS for conferencing. + // This is bad because it prevents people running their own ASes from being used. + // This isn't permanent and will be customisable in the future: see the proposal + // at docs/conferencing.md for more info. + private static final String USER_PREFIX = "fs_"; + private static final String DOMAIN = "matrix.org"; + private static final Map mConferenceUserIdByRoomId = new HashMap<>(); + + /** + * Return the id of the conference user dedicated for a room Id + * + * @param roomId the room id + * @return the conference user id + */ + public static String getConferenceUserId(String roomId) { + // sanity check + if (null == roomId) { + return null; + } + + String conferenceUserId = mConferenceUserIdByRoomId.get(roomId); + + // it does not exist, compute it. + if (null == conferenceUserId) { + byte[] data = null; + + try { + data = roomId.getBytes("UTF-8"); + } catch (Exception e) { + Log.e(LOG_TAG, "conferenceUserIdForRoom failed " + e.getMessage(), e); + } + + if (null == data) { + return null; + } + + String base64 = Base64.encodeToString(data, Base64.NO_WRAP | Base64.URL_SAFE).replace("=", ""); + conferenceUserId = "@" + USER_PREFIX + base64 + ":" + DOMAIN; + + mConferenceUserIdByRoomId.put(roomId, conferenceUserId); + } + + return conferenceUserId; + } + + /** + * Test if the provided user is a valid conference user Id + * + * @param userId the user id to test + * @return true if it is a valid conference user id + */ + public static boolean isConferenceUserId(String userId) { + // test first if it a known conference user id + if (mConferenceUserIdByRoomId.values().contains(userId)) { + return true; + } + + boolean res = false; + + String prefix = "@" + USER_PREFIX; + String suffix = ":" + DOMAIN; + + if (!TextUtils.isEmpty(userId) && userId.startsWith(prefix) && userId.endsWith(suffix)) { + String roomIdBase64 = userId.substring(prefix.length(), userId.length() - suffix.length()); + try { + res = MXPatterns.isRoomId((new String(Base64.decode(roomIdBase64, Base64.NO_WRAP | Base64.URL_SAFE), "UTF-8"))); + } catch (Exception e) { + Log.e(LOG_TAG, "isConferenceUserId : failed " + e.getMessage(), e); + } + } + + return res; + } + + /** + * Invite the conference user to a room. + * It is mandatory before starting a conference call. + * + * @param room the room + * @param callback the async callback + */ + private void inviteConferenceUser(final Room room, final ApiCallback callback) { + Log.d(LOG_TAG, "inviteConferenceUser " + room.getRoomId()); + + String conferenceUserId = getConferenceUserId(room.getRoomId()); + RoomMember conferenceMember = room.getMember(conferenceUserId); + + if ((null != conferenceMember) && TextUtils.equals(conferenceMember.membership, RoomMember.MEMBERSHIP_JOIN)) { + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + callback.onSuccess(null); + } + }); + } else { + room.invite(conferenceUserId, callback); + } + } + + /** + * Get the room with the conference user dedicated for the passed room. + * + * @param roomId the room id. + * @param callback the async callback. + */ + private void getConferenceUserRoom(final String roomId, final ApiCallback callback) { + Log.d(LOG_TAG, "getConferenceUserRoom with room id " + roomId); + + String conferenceUserId = getConferenceUserId(roomId); + + Room conferenceRoom = null; + Collection rooms = mSession.getDataHandler().getStore().getRooms(); + + // Use an existing 1:1 with the conference user; else make one + for (Room room : rooms) { + if (room.isConferenceUserRoom() && room.getNumberOfMembers() == 2 && null != room.getMember(conferenceUserId)) { + conferenceRoom = room; + break; + } + } + + if (null != conferenceRoom) { + Log.d(LOG_TAG, "getConferenceUserRoom : the room already exists"); + + final Room fConferenceRoom = conferenceRoom; + mSession.getDataHandler().getStore().commit(); + + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + callback.onSuccess(fConferenceRoom); + } + }); + } else { + Log.d(LOG_TAG, "getConferenceUserRoom : create the room"); + + CreateRoomParams params = new CreateRoomParams(); + params.preset = CreateRoomParams.PRESET_PRIVATE_CHAT; + params.invitedUserIds = Arrays.asList(conferenceUserId); + + mSession.createRoom(params, new ApiCallback() { + @Override + public void onSuccess(String roomId) { + Log.d(LOG_TAG, "getConferenceUserRoom : the room creation succeeds"); + + Room room = mSession.getDataHandler().getRoom(roomId); + + if (null != room) { + room.setIsConferenceUserRoom(true); + mSession.getDataHandler().getStore().commit(); + callback.onSuccess(room); + } + } + + @Override + public void onNetworkError(Exception e) { + Log.e(LOG_TAG, "getConferenceUserRoom : failed " + e.getMessage(), e); + callback.onNetworkError(e); + } + + @Override + public void onMatrixError(MatrixError e) { + Log.e(LOG_TAG, "getConferenceUserRoom : failed " + e.getMessage()); + callback.onMatrixError(e); + } + + @Override + public void onUnexpectedError(Exception e) { + Log.e(LOG_TAG, "getConferenceUserRoom : failed " + e.getMessage(), e); + callback.onUnexpectedError(e); + } + }); + } + } + + //============================================================================================================== + // listeners management + //============================================================================================================== + + /** + * Add a listener + * + * @param listener the listener to add + */ + public void addListener(IMXCallsManagerListener listener) { + if (null != listener) { + synchronized (this) { + mListeners.add(listener); + } + } + } + + /** + * Remove a listener + * + * @param listener the listener to remove + */ + public void removeListener(IMXCallsManagerListener listener) { + if (null != listener) { + synchronized (this) { + mListeners.remove(listener); + } + } + } + + /** + * @return a copy of the listeners + */ + private Collection getListeners() { + Collection listeners; + + synchronized (this) { + listeners = new HashSet<>(mListeners); + } + + return listeners; + } + + /** + * dispatch the onIncomingCall event to the listeners + * + * @param call the call + * @param unknownDevices the unknown e2e devices list. + */ + private void dispatchOnIncomingCall(IMXCall call, final MXUsersDevicesMap unknownDevices) { + Log.d(LOG_TAG, "dispatchOnIncomingCall " + call.getCallId()); + + Collection listeners = getListeners(); + + for (IMXCallsManagerListener l : listeners) { + try { + l.onIncomingCall(call, unknownDevices); + } catch (Exception e) { + Log.e(LOG_TAG, "dispatchOnIncomingCall " + e.getMessage(), e); + } + } + } + + /** + * dispatch the call creation to the listeners + * + * @param call the call + */ + private void dispatchOnOutgoingCall(IMXCall call) { + Log.d(LOG_TAG, "dispatchOnOutgoingCall " + call.getCallId()); + + Collection listeners = getListeners(); + + for (IMXCallsManagerListener l : listeners) { + try { + l.onOutgoingCall(call); + } catch (Exception e) { + Log.e(LOG_TAG, "dispatchOnOutgoingCall " + e.getMessage(), e); + } + } + } + + /** + * dispatch the onCallHangUp event to the listeners + * + * @param call the call + */ + private void dispatchOnCallHangUp(IMXCall call) { + Log.d(LOG_TAG, "dispatchOnCallHangUp"); + + Collection listeners = getListeners(); + + for (IMXCallsManagerListener l : listeners) { + try { + l.onCallHangUp(call); + } catch (Exception e) { + Log.e(LOG_TAG, "dispatchOnCallHangUp " + e.getMessage(), e); + } + } + } + + /** + * dispatch the onVoipConferenceStarted event to the listeners + * + * @param roomId the room Id + */ + private void dispatchOnVoipConferenceStarted(String roomId) { + Log.d(LOG_TAG, "dispatchOnVoipConferenceStarted : " + roomId); + + Collection listeners = getListeners(); + + for (IMXCallsManagerListener l : listeners) { + try { + l.onVoipConferenceStarted(roomId); + } catch (Exception e) { + Log.e(LOG_TAG, "dispatchOnVoipConferenceStarted " + e.getMessage(), e); + } + } + } + + /** + * dispatch the onVoipConferenceFinished event to the listeners + * + * @param roomId the room Id + */ + private void dispatchOnVoipConferenceFinished(String roomId) { + Log.d(LOG_TAG, "onVoipConferenceFinished : " + roomId); + + Collection listeners = getListeners(); + + for (IMXCallsManagerListener l : listeners) { + try { + l.onVoipConferenceFinished(roomId); + } catch (Exception e) { + Log.e(LOG_TAG, "dispatchOnVoipConferenceFinished " + e.getMessage(), e); + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/MXCallsManagerListener.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/MXCallsManagerListener.java new file mode 100644 index 0000000000..ba6177f964 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/MXCallsManagerListener.java @@ -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 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) { + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/MXChromeCall.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/MXChromeCall.java new file mode 100644 index 0000000000..7ef50613f9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/MXChromeCall.java @@ -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() { + @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); + } + } + }); + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/MXWebRtcCall.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/MXWebRtcCall.java new file mode 100644 index 0000000000..0a60d0a70a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/MXWebRtcCall.java @@ -0,0 +1,1733 @@ +/* + * 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.call; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.support.v4.content.ContextCompat; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.view.View; +import android.widget.RelativeLayout; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.oney.WebRTCModule.EglUtils; + +import im.vector.matrix.android.internal.legacy.MXSession; +import im.vector.matrix.android.internal.legacy.rest.model.Event; +import im.vector.matrix.android.internal.legacy.util.Log; +import org.webrtc.AudioSource; +import org.webrtc.AudioTrack; +import org.webrtc.Camera1Enumerator; +import org.webrtc.Camera2Enumerator; +import org.webrtc.CameraEnumerator; +import org.webrtc.CameraVideoCapturer; +import org.webrtc.DataChannel; +import org.webrtc.EglBase; +import org.webrtc.IceCandidate; +import org.webrtc.MediaConstraints; +import org.webrtc.MediaStream; +import org.webrtc.PeerConnection; +import org.webrtc.PeerConnectionFactory; +import org.webrtc.RtpReceiver; +import org.webrtc.SdpObserver; +import org.webrtc.SessionDescription; +import org.webrtc.VideoSource; +import org.webrtc.VideoTrack; + +import java.util.ArrayList; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; + +public class MXWebRtcCall extends MXCall { + private static final String LOG_TAG = MXWebRtcCall.class.getSimpleName(); + + private static final String VIDEO_TRACK_ID = "ARDAMSv0"; + private static final String AUDIO_TRACK_ID = "ARDAMSa0"; + + private static final int DEFAULT_WIDTH = 640; + private static final int DEFAULT_HEIGHT = 360; + private static final int DEFAULT_FPS = 30; + + private static final int CAMERA_TYPE_FRONT = 1; + private static final int CAMERA_TYPE_REAR = 2; + private static final int CAMERA_TYPE_UNDEFINED = -1; + + static private PeerConnectionFactory mPeerConnectionFactory = null; + static private String mFrontCameraName = null; + static private String mBackCameraName = null; + static private CameraVideoCapturer mCameraVideoCapturer = null; + + private RelativeLayout mCallView = null; + + private boolean mIsCameraSwitched; + private boolean mIsCameraUnplugged = false; + private VideoSource mVideoSource = null; + private VideoTrack mLocalVideoTrack = null; + private AudioSource mAudioSource = null; + private AudioTrack mLocalAudioTrack = null; + private MediaStream mLocalMediaStream = null; + + private VideoTrack mRemoteVideoTrack = null; + private PeerConnection mPeerConnection = null; + + // default value + private String mCallState = CALL_STATE_CREATED; + + private boolean mUsingLargeLocalRenderer = true; + private MXWebRtcView mFullScreenRTCView = null; + private MXWebRtcView mPipRTCView = null; + + private static boolean mIsInitialized = false; + // null -> not initialized + // true / false for the supported status + private static Boolean mIsSupported; + + // candidate management + private boolean mIsIncomingPrepared = false; + private JsonArray mPendingCandidates = new JsonArray(); + + private JsonObject mCallInviteParams = null; + private int mCameraInUse = CAMERA_TYPE_UNDEFINED; + + private boolean mIsAnswered = false; + + /** + * @param context the context + * @return true if this stack can perform calls. + */ + public static boolean isSupported(Context context) { + if (null == mIsSupported) { + initializeAndroidGlobals(context.getApplicationContext()); + + Log.d(LOG_TAG, "isSupported " + mIsSupported); + } + + return mIsSupported; + } + + /** + * Tells if the camera2 Api is supported + * + * @param context the context + * @return true if the Camera2 API is supported + */ + private static boolean useCamera2(Context context) { + return Camera2Enumerator.isSupported(context); + } + + /** + * Test if the camera is not used by another app. + * It is used to prevent crashes at org.webrtc.Camera1Session.create(Camera1Session.java:80) + * when the front camera is not available. + * + * @param context the context + * @param isFrontOne true if the camera is the + * @return true if the camera is used. + */ + @SuppressLint("Deprecation") + private static boolean isCameraInUse(Context context, boolean isFrontOne) { + boolean isUsed = false; + + if (!useCamera2(context)) { + int cameraId = -1; + int numberOfCameras = android.hardware.Camera.getNumberOfCameras(); + for (int i = 0; i < numberOfCameras; i++) { + android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo(); + android.hardware.Camera.getCameraInfo(i, info); + + if ((info.facing == android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT) && isFrontOne) { + cameraId = i; + break; + } else if ((info.facing == android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK) && !isFrontOne) { + cameraId = i; + break; + } + } + + if (cameraId >= 0) { + android.hardware.Camera c = null; + try { + c = android.hardware.Camera.open(cameraId); + } catch (Exception e) { + Log.e(LOG_TAG, "## isCameraInUse() : failed " + e.getMessage(), e); + } finally { + isUsed = (null == c); + if (c != null) { + c.release(); + } + } + } + } + + return isUsed; + } + + /** + * Get a camera enumerator + * + * @param context the context + * @return the camera enumerator + */ + private static CameraEnumerator getCameraEnumerator(Context context) { + if (useCamera2(context)) { + return new Camera2Enumerator(context); + } else { + return new Camera1Enumerator(false); + } + } + + /** + * Constructor + * + * @param session the session + * @param context the context + * @param turnServer the turn server + */ + public MXWebRtcCall(MXSession session, Context context, JsonElement turnServer) { + if (!isSupported(context)) { + throw new AssertionError("MXWebRtcCall : not supported with the current android version"); + } + + if (null == session) { + throw new AssertionError("MXWebRtcCall : session cannot be null"); + } + + if (null == context) { + throw new AssertionError("MXWebRtcCall : context cannot be null"); + } + + Log.d(LOG_TAG, "MXWebRtcCall constructor " + turnServer); + + mCallId = "c" + System.currentTimeMillis(); + mSession = session; + mContext = context; + mTurnServer = turnServer; + } + + /** + * Initialize globals + */ + private static void initializeAndroidGlobals(Context context) { + if (!mIsInitialized) { + try { + mIsInitialized = PeerConnectionFactory.initializeAndroidGlobals( + context, + true, // enable audio initializing + true, // enable video initializing + true // enable hardware acceleration + ); + + PeerConnectionFactory.initializeFieldTrials(null); + + mIsSupported = true; + Log.d(LOG_TAG, "## initializeAndroidGlobals(): mIsInitialized=" + mIsInitialized); + } catch (Throwable e) { + Log.e(LOG_TAG, "## initializeAndroidGlobals(): Exception Msg=" + e.getMessage(), e); + mIsInitialized = true; + mIsSupported = false; + } + } + } + + /** + * Create the callviews + */ + @Override + public void createCallView() { + super.createCallView(); + + if ((null != mIsSupported) && mIsSupported) { + Log.d(LOG_TAG, "++ createCallView()"); + + dispatchOnStateDidChange(CALL_STATE_CREATING_CALL_VIEW); + mUIThreadHandler.postDelayed(new Runnable() { + @Override + public void run() { + mCallView = new RelativeLayout(mContext); + mCallView.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, + RelativeLayout.LayoutParams.MATCH_PARENT)); + mCallView.setBackgroundColor(ContextCompat.getColor(mContext, android.R.color.black)); + mCallView.setVisibility(View.GONE); + + dispatchOnCallViewCreated(mCallView); + + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + dispatchOnStateDidChange(CALL_STATE_READY); + dispatchOnReady(); + } + }); + } + }, 10); + } + } + + /** + * The connection is terminated + * + * @param endCallReasonId the reason of the call ending + */ + private void terminate(final int endCallReasonId) { + Log.d(LOG_TAG, "## terminate(): reason= " + endCallReasonId); + + if (isCallEnded()) { + return; + } + + dispatchOnStateDidChange(CALL_STATE_ENDED); + + boolean isPeerConnectionFactoryAllowed = false; + + if (null != mPeerConnection) { + mPeerConnection.dispose(); + mPeerConnection = null; + // the call has been initialized so mPeerConnectionFactory can be released + isPeerConnectionFactoryAllowed = true; + } + + if (null != mCameraVideoCapturer) { + mCameraVideoCapturer.dispose(); + mCameraVideoCapturer = null; + } + + if (null != mVideoSource) { + mVideoSource.dispose(); + mVideoSource = null; + } + + if (null != mAudioSource) { + mAudioSource.dispose(); + mAudioSource = null; + } + + // mPeerConnectionFactory is static so it might be used by another call + // so we test that the current has been created + if (isPeerConnectionFactoryAllowed && (null != mPeerConnectionFactory)) { + mPeerConnectionFactory.dispose(); + mPeerConnectionFactory = null; + } + + if (null != mCallView) { + final View fCallView = mCallView; + + fCallView.post(new Runnable() { + @Override + public void run() { + fCallView.setVisibility(View.GONE); + } + }); + + mCallView = null; + } + + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + dispatchOnCallEnd(endCallReasonId); + } + }); + } + + /** + * Send the invite event + * + * @param sessionDescription the session description. + */ + private void sendInvite(final SessionDescription sessionDescription) { + // check if the call has not been killed + if (isCallEnded()) { + Log.d(LOG_TAG, "## sendInvite(): isCallEnded"); + return; + } + + Log.d(LOG_TAG, "## sendInvite()"); + + // build the invitation event + JsonObject inviteContent = new JsonObject(); + inviteContent.addProperty("version", 0); + inviteContent.addProperty("call_id", mCallId); + inviteContent.addProperty("lifetime", CALL_TIMEOUT_MS); + + JsonObject offerContent = new JsonObject(); + offerContent.addProperty("sdp", sessionDescription.description); + offerContent.addProperty("type", sessionDescription.type.canonicalForm()); + inviteContent.add("offer", offerContent); + + Event event = new Event(Event.EVENT_TYPE_CALL_INVITE, inviteContent, mSession.getCredentials().userId, mCallSignalingRoom.getRoomId()); + + mPendingEvents.add(event); + + 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)) { + Log.d(LOG_TAG, "sendInvite : CALL_ERROR_USER_NOT_RESPONDING"); + dispatchOnCallError(CALL_ERROR_USER_NOT_RESPONDING); + hangup(null); + } + + // cancel the timer + mCallTimeoutTimer.cancel(); + mCallTimeoutTimer = null; + } catch (Exception e) { + Log.e(LOG_TAG, "## sendInvite(): Exception Msg= " + e.getMessage(), e); + } + } + }, CALL_TIMEOUT_MS); + } catch (Throwable throwable) { + Log.e(LOG_TAG, "## sendInvite(): failed " + throwable.getMessage(), throwable); + if (null != mCallTimeoutTimer) { + mCallTimeoutTimer.cancel(); + mCallTimeoutTimer = null; + } + } + + sendNextEvent(); + } + + /** + * Send the answer event + * + * @param sessionDescription the session description + */ + private void sendAnswer(final SessionDescription sessionDescription) { + // check if the call has not been killed + if (isCallEnded()) { + Log.d(LOG_TAG, "sendAnswer isCallEnded"); + return; + } + + Log.d(LOG_TAG, "sendAnswer"); + + // build the invitation event + JsonObject answerContent = new JsonObject(); + answerContent.addProperty("version", 0); + answerContent.addProperty("call_id", mCallId); + answerContent.addProperty("lifetime", CALL_TIMEOUT_MS); + + JsonObject offerContent = new JsonObject(); + offerContent.addProperty("sdp", sessionDescription.description); + offerContent.addProperty("type", sessionDescription.type.canonicalForm()); + answerContent.add("answer", offerContent); + + Event event = new Event(Event.EVENT_TYPE_CALL_ANSWER, answerContent, mSession.getCredentials().userId, mCallSignalingRoom.getRoomId()); + mPendingEvents.add(event); + sendNextEvent(); + + mIsAnswered = true; + } + + @Override + public void updateLocalVideoRendererPosition(VideoLayoutConfiguration aConfigurationToApply) { + super.updateLocalVideoRendererPosition(aConfigurationToApply); + try { + updateWebRtcViewLayout(mPipRTCView, aConfigurationToApply); + } catch (Exception e) { + Log.e(LOG_TAG, "## updateLocalVideoRendererPosition(): Exception Msg=" + e.getMessage(), e); + } + } + + @Override + public boolean isSwitchCameraSupported() { + String[] deviceNames = getCameraEnumerator(mContext).getDeviceNames(); + return (null != deviceNames) && (0 != deviceNames.length); + } + + @Override + public boolean switchRearFrontCamera() { + if ((null != mCameraVideoCapturer) && (isSwitchCameraSupported())) { + try { + mCameraVideoCapturer.switchCamera(null); + + // toggle the video capturer instance + if (CAMERA_TYPE_FRONT == mCameraInUse) { + mCameraInUse = CAMERA_TYPE_REAR; + } else { + mCameraInUse = CAMERA_TYPE_FRONT; + } + + // compute camera switch new status + mIsCameraSwitched = !mIsCameraSwitched; + + return true; + } catch (Exception e) { + Log.e(LOG_TAG, "## switchRearFrontCamera(): failed " + e.getMessage(), e); + } + } else { + Log.w(LOG_TAG, "## switchRearFrontCamera(): failure - invalid values"); + } + return false; + } + + @Override + public void muteVideoRecording(boolean muteValue) { + Log.d(LOG_TAG, "## muteVideoRecording(): muteValue=" + muteValue); + + if (!isCallEnded()) { + if (null != mLocalVideoTrack) { + mLocalVideoTrack.setEnabled(!muteValue); + } else { + Log.d(LOG_TAG, "## muteVideoRecording(): failure - invalid value"); + } + } else { + Log.d(LOG_TAG, "## muteVideoRecording(): the call is ended"); + } + } + + @Override + public boolean isVideoRecordingMuted() { + boolean isMuted = false; + + if (!isCallEnded()) { + if (null != mLocalVideoTrack) { + isMuted = !mLocalVideoTrack.enabled(); + } else { + Log.w(LOG_TAG, "## isVideoRecordingMuted(): failure - invalid value"); + } + + Log.d(LOG_TAG, "## isVideoRecordingMuted() = " + isMuted); + } else { + Log.d(LOG_TAG, "## isVideoRecordingMuted() : the call is ended"); + } + + return isMuted; + } + + @Override + public boolean isCameraSwitched() { + return mIsCameraSwitched; + } + + /** + * create the local stream + */ + private void createLocalStream() { + Log.d(LOG_TAG, "## createLocalStream(): IN"); + + // check there is at least one stream to start a call + if ((null == mLocalVideoTrack) && (null == mLocalAudioTrack)) { + Log.d(LOG_TAG, "## createLocalStream(): CALL_ERROR_CALL_INIT_FAILED"); + + dispatchOnCallError(CALL_ERROR_CALL_INIT_FAILED); + hangup("no_stream"); + terminate(IMXCall.END_CALL_REASON_UNDEFINED); + return; + } + + // create our local stream to add our audio and video tracks + mLocalMediaStream = mPeerConnectionFactory.createLocalMediaStream("ARDAMS"); + // add video track to local stream + if (null != mLocalVideoTrack) { + mLocalMediaStream.addTrack(mLocalVideoTrack); + } + // add audio track to local stream + if (null != mLocalAudioTrack) { + mLocalMediaStream.addTrack(mLocalAudioTrack); + } + + if (null != mFullScreenRTCView) { + mFullScreenRTCView.setStream(mLocalMediaStream); + mFullScreenRTCView.setVisibility(View.VISIBLE); + } + + // build ICE servers list + List iceServers = new ArrayList<>(); + + if (null != mTurnServer) { + try { + String username = null; + String password = null; + JsonObject object = mTurnServer.getAsJsonObject(); + + if (object.has("username")) { + username = object.get("username").getAsString(); + } + + if (object.has("password")) { + password = object.get("password").getAsString(); + } + + JsonArray uris = object.get("uris").getAsJsonArray(); + + for (int index = 0; index < uris.size(); index++) { + String url = uris.get(index).getAsString(); + + if ((null != username) && (null != password)) { + iceServers.add(new PeerConnection.IceServer(url, username, password)); + } else { + iceServers.add(new PeerConnection.IceServer(url)); + } + } + } catch (Exception e) { + Log.e(LOG_TAG, "## createLocalStream(): Exception in ICE servers list Msg=" + e.getMessage(), e); + } + } + + Log.d(LOG_TAG, "## createLocalStream(): " + iceServers.size() + " known ice servers"); + + // define at least on server + if (iceServers.isEmpty()) { + Log.d(LOG_TAG, "## createLocalStream(): use the default google server"); + iceServers.add(new PeerConnection.IceServer("stun:stun.l.google.com:19302")); + } + + // define constraints + MediaConstraints pcConstraints = new MediaConstraints(); + pcConstraints.optional.add(new MediaConstraints.KeyValuePair("RtpDataChannels", "true")); + + // start connecting to the other peer by creating the peer connection + mPeerConnection = mPeerConnectionFactory.createPeerConnection( + iceServers, + pcConstraints, + new PeerConnection.Observer() { + @Override + public void onSignalingChange(PeerConnection.SignalingState signalingState) { + Log.d(LOG_TAG, "## mPeerConnection creation: onSignalingChange state=" + signalingState); + } + + @Override + public void onIceConnectionChange(final PeerConnection.IceConnectionState iceConnectionState) { + Log.d(LOG_TAG, "## mPeerConnection creation: onIceConnectionChange " + iceConnectionState); + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + if (iceConnectionState == PeerConnection.IceConnectionState.CONNECTED) { + if ((null != mLocalVideoTrack) && mUsingLargeLocalRenderer && isVideo()) { + mLocalVideoTrack.setEnabled(false); + + // in conference call, there is no local preview, + // the local attendee video is sent by the server among the others conference attendees. + if (!isConference()) { + // add local preview, only for 1:1 call + //mLocalVideoTrack.addRenderer(mSmallLocalRenderer); + mPipRTCView.setStream(mLocalMediaStream); + mPipRTCView.setVisibility(View.VISIBLE); + + // to be able to display the avatar video above the large one + mPipRTCView.setZOrder(1); + } + + mLocalVideoTrack.setEnabled(true); + mUsingLargeLocalRenderer = false; + + mCallView.post(new Runnable() { + @Override + public void run() { + if (null != mCallView) { + mCallView.invalidate(); + } + } + }); + } + + dispatchOnStateDidChange(IMXCall.CALL_STATE_CONNECTED); + } + // theses states are ignored + // only the matrix hangup event is managed + /*else if (iceConnectionState == PeerConnection.IceConnectionState.DISCONNECTED) { + // TODO warn the user ? + hangup(null); + } else if (iceConnectionState == PeerConnection.IceConnectionState.CLOSED) { + // TODO warn the user ? + terminate(); + }*/ + else if (iceConnectionState == PeerConnection.IceConnectionState.FAILED) { + dispatchOnCallError(CALL_ERROR_ICE_FAILED); + hangup("ice_failed"); + } + } + }); + } + + @Override + public void onIceConnectionReceivingChange(boolean var1) { + Log.d(LOG_TAG, "## mPeerConnection creation: onIceConnectionReceivingChange " + var1); + } + + @Override + public void onIceCandidatesRemoved(IceCandidate[] var1) { + Log.d(LOG_TAG, "## mPeerConnection creation: onIceCandidatesRemoved " + var1); + } + + @Override + public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { + Log.d(LOG_TAG, "## mPeerConnection creation: onIceGatheringChange " + iceGatheringState); + } + + @Override + public void onAddTrack(RtpReceiver var1, MediaStream[] var2) { + Log.d(LOG_TAG, "## mPeerConnection creation: onAddTrack " + var1 + " -- " + var2); + } + + @Override + public void onIceCandidate(final IceCandidate iceCandidate) { + Log.d(LOG_TAG, "## mPeerConnection creation: onIceCandidate " + iceCandidate); + + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + if (!isCallEnded()) { + JsonObject content = new JsonObject(); + content.addProperty("version", 0); + content.addProperty("call_id", mCallId); + + JsonArray candidates = new JsonArray(); + JsonObject cand = new JsonObject(); + cand.addProperty("sdpMLineIndex", iceCandidate.sdpMLineIndex); + cand.addProperty("sdpMid", iceCandidate.sdpMid); + cand.addProperty("candidate", iceCandidate.sdp); + candidates.add(cand); + content.add("candidates", candidates); + + boolean addIt = true; + + // merge candidates + if (mPendingEvents.size() > 0) { + try { + Event lastEvent = mPendingEvents.get(mPendingEvents.size() - 1); + + if (TextUtils.equals(lastEvent.getType(), Event.EVENT_TYPE_CALL_CANDIDATES)) { + // return the content cast as a JsonObject + // it is not a copy + 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); + + // replace the candidates list + lastContent.remove("candidates"); + lastContent.add("candidates", lastContentCandidates); + + // don't need to save anything, lastContent is a reference not a copy + addIt = false; + } + } catch (Exception e) { + Log.e(LOG_TAG, "## createLocalStream(): createPeerConnection - onIceCandidate() Exception Msg=" + + e.getMessage(), e); + } + } + + if (addIt) { + Event event = new Event(Event.EVENT_TYPE_CALL_CANDIDATES, content, mSession.getCredentials().userId, + mCallSignalingRoom.getRoomId()); + + mPendingEvents.add(event); + sendNextEvent(); + } + } + } + }); + } + + @Override + public void onAddStream(final MediaStream mediaStream) { + Log.d(LOG_TAG, "## mPeerConnection creation: onAddStream " + mediaStream); + + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + if ((mediaStream.videoTracks.size() == 1) && !isCallEnded()) { + mRemoteVideoTrack = mediaStream.videoTracks.get(0); + mRemoteVideoTrack.setEnabled(true); + mFullScreenRTCView.setStream(mediaStream); + mFullScreenRTCView.setVisibility(View.VISIBLE); + } + } + }); + } + + @Override + public void onRemoveStream(final MediaStream mediaStream) { + Log.d(LOG_TAG, "## mPeerConnection creation: onRemoveStream " + mediaStream); + + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + if (null != mRemoteVideoTrack) { + mRemoteVideoTrack.dispose(); + mRemoteVideoTrack = null; + mediaStream.videoTracks.get(0).dispose(); + } + } + }); + + } + + @Override + public void onDataChannel(DataChannel dataChannel) { + Log.d(LOG_TAG, "## mPeerConnection creation: onDataChannel " + dataChannel); + } + + @Override + public void onRenegotiationNeeded() { + Log.d(LOG_TAG, "## mPeerConnection creation: onRenegotiationNeeded"); + } + }); + + if (null == mPeerConnection) { + dispatchOnCallError(CALL_ERROR_ICE_FAILED); + hangup("cannot create peer connection"); + return; + } + + // send our local video and audio stream to make it seen by the other part + mPeerConnection.addStream(mLocalMediaStream); + + MediaConstraints constraints = new MediaConstraints(); + constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true")); + constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", isVideo() ? "true" : "false")); + + // call createOffer only for outgoing calls + if (!isIncoming()) { + Log.d(LOG_TAG, "## createLocalStream(): !isIncoming() -> createOffer"); + + mPeerConnection.createOffer(new SdpObserver() { + @Override + public void onCreateSuccess(SessionDescription sessionDescription) { + Log.d(LOG_TAG, "createOffer onCreateSuccess"); + + final SessionDescription sdp = new SessionDescription(sessionDescription.type, sessionDescription.description); + + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + if (mPeerConnection != null) { + // must be done to before sending the invitation message + mPeerConnection.setLocalDescription(new SdpObserver() { + @Override + public void onCreateSuccess(SessionDescription sessionDescription) { + Log.d(LOG_TAG, "setLocalDescription onCreateSuccess"); + } + + @Override + public void onSetSuccess() { + Log.d(LOG_TAG, "setLocalDescription onSetSuccess"); + sendInvite(sdp); + dispatchOnStateDidChange(IMXCall.CALL_STATE_INVITE_SENT); + } + + @Override + public void onCreateFailure(String s) { + Log.e(LOG_TAG, "setLocalDescription onCreateFailure " + s); + dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED); + hangup(null); + } + + @Override + public void onSetFailure(String s) { + Log.e(LOG_TAG, "setLocalDescription onSetFailure " + s); + dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED); + hangup(null); + } + }, sdp); + } + } + }); + } + + @Override + public void onSetSuccess() { + Log.d(LOG_TAG, "createOffer onSetSuccess"); + } + + @Override + public void onCreateFailure(String s) { + Log.d(LOG_TAG, "createOffer onCreateFailure " + s); + dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED); + } + + @Override + public void onSetFailure(String s) { + Log.d(LOG_TAG, "createOffer onSetFailure " + s); + dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED); + } + }, constraints); + + dispatchOnStateDidChange(IMXCall.CALL_STATE_WAIT_CREATE_OFFER); + } + } + + /** + * @return true if the device has a camera device + */ + private boolean hasCameraDevice() { + CameraEnumerator enumerator = getCameraEnumerator(mContext); + String[] deviceNames = enumerator.getDeviceNames(); + int cameraCount = 0; + + mBackCameraName = mFrontCameraName = null; + + if (null != deviceNames) { + for (String deviceName : deviceNames) { + if (enumerator.isFrontFacing(deviceName) && !isCameraInUse(mContext, true)) { + mFrontCameraName = deviceName; + } else if (enumerator.isBackFacing(deviceName) && !isCameraInUse(mContext, false)) { + mBackCameraName = deviceName; + } + } + + cameraCount = deviceNames.length; + } + + Log.d(LOG_TAG, "hasCameraDevice(): camera number= " + cameraCount); + Log.d(LOG_TAG, "hasCameraDevice(): frontCameraName=" + mFrontCameraName + " backCameraName=" + mBackCameraName); + + return (null != mFrontCameraName) || (null != mBackCameraName); + } + + /** + * Create the video capturer + * + * @param cameraName the selected camera name + * @return the video capturer + */ + private CameraVideoCapturer createVideoCapturer(String cameraName) { + CameraVideoCapturer cameraVideoCapturer = null; + + CameraEnumerator camerasEnumerator = getCameraEnumerator(mContext); + final String[] deviceNames = camerasEnumerator.getDeviceNames(); + + + if ((null != deviceNames) && (deviceNames.length > 0)) { + for (String name : deviceNames) { + if (name.equals(cameraName)) { + cameraVideoCapturer = camerasEnumerator.createCapturer(name, null); + if (null != cameraVideoCapturer) { + break; + } + } + } + + if (null == cameraVideoCapturer) { + cameraVideoCapturer = camerasEnumerator.createCapturer(deviceNames[0], null); + } + } + + return cameraVideoCapturer; + } + + /** + * Create the local video stack + */ + private void createVideoTrack() { // permission crash + Log.d(LOG_TAG, "createVideoTrack"); + + // create the local renderer only if there is a camera on the device + if (hasCameraDevice()) { + try { + if (null != mCameraVideoCapturer) { + mCameraVideoCapturer.dispose(); + mCameraVideoCapturer = null; + } + + if (null != mFrontCameraName) { + mCameraVideoCapturer = createVideoCapturer(mFrontCameraName); + + if (null == mCameraVideoCapturer) { + Log.e(LOG_TAG, "Cannot create Video Capturer from front camera"); + } else { + mCameraInUse = CAMERA_TYPE_FRONT; + } + } + + if ((null == mCameraVideoCapturer) && (null != mBackCameraName)) { + mCameraVideoCapturer = createVideoCapturer(mBackCameraName); + + if (null == mCameraVideoCapturer) { + Log.e(LOG_TAG, "Cannot create Video Capturer from back camera"); + } else { + mCameraInUse = CAMERA_TYPE_REAR; + } + } + } catch (Exception ex2) { + // catch exception due to Android M permissions, when + // a call is received and the permissions (camera and audio) were not yet granted + Log.e(LOG_TAG, "createVideoTrack(): Exception Msg=" + ex2.getMessage(), ex2); + } + + if (null != mCameraVideoCapturer) { + Log.d(LOG_TAG, "createVideoTrack find a video capturer"); + + try { + mVideoSource = mPeerConnectionFactory.createVideoSource(mCameraVideoCapturer); + mCameraVideoCapturer.startCapture(DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_FPS); + + mLocalVideoTrack = mPeerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, mVideoSource); + mLocalVideoTrack.setEnabled(true); + } catch (Exception e) { + Log.e(LOG_TAG, "createVideoSource fails with exception " + e.getMessage(), e); + + mLocalVideoTrack = null; + + if (null != mVideoSource) { + mVideoSource.dispose(); + mVideoSource = null; + } + } + } else { + Log.e(LOG_TAG, "## createVideoTrack(): Cannot create Video Capturer - no camera available"); + } + } + } + + /** + * Create the local audio stack + */ + private void createAudioTrack() { + Log.d(LOG_TAG, "createAudioTrack"); + + MediaConstraints audioConstraints = new MediaConstraints(); + + // add all existing audio filters to avoid having echos + audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googEchoCancellation", "true")); + audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googEchoCancellation2", "true")); + audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googDAEchoCancellation", "true")); + + audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googTypingNoiseDetection", "true")); + + audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAutoGainControl", "true")); + audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAutoGainControl2", "true")); + + audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googNoiseSuppression", "true")); + audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googNoiseSuppression2", "true")); + + audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAudioMirroring", "false")); + audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googHighpassFilter", "true")); + + mAudioSource = mPeerConnectionFactory.createAudioSource(audioConstraints); + mLocalAudioTrack = mPeerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, mAudioSource); + } + + /** + * Update the webRtcView layout + * + * @param webRTCView the view + * @param aLocalVideoPosition the video configuration + */ + private void updateWebRtcViewLayout(MXWebRtcView webRTCView, VideoLayoutConfiguration aLocalVideoPosition) { + if (null != webRTCView) { + final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); + + int screenWidth = (aLocalVideoPosition.mDisplayWidth > 0) ? aLocalVideoPosition.mDisplayWidth : displayMetrics.widthPixels; + int screenHeight = (aLocalVideoPosition.mDisplayHeight > 0) ? aLocalVideoPosition.mDisplayHeight : displayMetrics.heightPixels; + + int x = screenWidth * aLocalVideoPosition.mX / 100; + int y = screenHeight * aLocalVideoPosition.mY / 100; + int width = screenWidth * aLocalVideoPosition.mWidth / 100; + int height = screenHeight * aLocalVideoPosition.mHeight / 100; + + RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(width, height); + params.leftMargin = x; + params.topMargin = y; + webRTCView.setLayoutParams(params); + } + } + + /** + * Initialize the call UI + * + * @param callInviteParams the invite params + * @param aLocalVideoPosition position of the local video attendee + */ + @SuppressLint("deprecation") + private void initCallUI(final JsonObject callInviteParams, VideoLayoutConfiguration aLocalVideoPosition) { + Log.d(LOG_TAG, "## initCallUI(): IN"); + + if (isCallEnded()) { + Log.w(LOG_TAG, "## initCallUI(): skipped due to call is ended"); + return; + } + + if (isVideo()) { + Log.d(LOG_TAG, "## initCallUI(): building UI video call"); + try { + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + if (null == mPeerConnectionFactory) { + Log.d(LOG_TAG, "## initCallUI(): video call and no mPeerConnectionFactory"); + + mPeerConnectionFactory = new PeerConnectionFactory(null); + + // Initialize EGL contexts required for HW acceleration. + EglBase.Context eglContext = EglUtils.getRootEglBaseContext(); + if (eglContext != null) { + mPeerConnectionFactory.setVideoHwAccelerationOptions(eglContext, eglContext); + } + + createVideoTrack(); + createAudioTrack(); + createLocalStream(); + + if (null != callInviteParams) { + dispatchOnStateDidChange(CALL_STATE_RINGING); + setRemoteDescription(callInviteParams); + } + } + } + }); + + } catch (Exception e) { + // GA issue + // it seems that setView triggers some exception like "setRenderer has already been called" + Log.e(LOG_TAG, "## initCallUI(): VideoRendererGui.setView : Exception Msg =" + e.getMessage(), e); + } + + try { + Log.d(LOG_TAG, "## initCallUI() building UI"); + + mFullScreenRTCView = new MXWebRtcView(mContext); + mFullScreenRTCView.setBackgroundColor(ContextCompat.getColor(mContext, android.R.color.black)); + mCallView.addView(mFullScreenRTCView, + new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)); + mFullScreenRTCView.setVisibility(View.GONE); + + mPipRTCView = new MXWebRtcView(mContext); + mCallView.addView(mPipRTCView, + new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)); + mPipRTCView.setBackgroundColor(ContextCompat.getColor(mContext, android.R.color.transparent)); + mPipRTCView.setVisibility(View.GONE); + + if (null != aLocalVideoPosition) { + updateWebRtcViewLayout(mPipRTCView, aLocalVideoPosition); + Log.d(LOG_TAG, "## initCallUI(): " + aLocalVideoPosition); + } else { + updateWebRtcViewLayout(mPipRTCView, new VideoLayoutConfiguration(5, 5, 25, 25)); + } + } catch (Exception e) { + Log.e(LOG_TAG, "## initCallUI(): Exception Msg =" + e.getMessage(), e); + } + + // reported gy google analytics + // it should never happens + if (null != mCallView) { + mCallView.setVisibility(View.VISIBLE); + } + + } else { + Log.d(LOG_TAG, "## initCallUI(): build audio call"); + + // audio call + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + if (null == mPeerConnectionFactory) { + mPeerConnectionFactory = new PeerConnectionFactory(); + createAudioTrack(); + createLocalStream(); + + if (null != callInviteParams) { + dispatchOnStateDidChange(CALL_STATE_RINGING); + setRemoteDescription(callInviteParams); + } + } + } + }); + } + } + + /** + * The activity is paused. + */ + @Override + public void onPause() { + super.onPause(); + + Log.d(LOG_TAG, "onPause"); + + try { + if (!isCallEnded()) { + Log.d(LOG_TAG, "onPause with active call"); + + // unplugged the camera to avoid loosing the video when the application is suspended + if (!isVideoRecordingMuted()) { + muteVideoRecording(true); + mIsCameraUnplugged = true; + } + } + } catch (Exception e) { + Log.e(LOG_TAG, "onPause failed " + e.getMessage(), e); + } + } + + /** + * The activity is resumed. + */ + @Override + public void onResume() { + super.onResume(); + + Log.d(LOG_TAG, "onResume"); + + try { + if (!isCallEnded()) { + Log.d(LOG_TAG, "onResume with active call"); + + if (mIsCameraUnplugged) { + muteVideoRecording(false); + mIsCameraUnplugged = false; + } + } + } catch (Exception e) { + Log.e(LOG_TAG, "onResume failed " + e.getMessage(), e); + } + } + + /** + * Start an outgoing call. + */ + @Override + public void placeCall(VideoLayoutConfiguration aLocalVideoPosition) { + Log.d(LOG_TAG, "placeCall"); + super.placeCall(aLocalVideoPosition); + + dispatchOnStateDidChange(IMXCall.CALL_STATE_WAIT_LOCAL_MEDIA); + initCallUI(null, aLocalVideoPosition); + } + + /** + * Set the remote description + * + * @param callInviteParams the invitation params + */ + private void setRemoteDescription(final JsonObject callInviteParams) { + Log.d(LOG_TAG, "setRemoteDescription " + callInviteParams); + + SessionDescription aDescription = null; + // extract the description + try { + if (callInviteParams.has("offer")) { + JsonObject answer = callInviteParams.getAsJsonObject("offer"); + String type = answer.get("type").getAsString(); + String sdp = answer.get("sdp").getAsString(); + + if (!TextUtils.isEmpty(type) && !TextUtils.isEmpty(sdp)) { + aDescription = new SessionDescription(SessionDescription.Type.OFFER, sdp); + } + } + + } catch (Exception e) { + Log.e(LOG_TAG, "## setRemoteDescription(): Exception Msg=" + e.getMessage(), e); + } + + mPeerConnection.setRemoteDescription(new SdpObserver() { + @Override + public void onCreateSuccess(SessionDescription sessionDescription) { + Log.d(LOG_TAG, "setRemoteDescription onCreateSuccess"); + } + + @Override + public void onSetSuccess() { + Log.d(LOG_TAG, "setRemoteDescription onSetSuccess"); + mIsIncomingPrepared = true; + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + checkPendingCandidates(); + } + }); + } + + @Override + public void onCreateFailure(String s) { + Log.e(LOG_TAG, "setRemoteDescription onCreateFailure " + s); + dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED); + } + + @Override + public void onSetFailure(String s) { + Log.e(LOG_TAG, "setRemoteDescription onSetFailure " + s); + dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED); + } + }, aDescription); + } + + /** + * Prepare a call reception. + * + * @param aCallInviteParams the invitation Event content + * @param aCallId the call ID + */ + @Override + public void prepareIncomingCall(final JsonObject aCallInviteParams, final String aCallId, final 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; + + dispatchOnStateDidChange(CALL_STATE_WAIT_LOCAL_MEDIA); + + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + initCallUI(aCallInviteParams, aLocalVideoPosition); + } + }); + } 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(): Exception Msg=" + e.getMessage(), e); + } + } + } + + /** + * The call has been detected as an incoming one. + * The application launches the dedicated activity and expects to launch the incoming call. + * The local video attendee is displayed in the screen according to the values given in aLocalVideoPosition. + * + * @param aLocalVideoPosition local video position + */ + @Override + public void launchIncomingCall(VideoLayoutConfiguration aLocalVideoPosition) { + Log.d(LOG_TAG, "launchIncomingCall : call state " + getCallState()); + + super.launchIncomingCall(aLocalVideoPosition); + if (CALL_STATE_READY.equals(getCallState())) { + prepareIncomingCall(mCallInviteParams, mCallId, aLocalVideoPosition); + } + } + + /** + * The callee accepts the call. + * + * @param event the event + */ + private void onCallAnswer(final Event event) { + Log.d(LOG_TAG, "onCallAnswer : call state " + getCallState()); + + if (!CALL_STATE_CREATED.equals(getCallState()) && (null != mPeerConnection)) { + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + dispatchOnStateDidChange(IMXCall.CALL_STATE_CONNECTING); + SessionDescription aDescription = null; + + // extract the description + try { + JsonObject eventContent = event.getContentAsJsonObject(); + + if (eventContent.has("answer")) { + JsonObject answer = eventContent.getAsJsonObject("answer"); + String type = answer.get("type").getAsString(); + String sdp = answer.get("sdp").getAsString(); + + if (!TextUtils.isEmpty(type) && !TextUtils.isEmpty(sdp) && type.equals("answer")) { + aDescription = new SessionDescription(SessionDescription.Type.ANSWER, sdp); + } + } + + } catch (Exception e) { + Log.e(LOG_TAG, "onCallAnswer : " + e.getMessage(), e); + } + + mPeerConnection.setRemoteDescription(new SdpObserver() { + @Override + public void onCreateSuccess(SessionDescription sessionDescription) { + Log.d(LOG_TAG, "setRemoteDescription onCreateSuccess"); + } + + @Override + public void onSetSuccess() { + Log.d(LOG_TAG, "setRemoteDescription onSetSuccess"); + } + + @Override + public void onCreateFailure(String s) { + Log.e(LOG_TAG, "setRemoteDescription onCreateFailure " + s); + dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED); + } + + @Override + public void onSetFailure(String s) { + Log.e(LOG_TAG, "setRemoteDescription onSetFailure " + s); + dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED); + } + }, aDescription); + } + }); + } + } + + /** + * The other call member hangs up the call. + * + * @param hangUpReasonId hang up reason + */ + private void onCallHangup(final int hangUpReasonId) { + Log.d(LOG_TAG, "## onCallHangup(): call state=" + getCallState()); + String state = getCallState(); + + if (!CALL_STATE_CREATED.equals(state) && (null != mPeerConnection)) { + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + terminate(hangUpReasonId); + } + }); + } else if (CALL_STATE_WAIT_LOCAL_MEDIA.equals(state) && isVideo()) { + // specific case fixing: a video call hung up by the calling side + // when the callee is still displaying the InComingCallActivity dialog. + // If terminate() was not called, the dialog was never dismissed. + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + terminate(hangUpReasonId); + } + }); + } + } + + /** + * A new Ice candidate is received + * + * @param candidates the channel candidates + */ + private void onNewCandidates(final JsonArray candidates) { + Log.d(LOG_TAG, "## onNewCandidates(): call state " + getCallState() + " with candidates " + candidates); + + if (!CALL_STATE_CREATED.equals(getCallState()) && (null != mPeerConnection)) { + List candidatesList = new ArrayList<>(); + + // convert the JSON to IceCandidate + for (int index = 0; index < candidates.size(); index++) { + JsonObject item = candidates.get(index).getAsJsonObject(); + try { + String candidate = item.get("candidate").getAsString(); + String sdpMid = item.get("sdpMid").getAsString(); + int sdpLineIndex = item.get("sdpMLineIndex").getAsInt(); + + candidatesList.add(new IceCandidate(sdpMid, sdpLineIndex, candidate)); + } catch (Exception e) { + Log.e(LOG_TAG, "## onNewCandidates(): Exception Msg=" + e.getMessage(), e); + } + } + + for (IceCandidate cand : candidatesList) { + Log.d(LOG_TAG, "## onNewCandidates(): addIceCandidate " + cand); + mPeerConnection.addIceCandidate(cand); + } + } + } + + /** + * Add ice candidates + * + * @param candidates ic candidates + */ + private void addCandidates(JsonArray candidates) { + if (mIsIncomingPrepared || !isIncoming()) { + Log.d(LOG_TAG, "addCandidates : ready"); + onNewCandidates(candidates); + } else { + synchronized (LOG_TAG) { + Log.d(LOG_TAG, "addCandidates : pending"); + mPendingCandidates.addAll(candidates); + } + } + } + + /** + * Some Ice candidates could have been received while creating the call view. + * Check if some of them have been defined. + */ + private void checkPendingCandidates() { + Log.d(LOG_TAG, "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); + + if (event.isCallEvent()) { + String eventType = event.getType(); + + Log.d(LOG_TAG, "handleCallEvent " + eventType); + + // 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)) { + JsonObject eventContent = event.getContentAsJsonObject(); + + JsonArray candidates = eventContent.getAsJsonArray("candidates"); + addCandidates(candidates); + } else if (Event.EVENT_TYPE_CALL_HANGUP.equals(eventType)) { + onCallHangup(IMXCall.END_CALL_REASON_PEER_HANG_UP); + } + + } else { // event from the current member, but sent from another device + switch (eventType) { + case Event.EVENT_TYPE_CALL_INVITE: + // warn in the UI thread + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + dispatchOnStateDidChange(CALL_STATE_RINGING); + } + }); + break; + + case Event.EVENT_TYPE_CALL_ANSWER: + // call answered from another device + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + onAnsweredElsewhere(); + } + }); + break; + + case Event.EVENT_TYPE_CALL_HANGUP: + // current member answered elsewhere + onCallHangup(IMXCall.END_CALL_REASON_PEER_HANG_UP_ELSEWHERE); + break; + + default: + break; + } // switch end + } + } + } + + // user actions + + /** + * The call is accepted. + */ + @Override + public void answer() { + super.answer(); + Log.d(LOG_TAG, "answer " + getCallState()); + + if (!CALL_STATE_CREATED.equals(getCallState()) && (null != mPeerConnection)) { + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + if (null == mPeerConnection) { + Log.d(LOG_TAG, "answer the connection has been closed"); + return; + } + + dispatchOnStateDidChange(CALL_STATE_CREATE_ANSWER); + + MediaConstraints constraints = new MediaConstraints(); + constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true")); + constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", isVideo() ? "true" : "false")); + + mPeerConnection.createAnswer(new SdpObserver() { + @Override + public void onCreateSuccess(SessionDescription sessionDescription) { + Log.d(LOG_TAG, "createAnswer onCreateSuccess"); + + final SessionDescription sdp = new SessionDescription(sessionDescription.type, sessionDescription.description); + + mUIThreadHandler.post(new Runnable() { + @Override + public void run() { + if (mPeerConnection != null) { + // must be done to before sending the invitation message + mPeerConnection.setLocalDescription(new SdpObserver() { + @Override + public void onCreateSuccess(SessionDescription sessionDescription) { + Log.d(LOG_TAG, "setLocalDescription onCreateSuccess"); + } + + @Override + public void onSetSuccess() { + Log.d(LOG_TAG, "setLocalDescription onSetSuccess"); + sendAnswer(sdp); + dispatchOnStateDidChange(IMXCall.CALL_STATE_CONNECTING); + } + + @Override + public void onCreateFailure(String s) { + Log.e(LOG_TAG, "setLocalDescription onCreateFailure " + s); + dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED); + hangup(null); + } + + @Override + public void onSetFailure(String s) { + Log.e(LOG_TAG, "setLocalDescription onSetFailure " + s); + dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED); + hangup(null); + } + }, sdp); + } + } + }); + } + + @Override + public void onSetSuccess() { + Log.d(LOG_TAG, "createAnswer onSetSuccess"); + } + + @Override + public void onCreateFailure(String s) { + Log.e(LOG_TAG, "createAnswer onCreateFailure " + s); + dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED); + hangup(null); + } + + @Override + public void onSetFailure(String s) { + Log.e(LOG_TAG, "createAnswer onSetFailure " + s); + dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED); + hangup(null); + } + }, constraints); + + } + }); + } + + } + + /** + * The call is hung up. + */ + @Override + public void hangup(String reason) { + super.hangup(reason); + + Log.d(LOG_TAG, "## hangup(): reason=" + reason); + + if (!isCallEnded()) { + sendHangup(reason); + terminate(IMXCall.END_CALL_REASON_UNDEFINED); + } + } + + /** + * @return the callstate (must be a CALL_STATE_XX value) + */ + @Override + public String getCallState() { + return mCallState; + } + + /** + * @return the callView + */ + @Override + public View getCallView() { + return mCallView; + } + + /** + * @return the callView visibility + */ + @Override + public int getVisibility() { + if (null != mCallView) { + return mCallView.getVisibility(); + } else { + return View.GONE; + } + } + + /** + * Set the callview visibility + * + * @return true if the operation succeeds + */ + @Override + public boolean setVisibility(int visibility) { + if (null != mCallView) { + mCallView.setVisibility(visibility); + return true; + } + + return false; + } + + /** + * The call has been answered on another device. + * We distinguish the case where an account is active on + * multiple devices and a video call is launched on the account. In this case + * the callee who did not answer must display a "answered elsewhere" message. + */ + @Override + public void onAnsweredElsewhere() { + super.onAnsweredElsewhere(); + + String state = getCallState(); + + Log.d(LOG_TAG, "onAnsweredElsewhere in state " + state); + + if (!isCallEnded() && !mIsAnswered) { + dispatchAnsweredElsewhere(); + terminate(IMXCall.END_CALL_REASON_UNDEFINED); + } + } + + @Override + protected void dispatchOnStateDidChange(String newState) { + Log.d(LOG_TAG, "dispatchOnStateDidChange " + newState); + + mCallState = newState; + + // call timeout management + if (CALL_STATE_CONNECTING.equals(mCallState) || CALL_STATE_CONNECTED.equals(mCallState)) { + if (null != mCallTimeoutTimer) { + mCallTimeoutTimer.cancel(); + mCallTimeoutTimer = null; + } + } + + super.dispatchOnStateDidChange(newState); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/MXWebRtcView.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/MXWebRtcView.java new file mode 100644 index 0000000000..79925e0383 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/MXWebRtcView.java @@ -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. + *

+ * The default value is in accord with + * https://www.w3.org/TR/html5/embedded-content-0.html#the-video-element: + *

+ * 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 Method 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 View has {@link View#isInLayout()}, invokes it and + * returns its return value; otherwise, returns false like + * {@link ViewCompat#isInLayout(View)}. + * + * @return If this View has View#isInLayout(), invokes it + * and returns its return value; otherwise, returns false. + */ + 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 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); + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/VideoLayoutConfiguration.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/VideoLayoutConfiguration.java new file mode 100644 index 0000000000..903686dabb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/call/VideoLayoutConfiguration.java @@ -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; + } +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/IncomingRoomKeyRequest.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/IncomingRoomKeyRequest.java new file mode 100755 index 0000000000..31791473e8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/IncomingRoomKeyRequest.java @@ -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(); + } +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/IncomingRoomKeyRequestCancellation.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/IncomingRoomKeyRequestCancellation.java new file mode 100755 index 0000000000..45be295f1f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/IncomingRoomKeyRequestCancellation.java @@ -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; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXCrypto.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXCrypto.java new file mode 100755 index 0000000000..fb67654dd4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXCrypto.java @@ -0,0 +1,2776 @@ +/* + * 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.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +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 com.google.gson.reflect.TypeToken; + +import im.vector.matrix.android.internal.legacy.MXSession; +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.crypto.data.MXDeviceInfo; +import im.vector.matrix.android.internal.legacy.crypto.data.MXEncryptEventContentResult; +import im.vector.matrix.android.internal.legacy.crypto.data.MXKey; +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.data.Room; +import im.vector.matrix.android.internal.legacy.data.RoomState; +import im.vector.matrix.android.internal.legacy.data.cryptostore.IMXCryptoStore; +import im.vector.matrix.android.internal.legacy.listeners.IMXNetworkEventListener; +import im.vector.matrix.android.internal.legacy.listeners.MXEventListener; +import im.vector.matrix.android.internal.legacy.network.NetworkConnectivityReceiver; +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.EventContent; +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.rest.model.crypto.KeysUploadResponse; +import im.vector.matrix.android.internal.legacy.rest.model.crypto.RoomKeyContent; +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.rest.model.sync.SyncResponse; +import im.vector.matrix.android.internal.legacy.util.JsonUtils; +import im.vector.matrix.android.internal.legacy.util.Log; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; + +/** + * A `MXCrypto` class instance manages the end-to-end crypto for a MXSession instance. + *

+ * Messages posted by the user are automatically redirected to MXCrypto in order to be encrypted + * before sending. + * In the other hand, received events goes through MXCrypto for decrypting. + * MXCrypto maintains all necessary keys and their sharing with other devices required for the crypto. + * Specially, it tracks all room membership changes events in order to do keys updates. + */ +public class MXCrypto { + private static final String LOG_TAG = MXCrypto.class.getSimpleName(); + + // max number of keys to upload at once + // Creating keys can be an expensive operation so we limit the + // number we generate in one go to avoid blocking the application + // for too long. + private static final int ONE_TIME_KEY_GENERATION_MAX_NUMBER = 5; + + // frequency with which to check & upload one-time keys + private static final long ONE_TIME_KEY_UPLOAD_PERIOD = 60 * 1000; // one minute + + // The Matrix session. + private final MXSession mSession; + + // the crypto store + public IMXCryptoStore mCryptoStore; + + // MXEncrypting instance for each room. + private final Map mRoomEncryptors; + + // A map from algorithm to MXDecrypting instance, for each room + private final Map> mRoomDecryptors; + + // Our device keys + private MXDeviceInfo mMyDevice; + + // The libolm wrapper. + private MXOlmDevice mOlmDevice; + + private Map> mLastPublishedOneTimeKeys; + + // the encryption is starting + private boolean mIsStarting; + + // tell if the crypto is started + private boolean mIsStarted; + + // the crypto background threads + private HandlerThread mEncryptingHandlerThread = null; + private Handler mEncryptingHandler = null; + + private HandlerThread mDecryptingHandlerThread = null; + private Handler mDecryptingHandler = null; + + // the UI thread + private Handler mUIHandler = null; + + private NetworkConnectivityReceiver mNetworkConnectivityReceiver; + + private Integer mOneTimeKeyCount; + + private final MXDeviceList mDevicesList; + + private final MXOutgoingRoomKeyRequestManager mOutgoingRoomKeyRequestManager; + + private final IMXNetworkEventListener mNetworkListener = new IMXNetworkEventListener() { + @Override + public void onNetworkConnectionUpdate(boolean isConnected) { + if (isConnected && !isStarted()) { + Log.d(LOG_TAG, "Start MXCrypto because a network connection has been retrieved "); + start(false, null); + } + } + }; + + private final MXEventListener mEventListener = new MXEventListener() { + /* + * Warning, if a method is added here, the corresponding call has to be also added in MxEventDispatcher + */ + + @Override + public void onToDeviceEvent(Event event) { + MXCrypto.this.onToDeviceEvent(event); + } + + @Override + public void onLiveEvent(Event event, RoomState roomState) { + if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_MESSAGE_ENCRYPTION)) { + onCryptoEvent(event); + } else if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_STATE_ROOM_MEMBER)) { + onRoomMembership(event); + } + } + }; + + // initialization callbacks + private final List> mInitializationCallbacks = new ArrayList(); + + // Warn the user if some new devices are detected while encrypting a message. + private boolean mWarnOnUnknownDevices = true; + + // tell if there is a OTK check in progress + private boolean mOneTimeKeyCheckInProgress = false; + + // last OTK check timestamp + private long mLastOneTimeKeyCheck = 0; + + // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations + // we received in the current sync. + private final List mReceivedRoomKeyRequests = new ArrayList<>(); + private final List mReceivedRoomKeyRequestCancellations = new ArrayList<>(); + + // Set of parameters used to configure/customize the end-to-end crypto. + private MXCryptoConfig mCryptoConfig; + + /** + * Constructor + * + * @param matrixSession the session + * @param cryptoStore the crypto store + * @param cryptoConfig the optional set of parameters used to configure the e2e encryption. + */ + public MXCrypto(MXSession matrixSession, IMXCryptoStore cryptoStore, @Nullable MXCryptoConfig cryptoConfig) { + mSession = matrixSession; + mCryptoStore = cryptoStore; + + if (null != cryptoConfig) { + mCryptoConfig = cryptoConfig; + } else { + // Consider the default configuration value + mCryptoConfig = new MXCryptoConfig(); + } + + mOlmDevice = new MXOlmDevice(mCryptoStore); + mRoomEncryptors = new HashMap<>(); + mRoomDecryptors = new HashMap<>(); + + String deviceId = mSession.getCredentials().deviceId; + // deviceId should always be defined + boolean refreshDevicesList = !TextUtils.isEmpty(deviceId); + + if (TextUtils.isEmpty(deviceId)) { + // use the stored one + mSession.getCredentials().deviceId = deviceId = mCryptoStore.getDeviceId(); + } + + if (TextUtils.isEmpty(deviceId)) { + mSession.getCredentials().deviceId = deviceId = UUID.randomUUID().toString(); + Log.d(LOG_TAG, "Warning: No device id in MXCredentials. An id was created. Think of storing it"); + mCryptoStore.storeDeviceId(deviceId); + } + + mMyDevice = new MXDeviceInfo(deviceId); + mMyDevice.userId = mSession.getMyUserId(); + + mDevicesList = new MXDeviceList(matrixSession, this); + + Map keys = new HashMap<>(); + + if (!TextUtils.isEmpty(mOlmDevice.getDeviceEd25519Key())) { + keys.put("ed25519:" + mSession.getCredentials().deviceId, mOlmDevice.getDeviceEd25519Key()); + } + + if (!TextUtils.isEmpty(mOlmDevice.getDeviceCurve25519Key())) { + keys.put("curve25519:" + mSession.getCredentials().deviceId, mOlmDevice.getDeviceCurve25519Key()); + } + + mMyDevice.keys = keys; + + mMyDevice.algorithms = MXCryptoAlgorithms.sharedAlgorithms().supportedAlgorithms(); + mMyDevice.mVerified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED; + + // Add our own deviceinfo to the store + Map endToEndDevicesForUser = mCryptoStore.getUserDevices(mSession.getMyUserId()); + + Map myDevices; + + if (null != endToEndDevicesForUser) { + myDevices = new HashMap<>(endToEndDevicesForUser); + } else { + myDevices = new HashMap<>(); + } + + myDevices.put(mMyDevice.deviceId, mMyDevice); + + mCryptoStore.storeUserDevices(mSession.getMyUserId(), myDevices); + mSession.getDataHandler().setCryptoEventsListener(mEventListener); + + mEncryptingHandlerThread = new HandlerThread("MXCrypto_encrypting_" + mSession.getMyUserId(), Thread.MIN_PRIORITY); + mEncryptingHandlerThread.start(); + + mDecryptingHandlerThread = new HandlerThread("MXCrypto_decrypting_" + mSession.getMyUserId(), Thread.MIN_PRIORITY); + mDecryptingHandlerThread.start(); + + mUIHandler = new Handler(Looper.getMainLooper()); + + if (refreshDevicesList) { + // ensure to have the up-to-date devices list + // got some issues when upgrading from Riot < 0.6.4 + mDevicesList.handleDeviceListsChanges(Arrays.asList(mSession.getMyUserId()), null); + } + + mOutgoingRoomKeyRequestManager = new MXOutgoingRoomKeyRequestManager(mSession, this); + + mReceivedRoomKeyRequests.addAll(mCryptoStore.getPendingIncomingRoomKeyRequests()); + } + + /** + * @return the encrypting thread handler + */ + public Handler getEncryptingThreadHandler() { + // mEncryptingHandlerThread was not yet ready + if (null == mEncryptingHandler) { + mEncryptingHandler = new Handler(mEncryptingHandlerThread.getLooper()); + } + + // fail to get the handler + // might happen if the thread is not yet ready + if (null == mEncryptingHandler) { + return mUIHandler; + } + + return mEncryptingHandler; + } + + /** + * @return the decrypting thread handler + */ + private Handler getDecryptingThreadHandler() { + // mDecryptingHandlerThread was not yet ready + if (null == mDecryptingHandler) { + mDecryptingHandler = new Handler(mDecryptingHandlerThread.getLooper()); + } + + // fail to get the handler + // might happen if the thread is not yet ready + if (null == mDecryptingHandler) { + return mUIHandler; + } + + return mDecryptingHandler; + } + + /** + * @return the UI thread handler + */ + public Handler getUIHandler() { + return mUIHandler; + } + + public void setNetworkConnectivityReceiver(NetworkConnectivityReceiver networkConnectivityReceiver) { + mNetworkConnectivityReceiver = networkConnectivityReceiver; + } + + /** + * @return true if some saved data is corrupted + */ + public boolean isCorrupted() { + return (null != mCryptoStore) && mCryptoStore.isCorrupted(); + } + + /** + * @return true if this instance has been released + */ + public boolean hasBeenReleased() { + return (null == mOlmDevice); + } + + /** + * @return my device info + */ + public MXDeviceInfo getMyDevice() { + return mMyDevice; + } + + /** + * @return the crypto store + */ + public IMXCryptoStore getCryptoStore() { + return mCryptoStore; + } + + /** + * @return the deviceList + */ + public MXDeviceList getDeviceList() { + return mDevicesList; + } + + /** + * Provides the tracking status + * + * @param userId the user id + * @return the tracking status + */ + public int getDeviceTrackingStatus(String userId) { + return mCryptoStore.getDeviceTrackingStatus(userId, MXDeviceList.TRACKING_STATUS_NOT_TRACKED); + } + + /** + * Tell if the MXCrypto is started + * + * @return true if the crypto is started + */ + public boolean isStarted() { + return mIsStarted; + } + + /** + * Tells if the MXCrypto is starting. + * + * @return true if the crypto is starting + */ + public boolean isStarting() { + return mIsStarting; + } + + /** + * Start the crypto module. + * Device keys will be uploaded, then one time keys if there are not enough on the homeserver + * and, then, if this is the first time, this new device will be announced to all other users + * devices. + * + * @param isInitialSync true if it starts from an initial sync + * @param aCallback the asynchronous callback + */ + public void start(final boolean isInitialSync, final ApiCallback aCallback) { + synchronized (mInitializationCallbacks) { + if ((null != aCallback) && (mInitializationCallbacks.indexOf(aCallback) < 0)) { + mInitializationCallbacks.add(aCallback); + } + } + + if (mIsStarting) { + return; + } + + // do not start if there is not network connection + if ((null != mNetworkConnectivityReceiver) && !mNetworkConnectivityReceiver.isConnected()) { + // wait that a valid network connection is retrieved + mNetworkConnectivityReceiver.removeEventListener(mNetworkListener); + mNetworkConnectivityReceiver.addEventListener(mNetworkListener); + return; + } + + mIsStarting = true; + + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + uploadDeviceKeys(new ApiCallback() { + private void onError() { + getUIHandler().postDelayed(new Runnable() { + @Override + public void run() { + if (!isStarted()) { + mIsStarting = false; + start(isInitialSync, null); + } + } + }, 1000); + } + + @Override + public void onSuccess(KeysUploadResponse info) { + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + if (!hasBeenReleased()) { + Log.d(LOG_TAG, "###########################################################"); + Log.d(LOG_TAG, "uploadDeviceKeys done for " + mSession.getMyUserId()); + Log.d(LOG_TAG, " - device id : " + mSession.getCredentials().deviceId); + Log.d(LOG_TAG, " - ed25519 : " + mOlmDevice.getDeviceEd25519Key()); + Log.d(LOG_TAG, " - curve25519 : " + mOlmDevice.getDeviceCurve25519Key()); + Log.d(LOG_TAG, " - oneTimeKeys: " + mLastPublishedOneTimeKeys); // They are + Log.d(LOG_TAG, ""); + + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + maybeUploadOneTimeKeys(new ApiCallback() { + @Override + public void onSuccess(Void info) { + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + if (null != mNetworkConnectivityReceiver) { + mNetworkConnectivityReceiver.removeEventListener(mNetworkListener); + } + + mIsStarting = false; + mIsStarted = true; + + mOutgoingRoomKeyRequestManager.start(); + + synchronized (mInitializationCallbacks) { + for (ApiCallback callback : mInitializationCallbacks) { + final ApiCallback fCallback = callback; + getUIHandler().post(new Runnable() { + @Override + public void run() { + fCallback.onSuccess(null); + } + }); + } + mInitializationCallbacks.clear(); + } + + if (isInitialSync) { + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + // refresh the devices list for each known room members + getDeviceList().invalidateAllDeviceLists(); + mDevicesList.refreshOutdatedDeviceLists(); + } + }); + } else { + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + processReceivedRoomKeyRequests(); + } + }); + } + } + }); + } + + @Override + public void onNetworkError(Exception e) { + Log.e(LOG_TAG, "## start failed : " + e.getMessage(), e); + onError(); + } + + @Override + public void onMatrixError(MatrixError e) { + Log.e(LOG_TAG, "## start failed : " + e.getMessage()); + onError(); + } + + @Override + public void onUnexpectedError(Exception e) { + Log.e(LOG_TAG, "## start failed : " + e.getMessage(), e); + onError(); + } + }); + } + }); + } + } + }); + } + + @Override + public void onNetworkError(Exception e) { + Log.e(LOG_TAG, "## start failed : " + e.getMessage(), e); + onError(); + } + + @Override + public void onMatrixError(MatrixError e) { + Log.e(LOG_TAG, "## start failed : " + e.getMessage()); + onError(); + } + + @Override + public void onUnexpectedError(Exception e) { + Log.e(LOG_TAG, "## start failed : " + e.getMessage(), e); + onError(); + } + }); + } + }); + } + + /** + * Close the crypto + */ + public void close() { + if (null != mEncryptingHandlerThread) { + mSession.getDataHandler().setCryptoEventsListener(null); + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + if (null != mOlmDevice) { + mOlmDevice.release(); + mOlmDevice = null; + } + + mMyDevice = null; + + mCryptoStore.close(); + mCryptoStore = null; + + if (null != mEncryptingHandlerThread) { + mEncryptingHandlerThread.quit(); + mEncryptingHandlerThread = null; + } + + mOutgoingRoomKeyRequestManager.stop(); + } + }); + + getDecryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + if (null != mDecryptingHandlerThread) { + mDecryptingHandlerThread.quit(); + mDecryptingHandlerThread = null; + } + } + }); + } + } + + /** + * @return the olmdevice instance + */ + public MXOlmDevice getOlmDevice() { + return mOlmDevice; + } + + /** + * A sync response has been received + * + * @param syncResponse the syncResponse + * @param fromToken the start sync token + * @param isCatchingUp true if there is a catch-up in progress. + */ + public void onSyncCompleted(final SyncResponse syncResponse, final String fromToken, final boolean isCatchingUp) { + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + if (null != syncResponse.deviceLists) { + getDeviceList().handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left); + } + + if (null != syncResponse.deviceOneTimeKeysCount) { + int currentCount = (null != syncResponse.deviceOneTimeKeysCount.signed_curve25519) ? + syncResponse.deviceOneTimeKeysCount.signed_curve25519 : 0; + updateOneTimeKeyCount(currentCount); + } + + if (isStarted()) { + // Make sure we process to-device messages before generating new one-time-keys #2782 + mDevicesList.refreshOutdatedDeviceLists(); + } + + if (!isCatchingUp && isStarted()) { + maybeUploadOneTimeKeys(); + + processReceivedRoomKeyRequests(); + } + } + }); + } + + /** + * Get the stored device keys for a user. + * + * @param userId the user to list keys for. + * @param callback the asynchronous callback + */ + public void getUserDevices(final String userId, final ApiCallback> callback) { + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + final List list = getUserDevices(userId); + + if (null != callback) { + getUIHandler().post(new Runnable() { + @Override + public void run() { + callback.onSuccess(list); + } + }); + } + } + }); + } + + /** + * Stores the current one_time_key count which will be handled later (in a call of + * _onSyncCompleted). The count is e.g. coming from a /sync response. + * + * @param currentCount the new count + */ + private void updateOneTimeKeyCount(int currentCount) { + mOneTimeKeyCount = currentCount; + } + + /** + * Find a device by curve25519 identity key + * + * @param userId the owner of the device. + * @param algorithm the encryption algorithm. + * @param senderKey the curve25519 key to match. + * @return the device info. + */ + public MXDeviceInfo deviceWithIdentityKey(final String senderKey, final String userId, final String algorithm) { + if (!hasBeenReleased()) { + if (!TextUtils.equals(algorithm, MXCryptoAlgorithms.MXCRYPTO_ALGORITHM_MEGOLM) + && !TextUtils.equals(algorithm, MXCryptoAlgorithms.MXCRYPTO_ALGORITHM_OLM)) { + // We only deal in olm keys + return null; + } + + if (!TextUtils.isEmpty(userId)) { + final List result = new ArrayList<>(); + final CountDownLatch lock = new CountDownLatch(1); + + getDecryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + List devices = getUserDevices(userId); + + if (null != devices) { + for (MXDeviceInfo device : devices) { + Set keys = device.keys.keySet(); + + for (String keyId : keys) { + if (keyId.startsWith("curve25519:")) { + if (TextUtils.equals(senderKey, device.keys.get(keyId))) { + result.add(device); + } + } + } + } + } + + lock.countDown(); + } + }); + + try { + lock.await(); + } catch (Exception e) { + Log.e(LOG_TAG, "## deviceWithIdentityKey() : failed " + e.getMessage(), e); + } + + return (result.size() > 0) ? result.get(0) : null; + } + } + + // Doesn't match a known device + return null; + } + + /** + * Provides the device information for a device id and an user Id + * + * @param userId the user id + * @param deviceId the device id + * @param callback the asynchronous callback + */ + public void getDeviceInfo(final String userId, final String deviceId, final ApiCallback callback) { + getDecryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + final MXDeviceInfo di; + + if (!TextUtils.isEmpty(userId) && !TextUtils.isEmpty(deviceId)) { + di = mCryptoStore.getUserDevice(deviceId, userId); + } else { + di = null; + } + + if (null != callback) { + getUIHandler().post(new Runnable() { + @Override + public void run() { + callback.onSuccess(di); + } + }); + } + } + }); + } + + /** + * Set the devices as known + * + * @param devices the devices + * @param callback the as + */ + public void setDevicesKnown(final List devices, final ApiCallback callback) { + if (hasBeenReleased()) { + return; + } + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + // build a devices map + Map> devicesIdListByUserId = new HashMap<>(); + + for (MXDeviceInfo di : devices) { + List deviceIdsList = devicesIdListByUserId.get(di.userId); + + if (null == deviceIdsList) { + deviceIdsList = new ArrayList<>(); + devicesIdListByUserId.put(di.userId, deviceIdsList); + } + deviceIdsList.add(di.deviceId); + } + + Set userIds = devicesIdListByUserId.keySet(); + + for (String userId : userIds) { + Map storedDeviceIDs = mCryptoStore.getUserDevices(userId); + + // sanity checks + if (null != storedDeviceIDs) { + boolean isUpdated = false; + List deviceIds = devicesIdListByUserId.get(userId); + + for (String deviceId : deviceIds) { + MXDeviceInfo device = storedDeviceIDs.get(deviceId); + + // assume if the device is either verified or blocked + // it means that the device is known + if ((null != device) && device.isUnknown()) { + device.mVerified = MXDeviceInfo.DEVICE_VERIFICATION_UNVERIFIED; + isUpdated = true; + } + } + + if (isUpdated) { + mCryptoStore.storeUserDevices(userId, storedDeviceIDs); + } + } + } + + if (null != callback) { + getUIHandler().post(new Runnable() { + @Override + public void run() { + callback.onSuccess(null); + } + }); + } + } + }); + } + + /** + * Update the blocked/verified state of the given device. + * + * @param verificationStatus the new verification status + * @param deviceId the unique identifier for the device. + * @param userId the owner of the device + * @param callback the asynchronous callback + */ + public void setDeviceVerification(final int verificationStatus, final String deviceId, final String userId, final ApiCallback callback) { + if (hasBeenReleased()) { + return; + } + + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + MXDeviceInfo device = mCryptoStore.getUserDevice(deviceId, userId); + + // Sanity check + if (null == device) { + Log.e(LOG_TAG, "## setDeviceVerification() : Unknown device " + userId + ":" + deviceId); + if (null != callback) { + getUIHandler().post(new Runnable() { + @Override + public void run() { + callback.onSuccess(null); + } + }); + } + return; + } + + if (device.mVerified != verificationStatus) { + device.mVerified = verificationStatus; + mCryptoStore.storeUserDevice(userId, device); + } + + if (null != callback) { + getUIHandler().post(new Runnable() { + @Override + public void run() { + callback.onSuccess(null); + } + }); + } + } + }); + } + + /** + * Configure a room to use encryption. + * This method must be called in getEncryptingThreadHandler + * + * @param roomId the room id to enable encryption in. + * @param algorithm the encryption config for the room. + * @param inhibitDeviceQuery true to suppress device list query for users in the room (for now) + * @param members list of members to start tracking their devices + * @return true if the operation succeeds. + */ + private boolean setEncryptionInRoom(String roomId, String algorithm, boolean inhibitDeviceQuery, List members) { + if (hasBeenReleased()) { + return false; + } + + // If we already have encryption in this room, we should ignore this event + // (for now at least. Maybe we should alert the user somehow?) + String existingAlgorithm = mCryptoStore.getRoomAlgorithm(roomId); + + if (!TextUtils.isEmpty(existingAlgorithm) && !TextUtils.equals(existingAlgorithm, algorithm)) { + Log.e(LOG_TAG, "## setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in " + roomId); + return false; + } + + Class encryptingClass = MXCryptoAlgorithms.sharedAlgorithms().encryptorClassForAlgorithm(algorithm); + + if (null == encryptingClass) { + Log.e(LOG_TAG, "## setEncryptionInRoom() : Unable to encrypt with " + algorithm); + return false; + } + + mCryptoStore.storeRoomAlgorithm(roomId, algorithm); + + IMXEncrypting alg; + + try { + Constructor ctor = encryptingClass.getConstructors()[0]; + alg = (IMXEncrypting) ctor.newInstance(); + } catch (Exception e) { + Log.e(LOG_TAG, "## setEncryptionInRoom() : fail to load the class", e); + return false; + } + + alg.initWithMatrixSession(mSession, roomId); + + synchronized (mRoomEncryptors) { + mRoomEncryptors.put(roomId, alg); + } + + // if encryption was not previously enabled in this room, we will have been + // ignoring new device events for these users so far. We may well have + // up-to-date lists for some users, for instance if we were sharing other + // e2e rooms with them, so there is room for optimisation here, but for now + // we just invalidate everyone in the room. + if (null == existingAlgorithm) { + Log.d(LOG_TAG, "Enabling encryption in " + roomId + " for the first time; invalidating device lists for all users therein"); + + List userIds = new ArrayList<>(); + + for (RoomMember m : members) { + userIds.add(m.getUserId()); + } + + getDeviceList().startTrackingDeviceList(userIds); + + if (!inhibitDeviceQuery) { + getDeviceList().refreshOutdatedDeviceLists(); + } + } + + return true; + } + + /** + * Tells if a room is encrypted + * + * @param roomId the room id + * @return true if the room is encrypted + */ + public boolean isRoomEncrypted(String roomId) { + boolean res = false; + + if (null != roomId) { + synchronized (mRoomEncryptors) { + res = mRoomEncryptors.containsKey(roomId); + + if (!res) { + Room room = mSession.getDataHandler().getRoom(roomId); + + if (null != room) { + res = room.getState().isEncrypted(); + } + } + } + } + + return res; + } + + /** + * @return the stored device keys for a user. + */ + public List getUserDevices(final String userId) { + Map map = getCryptoStore().getUserDevices(userId); + return (null != map) ? new ArrayList<>(map.values()) : new ArrayList(); + } + + /** + * Try to make sure we have established olm sessions for the given users. + * It must be called in getEncryptingThreadHandler() thread. + * The callback is called in the UI thread. + * + * @param users a list of user ids. + * @param callback the asynchronous callback + */ + public void ensureOlmSessionsForUsers(List users, final ApiCallback> callback) { + Log.d(LOG_TAG, "## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers " + users); + + Map> devicesByUser = new HashMap<>(); + + for (String userId : users) { + devicesByUser.put(userId, new ArrayList()); + + List devices = getUserDevices(userId); + + for (MXDeviceInfo device : devices) { + String key = device.identityKey(); + + if (TextUtils.equals(key, mOlmDevice.getDeviceCurve25519Key())) { + // Don't bother setting up session to ourself + continue; + } + + if (device.isVerified()) { + // Don't bother setting up sessions with blocked users + continue; + } + + devicesByUser.get(userId).add(device); + } + } + + ensureOlmSessionsForDevices(devicesByUser, callback); + } + + /** + * Try to make sure we have established olm sessions for the given devices. + * It must be called in getCryptoHandler() thread. + * The callback is called in the UI thread. + * + * @param devicesByUser a map from userid to list of devices. + * @param callback the asynchronous callback + */ + public void ensureOlmSessionsForDevices(final Map> devicesByUser, + final ApiCallback> callback) { + List devicesWithoutSession = new ArrayList<>(); + + final MXUsersDevicesMap results = new MXUsersDevicesMap<>(); + + Set userIds = devicesByUser.keySet(); + + for (String userId : userIds) { + List deviceInfos = devicesByUser.get(userId); + + for (MXDeviceInfo deviceInfo : deviceInfos) { + String deviceId = deviceInfo.deviceId; + String key = deviceInfo.identityKey(); + + String sessionId = mOlmDevice.getSessionId(key); + + if (TextUtils.isEmpty(sessionId)) { + devicesWithoutSession.add(deviceInfo); + } + + MXOlmSessionResult olmSessionResult = new MXOlmSessionResult(deviceInfo, sessionId); + results.setObject(olmSessionResult, userId, deviceId); + } + } + + if (devicesWithoutSession.size() == 0) { + if (null != callback) { + getUIHandler().post(new Runnable() { + @Override + public void run() { + callback.onSuccess(results); + } + }); + } + return; + } + + // Prepare the request for claiming one-time keys + MXUsersDevicesMap usersDevicesToClaim = new MXUsersDevicesMap<>(); + + final String oneTimeKeyAlgorithm = MXKey.KEY_SIGNED_CURVE_25519_TYPE; + + for (MXDeviceInfo device : devicesWithoutSession) { + usersDevicesToClaim.setObject(oneTimeKeyAlgorithm, device.userId, device.deviceId); + } + + // TODO: this has a race condition - if we try to send another message + // while we are claiming a key, we will end up claiming two and setting up + // two sessions. + // + // That should eventually resolve itself, but it's poor form. + + Log.d(LOG_TAG, "## claimOneTimeKeysForUsersDevices() : " + usersDevicesToClaim); + + mSession.getCryptoRestClient().claimOneTimeKeysForUsersDevices(usersDevicesToClaim, new ApiCallback>() { + @Override + public void onSuccess(final MXUsersDevicesMap oneTimeKeys) { + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + try { + Log.d(LOG_TAG, "## claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: " + oneTimeKeys); + + Set userIds = devicesByUser.keySet(); + + for (String userId : userIds) { + List deviceInfos = devicesByUser.get(userId); + + for (MXDeviceInfo deviceInfo : deviceInfos) { + + MXKey oneTimeKey = null; + + List deviceIds = oneTimeKeys.getUserDeviceIds(userId); + + if (null != deviceIds) { + for (String deviceId : deviceIds) { + MXOlmSessionResult olmSessionResult = results.getObject(deviceId, userId); + + if (null != olmSessionResult.mSessionId) { + // We already have a result for this device + continue; + } + + MXKey key = oneTimeKeys.getObject(deviceId, userId); + + if (TextUtils.equals(key.type, oneTimeKeyAlgorithm)) { + oneTimeKey = key; + } + + if (null == oneTimeKey) { + Log.d(LOG_TAG, "## ensureOlmSessionsForDevices() : No one-time keys " + oneTimeKeyAlgorithm + + " for device " + userId + " : " + deviceId); + continue; + } + + // Update the result for this device in results + olmSessionResult.mSessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo); + } + } + } + } + } catch (Exception e) { + Log.e(LOG_TAG, "## ensureOlmSessionsForDevices() " + e.getMessage(), e); + } + + if (!hasBeenReleased()) { + if (null != callback) { + getUIHandler().post(new Runnable() { + @Override + public void run() { + callback.onSuccess(results); + } + }); + } + } + } + }); + } + + @Override + public void onNetworkError(Exception e) { + Log.e(LOG_TAG, "## ensureOlmSessionsForUsers(): claimOneTimeKeysForUsersDevices request failed" + e.getMessage(), e); + + if (null != callback) { + callback.onNetworkError(e); + } + } + + @Override + public void onMatrixError(MatrixError e) { + Log.e(LOG_TAG, "## ensureOlmSessionsForUsers(): claimOneTimeKeysForUsersDevices request failed" + e.getMessage()); + + if (null != callback) { + callback.onMatrixError(e); + } + } + + @Override + public void onUnexpectedError(Exception e) { + Log.e(LOG_TAG, "## ensureOlmSessionsForUsers(): claimOneTimeKeysForUsersDevices request failed" + e.getMessage(), e); + + if (null != callback) { + callback.onUnexpectedError(e); + } + } + }); + } + + private String verifyKeyAndStartSession(MXKey oneTimeKey, String userId, MXDeviceInfo deviceInfo) { + String sessionId = null; + + String deviceId = deviceInfo.deviceId; + String signKeyId = "ed25519:" + deviceId; + String signature = oneTimeKey.signatureForUserId(userId, signKeyId); + + if (!TextUtils.isEmpty(signature) && !TextUtils.isEmpty(deviceInfo.fingerprint())) { + boolean isVerified = false; + String errorMessage = null; + + try { + mOlmDevice.verifySignature(deviceInfo.fingerprint(), oneTimeKey.signalableJSONDictionary(), signature); + isVerified = true; + } catch (Exception e) { + errorMessage = e.getMessage(); + } + + // Check one-time key signature + if (isVerified) { + sessionId = getOlmDevice().createOutboundSession(deviceInfo.identityKey(), oneTimeKey.value); + + if (!TextUtils.isEmpty(sessionId)) { + Log.d(LOG_TAG, "## verifyKeyAndStartSession() : Started new sessionid " + sessionId + + " for device " + deviceInfo + "(theirOneTimeKey: " + oneTimeKey.value + ")"); + } else { + // Possibly a bad key + Log.e(LOG_TAG, "## verifyKeyAndStartSession() : Error starting session with device " + userId + ":" + deviceId); + } + } else { + Log.e(LOG_TAG, "## verifyKeyAndStartSession() : Unable to verify signature on one-time key for device " + userId + + ":" + deviceId + " Error " + errorMessage); + } + } + + return sessionId; + } + + + /** + * 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 room the room the event will be sent. + * @param callback the asynchronous callback + */ + public void encryptEventContent(final JsonElement eventContent, + final String eventType, + final Room room, + final ApiCallback callback) { + // wait that the crypto is really started + if (!isStarted()) { + Log.d(LOG_TAG, "## encryptEventContent() : wait after e2e init"); + + start(false, new ApiCallback() { + @Override + public void onSuccess(Void info) { + encryptEventContent(eventContent, eventType, room, callback); + } + + @Override + public void onNetworkError(Exception e) { + Log.e(LOG_TAG, "## encryptEventContent() : onNetworkError while waiting to start e2e : " + e.getMessage(), e); + + if (null != callback) { + callback.onNetworkError(e); + } + } + + @Override + public void onMatrixError(MatrixError e) { + Log.e(LOG_TAG, "## encryptEventContent() : onMatrixError while waiting to start e2e : " + e.getMessage()); + + if (null != callback) { + callback.onMatrixError(e); + } + } + + @Override + public void onUnexpectedError(Exception e) { + Log.e(LOG_TAG, "## encryptEventContent() : onUnexpectedError while waiting to start e2e : " + e.getMessage(), e); + + if (null != callback) { + callback.onUnexpectedError(e); + } + } + }); + + return; + } + + final ApiCallback> apiCallback = new SimpleApiCallback>(callback) { + @Override + public void onSuccess(final List members) { + // just as you are sending a secret message? + final List userdIds = new ArrayList<>(); + + for (RoomMember m : members) { + userdIds.add(m.getUserId()); + } + + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + IMXEncrypting alg; + + synchronized (mRoomEncryptors) { + alg = mRoomEncryptors.get(room.getRoomId()); + } + + if (null == alg) { + String algorithm = room.getState().encryptionAlgorithm(); + + if (null != algorithm) { + if (setEncryptionInRoom(room.getRoomId(), algorithm, false, members)) { + synchronized (mRoomEncryptors) { + alg = mRoomEncryptors.get(room.getRoomId()); + } + } + } + } + + if (null != alg) { + final long t0 = System.currentTimeMillis(); + Log.d(LOG_TAG, "## encryptEventContent() starts"); + + alg.encryptEventContent(eventContent, eventType, userdIds, new ApiCallback() { + @Override + public void onSuccess(final JsonElement encryptedContent) { + Log.d(LOG_TAG, "## encryptEventContent() : succeeds after " + (System.currentTimeMillis() - t0) + " ms"); + + if (null != callback) { + callback.onSuccess(new MXEncryptEventContentResult(encryptedContent, Event.EVENT_TYPE_MESSAGE_ENCRYPTED)); + } + } + + @Override + public void onNetworkError(final Exception e) { + Log.e(LOG_TAG, "## encryptEventContent() : onNetworkError " + e.getMessage(), e); + + if (null != callback) { + callback.onNetworkError(e); + } + } + + @Override + public void onMatrixError(final MatrixError e) { + Log.e(LOG_TAG, "## encryptEventContent() : onMatrixError " + e.getMessage()); + + if (null != callback) { + callback.onMatrixError(e); + } + } + + @Override + public void onUnexpectedError(final Exception e) { + Log.e(LOG_TAG, "## encryptEventContent() : onUnexpectedError " + e.getMessage(), e); + + if (null != callback) { + callback.onUnexpectedError(e); + } + } + }); + } else { + final String algorithm = room.getState().encryptionAlgorithm(); + final String reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, + (null == algorithm) ? MXCryptoError.NO_MORE_ALGORITHM_REASON : algorithm); + Log.e(LOG_TAG, "## encryptEventContent() : " + reason); + + if (null != callback) { + getUIHandler().post(new Runnable() { + @Override + public void run() { + callback.onMatrixError(new MXCryptoError(MXCryptoError.UNABLE_TO_ENCRYPT_ERROR_CODE, + MXCryptoError.UNABLE_TO_ENCRYPT, reason)); + } + }); + } + } + } + }); + } + }; + + // Check whether the event content must be encrypted for the invited members. + boolean encryptForInvitedMembers = mCryptoConfig.mEnableEncryptionForInvitedMembers + && room.shouldEncryptForInvitedMembers(); + + if (encryptForInvitedMembers) { + room.getActiveMembersAsync(apiCallback); + } else { + room.getJoinedMembersAsync(apiCallback); + } + } + + /** + * 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 MXEventDecryptionResult data, or null in case of error + */ + @Nullable + public MXEventDecryptionResult decryptEvent(final Event event, final String timeline) throws MXDecryptionException { + if (null == event) { + Log.e(LOG_TAG, "## decryptEvent : null event"); + return null; + } + + final EventContent eventContent = event.getWireEventContent(); + + if (null == eventContent) { + Log.e(LOG_TAG, "## decryptEvent : empty event content"); + return null; + } + + final List results = new ArrayList<>(); + final CountDownLatch lock = new CountDownLatch(1); + final List exceptions = new ArrayList<>(); + + getDecryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + MXEventDecryptionResult result = null; + IMXDecrypting alg = getRoomDecryptor(event.roomId, eventContent.algorithm); + + if (null == alg) { + String reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, eventContent.algorithm); + Log.e(LOG_TAG, "## decryptEvent() : " + reason); + exceptions.add(new MXDecryptionException(new MXCryptoError(MXCryptoError.UNABLE_TO_DECRYPT_ERROR_CODE, + MXCryptoError.UNABLE_TO_DECRYPT, reason))); + } else { + try { + result = alg.decryptEvent(event, timeline); + } catch (MXDecryptionException decryptionException) { + exceptions.add(decryptionException); + } + + if (null != result) { + results.add(result); + } + } + lock.countDown(); + } + }); + + try { + lock.await(); + } catch (Exception e) { + Log.e(LOG_TAG, "## decryptEvent() : failed " + e.getMessage(), e); + } + + if (!exceptions.isEmpty()) { + throw exceptions.get(0); + } + + if (!results.isEmpty()) { + return results.get(0); + } + + return null; + } + + /** + * Reset replay attack data for the given timeline. + * + * @param timelineId the timeline id + */ + public void resetReplayAttackCheckInTimeline(final String timelineId) { + if ((null != timelineId) && (null != getOlmDevice())) { + getDecryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + getOlmDevice().resetReplayAttackCheckInTimeline(timelineId); + } + }); + } + } + + /** + * Encrypt an event payload for a list of devices. + * This method must be called from the getCryptoHandler() thread. + * + * @param payloadFields fields to include in the encrypted payload. + * @param deviceInfos list of device infos to encrypt for. + * @return the content for an m.room.encrypted event. + */ + public Map encryptMessage(Map payloadFields, List deviceInfos) { + if (hasBeenReleased()) { + return new HashMap<>(); + } + + Map deviceInfoParticipantKey = new HashMap<>(); + List participantKeys = new ArrayList<>(); + + for (MXDeviceInfo di : deviceInfos) { + participantKeys.add(di.identityKey()); + deviceInfoParticipantKey.put(di.identityKey(), di); + } + + Map payloadJson = new HashMap<>(payloadFields); + + payloadJson.put("sender", mSession.getMyUserId()); + payloadJson.put("sender_device", mSession.getCredentials().deviceId); + + // Include the Ed25519 key so that the recipient knows what + // device this message came from. + // We don't need to include the curve25519 key since the + // recipient will already know this from the olm headers. + // When combined with the device keys retrieved from the + // homeserver signed by the ed25519 key this proves that + // the curve25519 key and the ed25519 key are owned by + // the same device. + Map keysMap = new HashMap<>(); + keysMap.put("ed25519", mOlmDevice.getDeviceEd25519Key()); + payloadJson.put("keys", keysMap); + + Map ciphertext = new HashMap<>(); + + for (String deviceKey : participantKeys) { + String sessionId = mOlmDevice.getSessionId(deviceKey); + + if (!TextUtils.isEmpty(sessionId)) { + Log.d(LOG_TAG, "Using sessionid " + sessionId + " for device " + deviceKey); + MXDeviceInfo deviceInfo = deviceInfoParticipantKey.get(deviceKey); + + payloadJson.put("recipient", deviceInfo.userId); + + Map recipientsKeysMap = new HashMap<>(); + recipientsKeysMap.put("ed25519", deviceInfo.fingerprint()); + payloadJson.put("recipient_keys", recipientsKeysMap); + + + String payloadString = JsonUtils.convertToUTF8(JsonUtils.canonicalize(JsonUtils.getGson(false).toJsonTree(payloadJson)).toString()); + ciphertext.put(deviceKey, mOlmDevice.encryptMessage(deviceKey, sessionId, payloadString)); + } + } + + Map res = new HashMap<>(); + + res.put("algorithm", MXCryptoAlgorithms.MXCRYPTO_ALGORITHM_OLM); + res.put("sender_key", mOlmDevice.getDeviceCurve25519Key()); + res.put("ciphertext", ciphertext); + + return res; + } + + /** + * Handle the 'toDevice' event + * + * @param event the event + */ + private void onToDeviceEvent(final Event event) { + if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_ROOM_KEY) + || TextUtils.equals(event.getType(), Event.EVENT_TYPE_FORWARDED_ROOM_KEY)) { + getDecryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + onRoomKeyEvent(event); + } + }); + } else if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_ROOM_KEY_REQUEST)) { + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + onRoomKeyRequestEvent(event); + } + }); + } + } + + /** + * Handle a key event. + * This method must be called on getDecryptingThreadHandler() thread. + * + * @param event the key event. + */ + private void onRoomKeyEvent(Event event) { + // sanity check + if (null == event) { + Log.e(LOG_TAG, "## onRoomKeyEvent() : null event"); + return; + } + + RoomKeyContent roomKeyContent = JsonUtils.toRoomKeyContent(event.getContentAsJsonObject()); + + String roomId = roomKeyContent.room_id; + String algorithm = roomKeyContent.algorithm; + + if (TextUtils.isEmpty(roomId) || TextUtils.isEmpty(algorithm)) { + Log.e(LOG_TAG, "## onRoomKeyEvent() : missing fields"); + return; + } + + IMXDecrypting alg = getRoomDecryptor(roomId, algorithm); + + if (null == alg) { + Log.e(LOG_TAG, "## onRoomKeyEvent() : Unable to handle keys for " + algorithm); + return; + } + + alg.onRoomKeyEvent(event); + } + + /** + * Called when we get an m.room_key_request event + * This method must be called on getEncryptingThreadHandler() thread. + * + * @param event the announcement event. + */ + private void onRoomKeyRequestEvent(final Event event) { + RoomKeyRequest roomKeyRequest = JsonUtils.toRoomKeyRequest(event.getContentAsJsonObject()); + + if (null != roomKeyRequest.action) { + switch (roomKeyRequest.action) { + case RoomKeyRequest.ACTION_REQUEST: { + synchronized (mReceivedRoomKeyRequests) { + mReceivedRoomKeyRequests.add(new IncomingRoomKeyRequest(event)); + } + break; + } + + case RoomKeyRequest.ACTION_REQUEST_CANCELLATION: { + synchronized (mReceivedRoomKeyRequestCancellations) { + mReceivedRoomKeyRequestCancellations.add(new IncomingRoomKeyRequestCancellation(event)); + } + break; + } + + default: + Log.e(LOG_TAG, "## onRoomKeyRequestEvent() : unsupported action " + roomKeyRequest.action); + } + } + } + + /** + * Process any m.room_key_request events which were queued up during the + * current sync. + */ + private void processReceivedRoomKeyRequests() { + List receivedRoomKeyRequests = null; + + synchronized (mReceivedRoomKeyRequests) { + if (!mReceivedRoomKeyRequests.isEmpty()) { + receivedRoomKeyRequests = new ArrayList(mReceivedRoomKeyRequests); + mReceivedRoomKeyRequests.clear(); + } + } + + if (null != receivedRoomKeyRequests) { + for (final IncomingRoomKeyRequest request : receivedRoomKeyRequests) { + String userId = request.mUserId; + String deviceId = request.mDeviceId; + RoomKeyRequestBody body = request.mRequestBody; + String roomId = body.room_id; + String alg = body.algorithm; + + Log.d(LOG_TAG, "m.room_key_request from " + userId + ":" + deviceId + " for " + roomId + " / " + body.session_id + " id " + request.mRequestId); + + if (!TextUtils.equals(mSession.getMyUserId(), userId)) { + // TODO: determine if we sent this device the keys already: in + Log.e(LOG_TAG, "## processReceivedRoomKeyRequests() : Ignoring room key request from other user for now"); + return; + } + + // todo: should we queue up requests we don't yet have keys for, + // in case they turn up later? + + // if we don't have a decryptor for this room/alg, we don't have + // the keys for the requested events, and can drop the requests. + + final IMXDecrypting decryptor = getRoomDecryptor(roomId, alg); + + if (null == decryptor) { + Log.e(LOG_TAG, "## processReceivedRoomKeyRequests() : room key request for unknown " + alg + " in room " + roomId); + continue; + } + + if (!decryptor.hasKeysForKeyRequest(request)) { + Log.e(LOG_TAG, "## processReceivedRoomKeyRequests() : room key request for unknown session " + body.session_id); + mCryptoStore.deleteIncomingRoomKeyRequest(request); + continue; + } + + if (TextUtils.equals(deviceId, getMyDevice().deviceId) && TextUtils.equals(mSession.getMyUserId(), userId)) { + Log.d(LOG_TAG, "## processReceivedRoomKeyRequests() : oneself device - ignored"); + mCryptoStore.deleteIncomingRoomKeyRequest(request); + continue; + } + + request.mShare = new Runnable() { + @Override + public void run() { + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + decryptor.shareKeysWithDevice(request); + mCryptoStore.deleteIncomingRoomKeyRequest(request); + } + }); + } + }; + + request.mIgnore = new Runnable() { + @Override + public void run() { + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + mCryptoStore.deleteIncomingRoomKeyRequest(request); + } + }); + } + }; + + // if the device is is verified already, share the keys + MXDeviceInfo device = mCryptoStore.getUserDevice(deviceId, userId); + + if (null != device) { + if (device.isVerified()) { + Log.d(LOG_TAG, "## processReceivedRoomKeyRequests() : device is already verified: sharing keys"); + mCryptoStore.deleteIncomingRoomKeyRequest(request); + request.mShare.run(); + continue; + } + + if (device.isBlocked()) { + Log.d(LOG_TAG, "## processReceivedRoomKeyRequests() : device is blocked -> ignored"); + mCryptoStore.deleteIncomingRoomKeyRequest(request); + continue; + } + } + + mCryptoStore.storeIncomingRoomKeyRequest(request); + onRoomKeyRequest(request); + } + } + + List receivedRoomKeyRequestCancellations = null; + + synchronized (mReceivedRoomKeyRequestCancellations) { + if (!mReceivedRoomKeyRequestCancellations.isEmpty()) { + receivedRoomKeyRequestCancellations = new ArrayList(mReceivedRoomKeyRequestCancellations); + mReceivedRoomKeyRequestCancellations.clear(); + } + } + + if (null != receivedRoomKeyRequestCancellations) { + for (IncomingRoomKeyRequestCancellation request : receivedRoomKeyRequestCancellations) { + Log.d(LOG_TAG, "## ## processReceivedRoomKeyRequests() : m.room_key_request cancellation for " + request.mUserId + + ":" + request.mDeviceId + " id " + request.mRequestId); + + // we should probably only notify the app of cancellations we told it + // about, but we don't currently have a record of that, so we just pass + // everything through. + onRoomKeyRequestCancellation(request); + mCryptoStore.deleteIncomingRoomKeyRequest(request); + } + } + } + + /** + * Handle an m.room.encryption event. + * + * @param event the encryption event. + */ + private void onCryptoEvent(final Event event) { + final EventContent eventContent = event.getWireEventContent(); + + final Room room = mSession.getDataHandler().getRoom(event.roomId); + + // Check whether the event content must be encrypted for the invited members. + boolean encryptForInvitedMembers = mCryptoConfig.mEnableEncryptionForInvitedMembers + && room.shouldEncryptForInvitedMembers(); + + ApiCallback> callback = new ApiCallback>() { + @Override + public void onSuccess(final List info) { + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + setEncryptionInRoom(event.roomId, eventContent.algorithm, true, info); + } + }); + } + + private void onError() { + // Ensure setEncryption in room is done, even if there is a failure to fetch the room members + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + setEncryptionInRoom(event.roomId, eventContent.algorithm, true, room.getState().getLoadedMembers()); + } + }); + } + + @Override + public void onNetworkError(Exception e) { + Log.w(LOG_TAG, "[MXCrypto] onCryptoEvent: Warning: Unable to get all members from the HS. Fallback by using lazy-loaded members", e); + + onError(); + } + + @Override + public void onMatrixError(MatrixError e) { + Log.w(LOG_TAG, "[MXCrypto] onCryptoEvent: Warning: Unable to get all members from the HS. Fallback by using lazy-loaded members"); + + onError(); + } + + @Override + public void onUnexpectedError(Exception e) { + Log.w(LOG_TAG, "[MXCrypto] onCryptoEvent: Warning: Unable to get all members from the HS. Fallback by using lazy-loaded members", e); + + onError(); + } + }; + + if (encryptForInvitedMembers) { + room.getActiveMembersAsync(callback); + } else { + room.getJoinedMembersAsync(callback); + } + } + + /** + * Handle a change in the membership state of a member of a room. + * + * @param event the membership event causing the change + */ + private void onRoomMembership(final Event event) { + final IMXEncrypting alg; + + synchronized (mRoomEncryptors) { + alg = mRoomEncryptors.get(event.roomId); + } + + if (null == alg) { + // No encrypting in this room + return; + } + + final String userId = event.stateKey; + final Room room = mSession.getDataHandler().getRoom(event.roomId); + + RoomMember roomMember = room.getState().getMember(userId); + + if (null != roomMember) { + final String membership = roomMember.membership; + + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + if (TextUtils.equals(membership, RoomMember.MEMBERSHIP_JOIN)) { + // make sure we are tracking the deviceList for this user. + getDeviceList().startTrackingDeviceList(Arrays.asList(userId)); + } else if (TextUtils.equals(membership, RoomMember.MEMBERSHIP_INVITE) + && room.shouldEncryptForInvitedMembers() + && mCryptoConfig.mEnableEncryptionForInvitedMembers) { + // track the deviceList for this invited user. + // Caution: there's a big edge case here in that federated servers do not + // know what other servers are in the room at the time they've been invited. + // They therefore will not send device updates if a user logs in whilst + // their state is invite. + getDeviceList().startTrackingDeviceList(Arrays.asList(userId)); + } + } + }); + } + } + + /** + * Upload my user's device keys. + * This method must called on getEncryptingThreadHandler() thread. + * The callback will called on UI thread. + * + * @param callback the asynchronous callback + */ + private void uploadDeviceKeys(ApiCallback callback) { + // Prepare the device keys data to send + // Sign it + String signature = mOlmDevice.signJSON(mMyDevice.signalableJSONDictionary()); + + Map submap = new HashMap<>(); + submap.put("ed25519:" + mMyDevice.deviceId, signature); + + Map> map = new HashMap<>(); + map.put(mSession.getMyUserId(), submap); + + mMyDevice.signatures = map; + + // For now, we set the device id explicitly, as we may not be using the + // same one as used in login. + mSession.getCryptoRestClient().uploadKeys(mMyDevice.JSONDictionary(), null, mMyDevice.deviceId, callback); + } + + /** + * OTK upload loop + * + * @param keyCount the number of key to generate + * @param keyLimit the limit + * @param callback the asynchronous callback + */ + private void uploadLoop(final int keyCount, final int keyLimit, final ApiCallback callback) { + if (keyLimit <= keyCount) { + // If we don't need to generate any more keys then we are done. + if (null != callback) { + getUIHandler().post(new Runnable() { + @Override + public void run() { + callback.onSuccess(null); + } + }); + } + return; + } + + final int keysThisLoop = Math.min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER); + + getOlmDevice().generateOneTimeKeys(keysThisLoop); + + uploadOneTimeKeys(new SimpleApiCallback(callback) { + @Override + public void onSuccess(final KeysUploadResponse response) { + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + if (response.hasOneTimeKeyCountsForAlgorithm("signed_curve25519")) { + uploadLoop(response.oneTimeKeyCountsForAlgorithm("signed_curve25519"), keyLimit, callback); + } else { + Log.e(LOG_TAG, "## uploadLoop() : response for uploading keys does not contain one_time_key_counts.signed_curve25519"); + getUIHandler().post(new Runnable() { + @Override + public void run() { + callback.onUnexpectedError( + new Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519")); + } + }); + } + } + }); + } + }); + } + + /** + * Check if the OTK must be uploaded. + */ + private void maybeUploadOneTimeKeys() { + maybeUploadOneTimeKeys(null); + } + + /** + * Check if the OTK must be uploaded. + * + * @param callback the asynchronous callback + */ + private void maybeUploadOneTimeKeys(final ApiCallback callback) { + if (mOneTimeKeyCheckInProgress) { + getUIHandler().post(new Runnable() { + @Override + public void run() { + if (null != callback) { + callback.onSuccess(null); + } + } + }); + return; + } + + if ((System.currentTimeMillis() - mLastOneTimeKeyCheck) < ONE_TIME_KEY_UPLOAD_PERIOD) { + // we've done a key upload recently. + getUIHandler().post(new Runnable() { + @Override + public void run() { + if (null != callback) { + callback.onSuccess(null); + } + } + }); + return; + } + + mLastOneTimeKeyCheck = System.currentTimeMillis(); + + mOneTimeKeyCheckInProgress = true; + + // We then check how many keys we can store in the Account object. + final long maxOneTimeKeys = getOlmDevice().getMaxNumberOfOneTimeKeys(); + + // Try to keep at most half that number on the server. This leaves the + // rest of the slots free to hold keys that have been claimed from the + // server but we haven't recevied a message for. + // If we run out of slots when generating new keys then olm will + // discard the oldest private keys first. This will eventually clean + // out stale private keys that won't receive a message. + final int keyLimit = (int) Math.floor(maxOneTimeKeys / 2.0); + + if (null != mOneTimeKeyCount) { + uploadOTK(mOneTimeKeyCount, keyLimit, callback); + } else { + // ask the server how many keys we have + mSession.getCryptoRestClient().uploadKeys(null, null, mMyDevice.deviceId, new ApiCallback() { + private void onFailed(String errorMessage) { + if (null != errorMessage) { + Log.e(LOG_TAG, "## uploadKeys() : failed " + errorMessage); + } + mOneTimeKeyCount = null; + mOneTimeKeyCheckInProgress = false; + } + + @Override + public void onSuccess(final KeysUploadResponse keysUploadResponse) { + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + if (!hasBeenReleased()) { + // We need to keep a pool of one time public keys on the server so that + // other devices can start conversations with us. But we can only store + // a finite number of private keys in the olm Account object. + // To complicate things further then can be a delay between a device + // claiming a public one time key from the server and it sending us a + // message. We need to keep the corresponding private key locally until + // we receive the message. + // But that message might never arrive leaving us stuck with duff + // private keys clogging up our local storage. + // So we need some kind of enginering compromise to balance all of + // these factors. + int keyCount = keysUploadResponse.oneTimeKeyCountsForAlgorithm("signed_curve25519"); + uploadOTK(keyCount, keyLimit, callback); + } + } + }); + } + + @Override + public void onNetworkError(final Exception e) { + onFailed(e.getMessage()); + + getUIHandler().post(new Runnable() { + @Override + public void run() { + if (null != callback) { + callback.onNetworkError(e); + } + } + }); + } + + @Override + public void onMatrixError(final MatrixError e) { + onFailed(e.getMessage()); + getUIHandler().post(new Runnable() { + @Override + public void run() { + if (null != callback) { + callback.onMatrixError(e); + } + } + }); + } + + @Override + public void onUnexpectedError(final Exception e) { + onFailed(e.getMessage()); + getUIHandler().post(new Runnable() { + @Override + public void run() { + if (null != callback) { + callback.onUnexpectedError(e); + } + } + }); + } + }); + } + } + + /** + * Upload some the OTKs. + * + * @param keyCount the key count + * @param keyLimit the limit + * @param callback the asynchronous callback + */ + private void uploadOTK(int keyCount, int keyLimit, final ApiCallback callback) { + uploadLoop(keyCount, keyLimit, new ApiCallback() { + private void uploadKeysDone(String errorMessage) { + if (null != errorMessage) { + Log.e(LOG_TAG, "## maybeUploadOneTimeKeys() : failed " + errorMessage); + } + mOneTimeKeyCount = null; + mOneTimeKeyCheckInProgress = false; + } + + @Override + public void onSuccess(Void info) { + Log.d(LOG_TAG, "## maybeUploadOneTimeKeys() : succeeded"); + uploadKeysDone(null); + + getUIHandler().post(new Runnable() { + @Override + public void run() { + if (null != callback) { + callback.onSuccess(null); + } + } + }); + } + + @Override + public void onNetworkError(final Exception e) { + uploadKeysDone(e.getMessage()); + + getUIHandler().post(new Runnable() { + @Override + public void run() { + if (null != callback) { + callback.onNetworkError(e); + } + } + }); + } + + @Override + public void onMatrixError(final MatrixError e) { + uploadKeysDone(e.getMessage()); + + getUIHandler().post(new Runnable() { + @Override + public void run() { + if (null != callback) { + callback.onMatrixError(e); + } + } + }); + } + + @Override + public void onUnexpectedError(final Exception e) { + uploadKeysDone(e.getMessage()); + getUIHandler().post(new Runnable() { + @Override + public void run() { + if (null != callback) { + callback.onUnexpectedError(e); + } + } + }); + } + }); + + } + + /** + * Upload my user's one time keys. + * This method must called on getEncryptingThreadHandler() thread. + * The callback will called on UI thread. + * + * @param callback the asynchronous callback + */ + private void uploadOneTimeKeys(final ApiCallback callback) { + final Map> oneTimeKeys = mOlmDevice.getOneTimeKeys(); + Map oneTimeJson = new HashMap<>(); + + Map curve25519Map = oneTimeKeys.get("curve25519"); + + if (null != curve25519Map) { + for (String key_id : curve25519Map.keySet()) { + Map k = new HashMap<>(); + k.put("key", curve25519Map.get(key_id)); + + // the key is also signed + String signature = mOlmDevice.signJSON(k); + Map submap = new HashMap<>(); + submap.put("ed25519:" + mMyDevice.deviceId, signature); + + Map> map = new HashMap<>(); + map.put(mSession.getMyUserId(), submap); + k.put("signatures", map); + + oneTimeJson.put("signed_curve25519:" + key_id, k); + } + } + + // For now, we set the device id explicitly, as we may not be using the + // same one as used in login. + mSession.getCryptoRestClient().uploadKeys(null, oneTimeJson, mMyDevice.deviceId, new SimpleApiCallback(callback) { + @Override + public void onSuccess(final KeysUploadResponse info) { + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + if (!hasBeenReleased()) { + mLastPublishedOneTimeKeys = oneTimeKeys; + mOlmDevice.markKeysAsPublished(); + + if (null != callback) { + getUIHandler().post(new Runnable() { + @Override + public void run() { + callback.onSuccess(info); + } + }); + } + } + } + }); + } + }); + } + + /** + * Get a decryptor for a given room and algorithm. + * If we already have a decryptor for the given room and algorithm, return + * it. Otherwise try to instantiate it. + * + * @param roomId the room id + * @param algorithm the crypto algorithm + * @return the decryptor + */ + private IMXDecrypting getRoomDecryptor(String roomId, String algorithm) { + // sanity check + if (TextUtils.isEmpty(algorithm)) { + Log.e(LOG_TAG, "## getRoomDecryptor() : null algorithm"); + return null; + } + + if (null == mRoomDecryptors) { + Log.e(LOG_TAG, "## getRoomDecryptor() : null mRoomDecryptors"); + return null; + } + + IMXDecrypting alg = null; + + if (!TextUtils.isEmpty(roomId)) { + synchronized (mRoomDecryptors) { + if (!mRoomDecryptors.containsKey(roomId)) { + mRoomDecryptors.put(roomId, new HashMap()); + } + + alg = mRoomDecryptors.get(roomId).get(algorithm); + } + + if (null != alg) { + return alg; + } + } + + Class decryptingClass = MXCryptoAlgorithms.sharedAlgorithms().decryptorClassForAlgorithm(algorithm); + + if (null != decryptingClass) { + try { + Constructor ctor = decryptingClass.getConstructors()[0]; + alg = (IMXDecrypting) ctor.newInstance(); + + if (null != alg) { + alg.initWithMatrixSession(mSession); + + if (!TextUtils.isEmpty(roomId)) { + synchronized (mRoomDecryptors) { + mRoomDecryptors.get(roomId).put(algorithm, alg); + } + } + } + } catch (Exception e) { + Log.e(LOG_TAG, "## getRoomDecryptor() : fail to load the class", e); + return null; + } + } + + return alg; + } + + /** + * Export the crypto keys + * + * @param password the password + * @param callback the exported keys + */ + public void exportRoomKeys(final String password, final ApiCallback callback) { + exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT, callback); + } + + /** + * Export the crypto keys + * + * @param password the password + * @param anIterationCount the encryption iteration count (0 means no encryption) + * @param callback the exported keys + */ + public void exportRoomKeys(final String password, int anIterationCount, final ApiCallback callback) { + final int iterationCount = Math.max(0, anIterationCount); + + getDecryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + if (null == mCryptoStore) { + getUIHandler().post(new Runnable() { + @Override + public void run() { + callback.onSuccess(new byte[0]); + } + }); + return; + } + + List> exportedSessions = new ArrayList<>(); + + List inboundGroupSessions = mCryptoStore.getInboundGroupSessions(); + + for (MXOlmInboundGroupSession2 session : inboundGroupSessions) { + Map map = session.exportKeys(); + + if (null != map) { + exportedSessions.add(map); + } + } + + final byte[] encryptedRoomKeys; + + try { + encryptedRoomKeys = MXMegolmExportEncryption + .encryptMegolmKeyFile(JsonUtils.getGson(false).toJsonTree(exportedSessions).toString(), password, iterationCount); + } catch (Exception e) { + callback.onUnexpectedError(e); + return; + } + + getUIHandler().post(new Runnable() { + @Override + public void run() { + callback.onSuccess(encryptedRoomKeys); + } + }); + } + }); + } + + /** + * Import the room keys + * + * @param roomKeysAsArray the room keys as array. + * @param password the password + * @param callback the asynchronous callback. + */ + public void importRoomKeys(final byte[] roomKeysAsArray, final String password, final ApiCallback callback) { + getDecryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + long t0 = System.currentTimeMillis(); + String roomKeys; + + try { + roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password); + } catch (final Exception e) { + getUIHandler().post(new Runnable() { + @Override + public void run() { + callback.onUnexpectedError(e); + } + }); + return; + } + + List> importedSessions; + + long t1 = System.currentTimeMillis(); + + Log.d(LOG_TAG, "## importRoomKeys starts"); + + try { + importedSessions = JsonUtils.getGson(false).fromJson(roomKeys, new TypeToken>>() { + }.getType()); + } catch (final Exception e) { + Log.e(LOG_TAG, "## importRoomKeys failed " + e.getMessage(), e); + getUIHandler().post(new Runnable() { + @Override + public void run() { + callback.onUnexpectedError(e); + } + }); + return; + } + + long t2 = System.currentTimeMillis(); + + Log.d(LOG_TAG, "## importRoomKeys retrieve " + importedSessions.size() + "sessions in " + (t1 - t0) + " ms"); + + for (int index = 0; index < importedSessions.size(); index++) { + Map map = importedSessions.get(index); + + MXOlmInboundGroupSession2 session = mOlmDevice.importInboundGroupSession(map); + + if ((null != session) && mRoomDecryptors.containsKey(session.mRoomId)) { + IMXDecrypting decrypting = mRoomDecryptors.get(session.mRoomId).get(map.get("algorithm")); + + if (null != decrypting) { + try { + String sessionId = session.mSession.sessionIdentifier(); + Log.d(LOG_TAG, "## importRoomKeys retrieve mSenderKey " + session.mSenderKey + " sessionId " + sessionId); + + decrypting.onNewSession(session.mSenderKey, sessionId); + } catch (Exception e) { + Log.e(LOG_TAG, "## importRoomKeys() : onNewSession failed " + e.getMessage(), e); + } + } + } + } + + long t3 = System.currentTimeMillis(); + + Log.d(LOG_TAG, "## importRoomKeys : done in " + (t3 - t0) + " ms (" + importedSessions.size() + " sessions)"); + Log.d(LOG_TAG, "## importRoomKeys : decryptMegolmKeyFile done in " + (t1 - t0) + " ms"); + Log.d(LOG_TAG, "## importRoomKeys : JSON parsing " + (t2 - t1) + " ms"); + Log.d(LOG_TAG, "## importRoomKeys : sessions import " + (t3 - t2) + " ms"); + + getUIHandler().post(new Runnable() { + @Override + public void run() { + callback.onSuccess(null); + } + }); + } + }); + } + + /** + * Tells if the encryption must fail if some unknown devices are detected. + * + * @return true to warn when some unknown devices are detected. + */ + public boolean warnOnUnknownDevices() { + return mWarnOnUnknownDevices; + } + + /** + * Update the warn status when some unknown devices are detected. + * + * @param warn true to warn when some unknown devices are detected. + */ + public void setWarnOnUnknownDevices(boolean warn) { + mWarnOnUnknownDevices = warn; + } + + /** + * Provides the list of unknown devices + * + * @param devicesInRoom the devices map + * @return the unknown devices map + */ + public static MXUsersDevicesMap getUnknownDevices(MXUsersDevicesMap devicesInRoom) { + MXUsersDevicesMap unknownDevices = new MXUsersDevicesMap<>(); + + List userIds = devicesInRoom.getUserIds(); + for (String userId : userIds) { + List deviceIds = devicesInRoom.getUserDeviceIds(userId); + for (String deviceId : deviceIds) { + MXDeviceInfo deviceInfo = devicesInRoom.getObject(deviceId, userId); + + if (deviceInfo.isUnknown()) { + unknownDevices.setObject(deviceInfo, userId, deviceId); + } + } + } + + return unknownDevices; + } + + /** + * Check if the user ids list have some unknown devices. + * A success means there is no unknown devices. + * If there are some unknown devices, a MXCryptoError.UNKNOWN_DEVICES_CODE exception is triggered. + * + * @param userIds the user ids list + * @param callback the asynchronous callback. + */ + public void checkUnknownDevices(List userIds, final ApiCallback callback) { + // force the refresh to ensure that the devices list is up-to-date + mDevicesList.downloadKeys(userIds, true, new SimpleApiCallback>(callback) { + @Override + public void onSuccess(MXUsersDevicesMap devicesMap) { + MXUsersDevicesMap unknownDevices = MXCrypto.getUnknownDevices(devicesMap); + + if (unknownDevices.getMap().size() == 0) { + callback.onSuccess(null); + } else { + // trigger an an unknown devices exception + callback.onMatrixError(new MXCryptoError(MXCryptoError.UNKNOWN_DEVICES_CODE, + MXCryptoError.UNABLE_TO_ENCRYPT, MXCryptoError.UNKNOWN_DEVICES_REASON, unknownDevices)); + } + } + }); + } + + /** + * 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 + * @param callback the asynchronous callback. + */ + public void setGlobalBlacklistUnverifiedDevices(final boolean block, final ApiCallback callback) { + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + mCryptoStore.setGlobalBlacklistUnverifiedDevices(block); + getUIHandler().post(new Runnable() { + @Override + public void run() { + if (null != callback) { + callback.onSuccess(null); + } + } + }); + } + }); + } + + /** + * Tells whether the client should ever send encrypted messages to unverified devices. + * The default value is false. + * This function must be called in the getEncryptingThreadHandler() thread. + * + * @return true to unilaterally blacklist all unverified devices. + */ + public boolean getGlobalBlacklistUnverifiedDevices() { + return mCryptoStore.getGlobalBlacklistUnverifiedDevices(); + } + + /** + * Tells whether the client should ever send encrypted messages to unverified devices. + * The default value is false. + * messages to unverified devices. + * + * @param callback the asynchronous callback + */ + public void getGlobalBlacklistUnverifiedDevices(final ApiCallback callback) { + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + if (null != callback) { + final boolean status = getGlobalBlacklistUnverifiedDevices(); + + getUIHandler().post(new Runnable() { + @Override + public void run() { + callback.onSuccess(status); + } + }); + } + } + }); + } + + /** + * Tells whether the client should encrypt messages only for the verified devices + * in this room. + * The default value is false. + * This function must be called in the getEncryptingThreadHandler() thread. + * + * @param roomId the room id + * @return true if the client should encrypt messages only for the verified devices. + */ + public boolean isRoomBlacklistUnverifiedDevices(String roomId) { + if (null != roomId) { + return mCryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId); + } else { + return false; + } + } + + /** + * Tells whether the client should encrypt messages only for the verified devices + * in this room. + * The default value is false. + * This function must be called in the getEncryptingThreadHandler() thread. + * + * @param roomId the room id + * @param callback the asynchronous callback + */ + public void isRoomBlacklistUnverifiedDevices(final String roomId, final ApiCallback callback) { + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + final boolean status = isRoomBlacklistUnverifiedDevices(roomId); + + getUIHandler().post(new Runnable() { + @Override + public void run() { + if (null != callback) { + callback.onSuccess(status); + } + } + }); + } + }); + } + + /** + * Manages the room black-listing for unverified devices. + * + * @param roomId the room id + * @param add true to add the room id to the list, false to remove it. + * @param callback the asynchronous callback + */ + private void setRoomBlacklistUnverifiedDevices(final String roomId, final boolean add, final ApiCallback callback) { + final Room room = mSession.getDataHandler().getRoom(roomId); + + // sanity check + if (null == room) { + getUIHandler().post(new Runnable() { + @Override + public void run() { + callback.onSuccess(null); + } + }); + + return; + } + + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + List roomIds = mCryptoStore.getRoomsListBlacklistUnverifiedDevices(); + + if (add) { + if (!roomIds.contains(roomId)) { + roomIds.add(roomId); + } + } else { + roomIds.remove(roomId); + } + + mCryptoStore.setRoomsListBlacklistUnverifiedDevices(roomIds); + + getUIHandler().post(new Runnable() { + @Override + public void run() { + if (null != callback) { + callback.onSuccess(null); + } + } + }); + } + }); + } + + + /** + * Add this room to the ones which don't encrypt messages to unverified devices. + * + * @param roomId the room id + * @param callback the asynchronous callback + */ + public void setRoomBlacklistUnverifiedDevices(final String roomId, final ApiCallback callback) { + setRoomBlacklistUnverifiedDevices(roomId, true, callback); + } + + /** + * Remove this room to the ones which don't encrypt messages to unverified devices. + * + * @param roomId the room id + * @param callback the asynchronous callback + */ + public void setRoomUnblacklistUnverifiedDevices(final String roomId, final ApiCallback callback) { + setRoomBlacklistUnverifiedDevices(roomId, false, callback); + } + + /** + * Send a request for some room keys, if we have not already done so. + * + * @param requestBody requestBody + * @param recipients recipients + */ + public void requestRoomKey(final Map requestBody, final List> recipients) { + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + mOutgoingRoomKeyRequestManager.sendRoomKeyRequest(requestBody, recipients); + } + }); + } + + /** + * Cancel any earlier room key request + * + * @param requestBody requestBody + */ + public void cancelRoomKeyRequest(final Map requestBody) { + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + mOutgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody); + } + }); + } + + /** + * Re request the encryption keys required to decrypt an event. + * + * @param event the event to decrypt again. + */ + public void reRequestRoomKeyForEvent(@NonNull final Event event) { + if (event.getWireContent().isJsonObject()) { + JsonObject wireContent = event.getWireContent().getAsJsonObject(); + + final String algorithm = wireContent.get("algorithm").getAsString(); + final String sender_key = wireContent.get("sender_key").getAsString(); + final String session_id = wireContent.get("session_id").getAsString(); + + getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + Map requestBody = new HashMap<>(); + requestBody.put("room_id", event.roomId); + requestBody.put("algorithm", algorithm); + requestBody.put("sender_key", sender_key); + requestBody.put("session_id", session_id); + + mOutgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody); + } + }); + } + } + + /** + * Room keys events listener + */ + public interface IRoomKeysRequestListener { + /** + * An room key request has been received. + * + * @param request the request + */ + void onRoomKeyRequest(IncomingRoomKeyRequest request); + + /** + * A room key request cancellation has been received. + * + * @param request the cancellation request + */ + void onRoomKeyRequestCancellation(IncomingRoomKeyRequestCancellation request); + } + + // the listeners + public final Set mRoomKeysRequestListeners = new HashSet<>(); + + /** + * Add a IRoomKeysRequestListener listener. + * + * @param listener listener + */ + public void addRoomKeysRequestListener(IRoomKeysRequestListener listener) { + synchronized (mRoomKeysRequestListeners) { + mRoomKeysRequestListeners.add(listener); + } + } + + /** + * Add a IRoomKeysRequestListener listener. + * + * @param listener listener + */ + public void removeRoomKeysRequestListener(IRoomKeysRequestListener listener) { + synchronized (mRoomKeysRequestListeners) { + mRoomKeysRequestListeners.remove(listener); + } + } + + /** + * Dispatch onRoomKeyRequest + * + * @param request the request + */ + private void onRoomKeyRequest(IncomingRoomKeyRequest request) { + synchronized (mRoomKeysRequestListeners) { + for (IRoomKeysRequestListener listener : mRoomKeysRequestListeners) { + try { + listener.onRoomKeyRequest(request); + } catch (Exception e) { + Log.e(LOG_TAG, "## onRoomKeyRequest() failed " + e.getMessage(), e); + } + } + } + } + + + /** + * A room key request cancellation has been received. + * + * @param request the cancellation request + */ + private void onRoomKeyRequestCancellation(IncomingRoomKeyRequestCancellation request) { + synchronized (mRoomKeysRequestListeners) { + for (IRoomKeysRequestListener listener : mRoomKeysRequestListeners) { + try { + listener.onRoomKeyRequestCancellation(request); + } catch (Exception e) { + Log.e(LOG_TAG, "## onRoomKeyRequestCancellation() failed " + e.getMessage(), e); + } + } + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXCryptoAlgorithms.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXCryptoAlgorithms.java new file mode 100755 index 0000000000..8a4c2199e2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXCryptoAlgorithms.java @@ -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> mEncryptors; + + // decryptors map + private final Map> 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) 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) 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) 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) 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 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 decryptorClassForAlgorithm(String algorithm) { + if (!TextUtils.isEmpty(algorithm)) { + return mDecryptors.get(algorithm); + } else { + return null; + } + } + + /** + * @return The list of registered algorithms. + */ + public List supportedAlgorithms() { + return new ArrayList<>(mEncryptors.keySet()); + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXCryptoConfig.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXCryptoConfig.java new file mode 100644 index 0000000000..f1ae3f50f4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXCryptoConfig.java @@ -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; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXCryptoError.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXCryptoError.java new file mode 100644 index 0000000000..9c98d4e282 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXCryptoError.java @@ -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; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXDecryptionException.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXDecryptionException.java new file mode 100644 index 0000000000..78ab57f7ef --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXDecryptionException.java @@ -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(); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXDeviceList.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXDeviceList.java new file mode 100755 index 0000000000..230cd2a500 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXDeviceList.java @@ -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 + *

+ * | + * 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 mUserKeyDownloadsInProgress = new HashSet<>(); + + // HS not ready for retry + private final Set mNotReadyToRetryHS = new HashSet<>(); + + // indexed by UserId + private final Map mPendingDownloadKeysRequestToken = new HashMap<>(); + + // download keys queue + class DownloadKeysPromise { + // list of remain pending device keys + final List mPendingUserIdsList; + + // the unfiltered user ids list + final List mUserIdsList; + + // the request callback + final ApiCallback> mCallback; + + /** + * Creator + * + * @param userIds the user ids list + * @param callback the asynchronous callback + */ + DownloadKeysPromise(List userIds, ApiCallback> callback) { + mPendingUserIdsList = new ArrayList<>(userIds); + mUserIdsList = new ArrayList<>(userIds); + mCallback = callback; + } + } + + // pending queues list + private final List 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 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 addDownloadKeysPromise(List userIds, ApiCallback> callback) { + if (null != userIds) { + List filteredUserIds = new ArrayList<>(); + List 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 userIds) { + if (null != userIds) { + boolean isUpdated = false; + Map 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 changed, List left) { + boolean isUpdated = false; + Map 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 userIds) { + if (null != userIds) { + synchronized (mUserKeyDownloadsInProgress) { + Map 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 userIds, Map> failures) { + if (null != failures) { + Set keys = failures.keySet(); + + for (String k : keys) { + Map 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 deviceTrackingStatuses = mCryptoStore.getDeviceTrackingStatuses(); + + if (null != userIds) { + if (mDownloadKeysQueues.size() > 0) { + List promisesToRemove = new ArrayList<>(); + + for (DownloadKeysPromise promise : mDownloadKeysQueues) { + promise.mPendingUserIdsList.removeAll(userIds); + + if (promise.mPendingUserIdsList.size() == 0) { + // private members + final MXUsersDevicesMap usersDevicesInfoMap = new MXUsersDevicesMap<>(); + + for (String userId : promise.mUserIdsList) { + Map 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> 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 userIds, boolean forceDownload, final ApiCallback> callback) { + Log.d(LOG_TAG, "## downloadKeys() : forceDownload " + forceDownload + " : " + userIds); + + // Map from userid -> deviceid -> DeviceInfo + final MXUsersDevicesMap stored = new MXUsersDevicesMap<>(); + + // List of user ids we need to download keys for + final List 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 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>() { + public void onSuccess(MXUsersDevicesMap 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 downloadUsers, final ApiCallback> callback) { + Log.d(LOG_TAG, "## doKeyDownloadForUsers() : doKeyDownloadForUsers " + downloadUsers); + + // get the user ids which did not already trigger a keys download + final List 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() { + @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 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 devices = keysQueryResponse.deviceKeys.get(userId); + + Log.d(LOG_TAG, "## doKeyDownloadForUsers() : Got keys for " + userId + " : " + devices); + + if (null != devices) { + Map mutableDevices = new HashMap<>(devices); + List 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 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 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 users = new ArrayList<>(); + + Map 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>() { + @Override + public void onSuccess(final MXUsersDevicesMap 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()); + } + }); + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXEncryptedAttachments.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXEncryptedAttachments.java new file mode 100755 index 0000000000..cd8e736cc3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXEncryptedAttachments.java @@ -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; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXEventDecryptionResult.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXEventDecryptionResult.java new file mode 100644 index 0000000000..f2759c551f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXEventDecryptionResult.java @@ -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 mForwardingCurve25519KeyChain = new ArrayList<>(); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXMegolmExportEncryption.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXMegolmExportEncryption.java new file mode 100755 index 0000000000..beb1b5dff6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXMegolmExportEncryption.java @@ -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; + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXOlmDevice.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXOlmDevice.java new file mode 100755 index 0000000000..0690f5dc1b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXOlmDevice.java @@ -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 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 "||" + // Values are true. + private final Map> 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 JSONDictionary) { + return signMessage(JsonUtils.getCanonicalizedJsonString(JSONDictionary)); + } + + /** + * @return The current (unused, unpublished) one-time keys for this account. + */ + public Map> 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 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 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 getSessionIds(String theirDeviceIdentityKey) { + Map 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 sessionIds = getSessionIds(theirDeviceIdentityKey); + + if ((null != sessionIds) && (0 != sessionIds.size())) { + List 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 encryptMessage(String theirDeviceIdentityKey, String sessionId, String payloadString) { + Map 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 forwardingCurve25519KeyChain, + Map 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 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 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 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 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); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXOutgoingRoomKeyRequestManager.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXOutgoingRoomKeyRequestManager.java new file mode 100755 index 0000000000..f3f1dc1d98 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/MXOutgoingRoomKeyRequestManager.java @@ -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. + *

+ * 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 requestBody, final List> 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 requestBody) { + cancelRoomKeyRequest(requestBody, false); + } + + /** + * Cancel room key requests, if any match the given details, and resend + * + * @param requestBody requestBody + */ + public void resendRoomKeyRequest(final Map 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 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 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() { + 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 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() { + 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 message, + List> recipients, + String transactionId, + final ApiCallback callback) { + MXUsersDevicesMap> contentMap = new MXUsersDevicesMap<>(); + + for (Map recipient : recipients) { + contentMap.setObject(message, recipient.get("userId"), recipient.get("deviceId")); + } + + mSession.getCryptoRestClient().sendToDevice(Event.EVENT_TYPE_ROOM_KEY_REQUEST, contentMap, transactionId, callback); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/OutgoingRoomKeyRequest.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/OutgoingRoomKeyRequest.java new file mode 100755 index 0000000000..5e9ba695f3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/OutgoingRoomKeyRequest.java @@ -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> mRecipients; + + // RequestBody + public Map mRequestBody; + + // current state of this request + public RequestState mState; + + public OutgoingRoomKeyRequest(Map requestBody, List> 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; + } +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/IMXDecrypting.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/IMXDecrypting.java new file mode 100644 index 0000000000..958b0d6f94 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/IMXDecrypting.java @@ -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); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/IMXEncrypting.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/IMXEncrypting.java new file mode 100644 index 0000000000..3765b4f59f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/IMXEncrypting.java @@ -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 userIds, ApiCallback callback); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/MXDecryptionResult.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/MXDecryptionResult.java new file mode 100755 index 0000000000..e584276089 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/MXDecryptionResult.java @@ -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 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 mForwardingCurve25519KeyChain; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/megolm/MXMegolmDecryption.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/megolm/MXMegolmDecryption.java new file mode 100644 index 0000000000..12f3e1d240 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/megolm/MXMegolmDecryption.java @@ -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>> 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> recipients = new ArrayList<>(); + + Map selfMap = new HashMap<>(); + selfMap.put("userId", mSession.getMyUserId()); + selfMap.put("deviceId", "*"); + recipients.add(selfMap); + + if (!TextUtils.equals(sender, mSession.getMyUserId())) { + Map senderMap = new HashMap<>(); + senderMap.put("userId", sender); + senderMap.put("deviceId", wireContent.device_id); + recipients.add(senderMap); + } + + Map 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>()); + } + + if (!mPendingEvents.get(k).containsKey(timelineId)) { + mPendingEvents.get(k).put(timelineId, new ArrayList()); + } + + 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 keysClaimed = new HashMap<>(); + List 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 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> pending = mPendingEvents.get(k); + + if (null != pending) { + // Have another go at decrypting events sent with this session. + mPendingEvents.remove(k); + + Set timelineIds = pending.keySet(); + + for (String timelineId : timelineIds) { + List 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>() { + @Override + public void onSuccess(MXUsersDevicesMap devicesMap) { + final String deviceId = request.mDeviceId; + final MXDeviceInfo deviceInfo = mSession.getCrypto().mCryptoStore.getUserDevice(deviceId, userId); + + if (null != deviceInfo) { + final RoomKeyRequestBody body = request.mRequestBody; + + Map> devicesByUser = new HashMap<>(); + devicesByUser.put(userId, new ArrayList<>(Arrays.asList(deviceInfo))); + + mSession.getCrypto().ensureOlmSessionsForDevices(devicesByUser, new ApiCallback>() { + @Override + public void onSuccess(MXUsersDevicesMap 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 payloadJson = new HashMap<>(); + payloadJson.put("type", Event.EVENT_TYPE_FORWARDED_ROOM_KEY); + payloadJson.put("content", inboundGroupSession.exportKeys()); + + Map encodedPayload = mSession.getCrypto().encryptMessage(payloadJson, Arrays.asList(deviceInfo)); + MXUsersDevicesMap> 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() { + @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); + } + }); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/megolm/MXMegolmEncryption.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/megolm/MXMegolmEncryption.java new file mode 100644 index 0000000000..eb4e13c771 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/megolm/MXMegolmEncryption.java @@ -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 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 getPendingEncryptions() { + List list = new ArrayList<>(); + + synchronized (mPendingEncryptions) { + list.addAll(mPendingEncryptions); + } + + return list; + } + + @Override + public void encryptEventContent(final JsonElement eventContent, + final String eventType, + final List userIds, + final ApiCallback 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>() { + + /** + * 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 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 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 queuedEncryptions = getPendingEncryptions(); + + for (MXQueuedEncryption queuedEncryption : queuedEncryptions) { + queuedEncryption.mApiCallback.onUnexpectedError(e); + } + + synchronized (mPendingEncryptions) { + mPendingEncryptions.removeAll(queuedEncryptions); + } + } + + @Override + public void onSuccess(MXUsersDevicesMap devicesInRoom) { + ensureOutboundSession(devicesInRoom, new ApiCallback() { + @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 keysClaimedMap = new HashMap<>(); + keysClaimedMap.put("ed25519", olmDevice.getDeviceEd25519Key()); + + olmDevice.addInboundGroupSession(sessionId, olmDevice.getSessionKey(sessionId), mRoomId, olmDevice.getDeviceCurve25519Key(), + new ArrayList(), keysClaimedMap, false); + + return new MXOutboundSessionInfo(sessionId); + } + + /** + * Ensure the outbound session + * + * @param devicesInRoom the devices list + * @param callback the asynchronous callback. + */ + private void ensureOutboundSession(MXUsersDevicesMap devicesInRoom, final ApiCallback 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> shareMap = new HashMap<>(); + + List userIds = devicesInRoom.getUserIds(); + + for (String userId : userIds) { + List 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()); + } + + shareMap.get(userId).add(deviceInfo); + } + } + } + + shareKey(fSession, shareMap, new ApiCallback() { + @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> devicesByUsers, + final ApiCallback 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> subMap = new HashMap<>(); + + final List userIds = new ArrayList<>(); + int devicesCount = 0; + + for (String userId : devicesByUsers.keySet()) { + List 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() { + @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> devicesByUser, + final ApiCallback callback) { + final String sessionKey = mCrypto.getOlmDevice().getSessionKey(session.mSessionId); + final int chainIndex = mCrypto.getOlmDevice().getMessageIndex(session.mSessionId); + + Map 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 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>() { + @Override + public void onSuccess(final MXUsersDevicesMap results) { + mCrypto.getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + Log.d(LOG_TAG, "## shareUserDevicesKey() : ensureOlmSessionsForDevices succeeds after " + (System.currentTimeMillis() - t0) + " ms"); + MXUsersDevicesMap> contentMap = new MXUsersDevicesMap<>(); + + boolean haveTargets = false; + List userIds = results.getUserIds(); + + for (String userId : userIds) { + List 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() { + @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 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 queuedEncryptions = getPendingEncryptions(); + + // Everything is in place, encrypt all pending events + for (MXQueuedEncryption queuedEncryption : queuedEncryptions) { + Map 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 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 userIds, final ApiCallback> 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>(callback) { + @Override + public void onSuccess(final MXUsersDevicesMap devices) { + mCrypto.getEncryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + boolean encryptToVerifiedDevicesOnly = mCrypto.getGlobalBlacklistUnverifiedDevices() + || mCrypto.isRoomBlacklistUnverifiedDevices(mRoomId); + + final MXUsersDevicesMap devicesInRoom = new MXUsersDevicesMap<>(); + final MXUsersDevicesMap unknownDevices = new MXUsersDevicesMap<>(); + + List userIds = devices.getUserIds(); + + for (String userId : userIds) { + List 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); + } + } + }); + } + }); + } + }); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/megolm/MXOutboundSessionInfo.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/megolm/MXOutboundSessionInfo.java new file mode 100644 index 0000000000..b57e928ae2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/megolm/MXOutboundSessionInfo.java @@ -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 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 devicesInRoom) { + List 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 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; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/olm/MXOlmDecryption.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/olm/MXOlmDecryption.java new file mode 100644 index 0000000000..20ca52f64a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/olm/MXOlmDecryption.java @@ -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 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 message = (Map) 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 message, String theirDeviceIdentityKey) { + Set sessionIdsSet = mOlmDevice.getSessionIds(theirDeviceIdentityKey); + + List 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 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"); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/olm/MXOlmEncryption.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/olm/MXOlmEncryption.java new file mode 100644 index 0000000000..e3db32cdcb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/algorithms/olm/MXOlmEncryption.java @@ -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 getUserDevices(final String userId) { + Map map = mCrypto.getCryptoStore().getUserDevices(userId); + return (null != map) ? new ArrayList<>(map.values()) : new ArrayList(); + } + + @Override + public void encryptEventContent(final JsonElement eventContent, + final String eventType, + final List userIds, + final ApiCallback 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(callback) { + @Override + public void onSuccess(Void info) { + List deviceInfos = new ArrayList<>(); + + for (String userId : userIds) { + List 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 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 users, final ApiCallback callback) { + mCrypto.getDeviceList().downloadKeys(users, false, new SimpleApiCallback>(callback) { + @Override + public void onSuccess(MXUsersDevicesMap info) { + mCrypto.ensureOlmSessionsForUsers(users, new SimpleApiCallback>(callback) { + @Override + public void onSuccess(MXUsersDevicesMap result) { + if (null != callback) { + callback.onSuccess(null); + } + } + }); + } + }); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXDeviceInfo.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXDeviceInfo.java new file mode 100755 index 0000000000..5bb2e7bc16 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXDeviceInfo.java @@ -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 algorithms; + + /** + * A map from : to >. + */ + public Map keys; + + /** + * The signature of this MXDeviceInfo. + * A map from : to >. + */ + public Map> signatures; + + /* + * Additional data from the home server. + */ + public Map 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 signalableJSONDictionary() { + Map 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 JSONDictionary() { + Map 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; + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXEncryptEventContentResult.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXEncryptEventContentResult.java new file mode 100755 index 0000000000..76a67847ca --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXEncryptEventContentResult.java @@ -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; + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXKey.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXKey.java new file mode 100755 index 0000000000..7666bda261 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXKey.java @@ -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> signatures; + + /** + * Default constructor + */ + public MXKey() { + } + + /** + * Convert a map to a MXKey + * + * @param map the map to convert + */ + public MXKey(Map> map) { + if ((null != map) && (map.size() > 0)) { + List mapKeys = new ArrayList<>(map.keySet()); + + String firstEntry = mapKeys.get(0); + setKeyFullId(firstEntry); + + Map params = map.get(firstEntry); + value = (String) params.get("key"); + signatures = (Map>) 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 signalableJSONDictionary() { + Map 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; + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXOlmInboundGroupSession.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXOlmInboundGroupSession.java new file mode 100755 index 0000000000..45713015f0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXOlmInboundGroupSession.java @@ -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 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); + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXOlmInboundGroupSession2.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXOlmInboundGroupSession2.java new file mode 100755 index 0000000000..d98838cd1e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXOlmInboundGroupSession2.java @@ -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 mKeysClaimed; + + // Devices which forwarded this session to us (normally empty). + public List 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 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) 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 exportKeys() { + Map 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; + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXOlmSessionResult.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXOlmSessionResult.java new file mode 100755 index 0000000000..9f9d55a7a5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXOlmSessionResult.java @@ -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; + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXQueuedEncryption.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXQueuedEncryption.java new file mode 100755 index 0000000000..b134e2d284 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXQueuedEncryption.java @@ -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 mApiCallback; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXUsersDevicesMap.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXUsersDevicesMap.java new file mode 100755 index 0000000000..8b1cdbb4f0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/crypto/data/MXUsersDevicesMap.java @@ -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 implements Serializable { + + // The device keys as returned by the homeserver: a map of a map (userId -> deviceId -> Object). + private final Map> mMap = new HashMap<>(); + + /** + * @return the inner map + */ + public Map> getMap() { + return mMap; + } + + /** + * Default constructor constructor + */ + public MXUsersDevicesMap() { + } + + /** + * The constructor + * + * @param map the map + */ + public MXUsersDevicesMap(Map> map) { + if (null != map) { + Set keys = map.keySet(); + + for (String key : keys) { + mMap.put(key, new HashMap<>(map.get(key))); + } + } + } + + /** + * @return a deep copy + */ + public MXUsersDevicesMap deepCopy() { + MXUsersDevicesMap copy = new MXUsersDevicesMap<>(); + + Set keys = mMap.keySet(); + + for (String key : keys) { + copy.mMap.put(key, new HashMap<>(mMap.get(key))); + } + + return copy; + } + + /** + * @return the user Ids + */ + public List 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 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 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 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 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"; + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/DataRetriever.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/DataRetriever.java new file mode 100644 index 0000000000..5bf6e46586 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/DataRetriever.java @@ -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 mPendingForwardRequestTokenByRoomId = new HashMap<>(); + private final Map mPendingBackwardRequestTokenByRoomId = new HashMap<>(); + private final Map 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 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 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 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(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 callback) { + putPendingToken(mPendingForwardRequestTokenByRoomId, roomId, token); + + mRestClient.getRoomMessagesFrom(roomId, token, EventTimeline.Direction.FORWARDS, RoomsRestClient.DEFAULT_MESSAGES_PAGINATION_LIMIT, + FilterUtil.createRoomEventFilter(withLazyLoading), + new SimpleApiCallback(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 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 callback) { + putPendingToken(mPendingRemoteRequestTokenByRoomId, roomId, token); + + mRestClient.getRoomMessagesFrom(roomId, token, EventTimeline.Direction.BACKWARDS, paginationCount, FilterUtil.createRoomEventFilter(withLazyLoading), + new SimpleApiCallback(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 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 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 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); + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/MyUser.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/MyUser.java new file mode 100644 index 0000000000..f57fdc1341 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/MyUser.java @@ -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> mRefreshListeners; + + private transient final Handler mUiHandler; + + // linked emails to the account + private transient List mEmailIdentifiers = new ArrayList<>(); + // linked phone number to the account + private transient List 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 callback) { + mDataHandler.getProfileRestClient().updateDisplayname(displayName, new SimpleApiCallback(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 callback) { + mDataHandler.getProfileRestClient().updateAvatarUrl(avatarUrl, new SimpleApiCallback(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 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 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 callback) { + if (null != pid) { + mDataHandler.getProfileRestClient().add3PID(pid, bind, new SimpleApiCallback(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 callback) { + if (null != pid) { + mDataHandler.getProfileRestClient().delete3PID(pid, new SimpleApiCallback(callback) { + @Override + public void onSuccess(Void info) { + // refresh the third party identifiers lists + refreshThirdPartyIdentifiers(callback); + } + }); + } + } + + /** + * Build the lists of identifiers + */ + private void buildIdentifiersLists() { + List 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 getlinkedEmails() { + if (mEmailIdentifiers == null) { + buildIdentifiersLists(); + } + + return mEmailIdentifiers; + } + + /** + * @return the list of linked emails + */ + public List 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 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 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 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 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() { + @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() { + @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>() { + @Override + public void onSuccess(List 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); + } + }); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/Pusher.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/Pusher.java new file mode 100644 index 0000000000..ad44666bcd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/Pusher.java @@ -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 data; + public Boolean append; + + + @Override + public java.lang.String toString() { + return "Pusher : \n\tappDisplayName " + appDisplayName + "\n\tdeviceDisplayName " + deviceDisplayName + "\n\tpushkey " + pushkey; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/Room.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/Room.java new file mode 100644 index 0000000000..6f1358fc9f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/Room.java @@ -0,0 +1,2939 @@ +/* + * 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.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.ExifInterface; +import android.media.MediaMetadataRetriever; +import android.media.MediaPlayer; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Pair; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; + +import java.io.File; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import im.vector.matrix.android.R; +import im.vector.matrix.android.internal.legacy.MXDataHandler; +import im.vector.matrix.android.internal.legacy.MXPatterns; +import im.vector.matrix.android.internal.legacy.call.MXCallsManager; +import im.vector.matrix.android.internal.legacy.crypto.MXCryptoError; +import im.vector.matrix.android.internal.legacy.crypto.data.MXEncryptEventContentResult; +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.data.timeline.EventTimelineFactory; +import im.vector.matrix.android.internal.legacy.db.MXMediasCache; +import im.vector.matrix.android.internal.legacy.listeners.IMXEventListener; +import im.vector.matrix.android.internal.legacy.listeners.MXEventListener; +import im.vector.matrix.android.internal.legacy.listeners.MXRoomEventListener; +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.AccountDataRestClient; +import im.vector.matrix.android.internal.legacy.rest.client.RoomsRestClient; +import im.vector.matrix.android.internal.legacy.rest.client.UrlPostTask; +import im.vector.matrix.android.internal.legacy.rest.model.BannedUser; +import im.vector.matrix.android.internal.legacy.rest.model.CreatedEvent; +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.PowerLevels; +import im.vector.matrix.android.internal.legacy.rest.model.ReceiptData; +import im.vector.matrix.android.internal.legacy.rest.model.RoomDirectoryVisibility; +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.message.FileInfo; +import im.vector.matrix.android.internal.legacy.rest.model.message.FileMessage; +import im.vector.matrix.android.internal.legacy.rest.model.message.ImageInfo; +import im.vector.matrix.android.internal.legacy.rest.model.message.ImageMessage; +import im.vector.matrix.android.internal.legacy.rest.model.message.LocationMessage; +import im.vector.matrix.android.internal.legacy.rest.model.message.Message; +import im.vector.matrix.android.internal.legacy.rest.model.message.ThumbnailInfo; +import im.vector.matrix.android.internal.legacy.rest.model.message.VideoInfo; +import im.vector.matrix.android.internal.legacy.rest.model.message.VideoMessage; +import im.vector.matrix.android.internal.legacy.rest.model.sync.InvitedRoomSync; +import im.vector.matrix.android.internal.legacy.rest.model.sync.RoomResponse; +import im.vector.matrix.android.internal.legacy.rest.model.sync.RoomSync; +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; + +/** + * Class representing a room and the interactions we have with it. + */ +public class Room { + + private static final String LOG_TAG = Room.class.getSimpleName(); + + // Account data + private RoomAccountData mAccountData = new RoomAccountData(); + + // handler + private MXDataHandler mDataHandler; + + // store + private IMXStore mStore; + + private String mMyUserId = null; + + // Map to keep track of the listeners the client adds vs. the ones we actually register to the global data handler. + // This is needed to find the right one when removing the listener. + private final Map mEventListeners = new HashMap<>(); + + // the user is leaving the room + private boolean mIsLeaving = false; + + // the room is syncing + private boolean mIsSyncing; + + // the unread messages count must be refreshed when the current sync is done. + private boolean mRefreshUnreadAfterSync = false; + + // the time line + private EventTimeline mTimeline; + + // initial sync callback. + private ApiCallback mOnInitialSyncCallback; + + // This is used to block live events and history requests until the state is fully processed and ready + private boolean mIsReady = false; + + // call conference user id + private String mCallConferenceUserId; + + // true when the current room is a left one + private boolean mIsLeft; + + /** + * Constructor + * FIXME All this @NonNull annotation must be also added to the class members and getters + * + * @param dataHandler the data handler + * @param store the store + * @param roomId the room id + */ + public Room(@NonNull final MXDataHandler dataHandler, @NonNull final IMXStore store, @NonNull final String roomId) { + mDataHandler = dataHandler; + mStore = store; + mMyUserId = mDataHandler.getUserId(); + mTimeline = EventTimelineFactory.liveTimeline(mDataHandler, this, roomId); + } + + /** + * @return the used data handler + */ + public MXDataHandler getDataHandler() { + return mDataHandler; + } + + /** + * @return the store in which the room is stored + */ + public IMXStore getStore() { + if (null == mStore) { + if (null != mDataHandler) { + mStore = mDataHandler.getStore(getRoomId()); + } + + if (null == mStore) { + Log.e(LOG_TAG, "## getStore() : cannot retrieve the store of " + getRoomId()); + } + } + + return mStore; + } + + /** + * Determine whether we should encrypt messages for invited users in this room. + *

+ * Check here whether the invited members are allowed to read messages in the room history + * from the point they were invited onwards. + * + * @return true if we should encrypt messages for invited users. + */ + public boolean shouldEncryptForInvitedMembers() { + String historyVisibility = getState().history_visibility; + return !TextUtils.equals(historyVisibility, RoomState.HISTORY_VISIBILITY_JOINED); + } + + /** + * Tells if the room is a call conference one + * i.e. this room has been created to manage the call conference + * + * @return true if it is a call conference room. + */ + public boolean isConferenceUserRoom() { + return getState().isConferenceUserRoom(); + } + + /** + * Set this room as a conference user room + * + * @param isConferenceUserRoom true when it is an user conference room. + */ + public void setIsConferenceUserRoom(boolean isConferenceUserRoom) { + getState().setIsConferenceUserRoom(isConferenceUserRoom); + } + + /** + * Test if there is an ongoing conference call. + * + * @return true if there is one. + */ + public boolean isOngoingConferenceCall() { + RoomMember conferenceUser = getState().getMember(MXCallsManager.getConferenceUserId(getRoomId())); + return (null != conferenceUser) && TextUtils.equals(conferenceUser.membership, RoomMember.MEMBERSHIP_JOIN); + } + + /** + * Defines that the current room is a left one + * + * @param isLeft true when the current room is a left one + */ + public void setIsLeft(boolean isLeft) { + mIsLeft = isLeft; + mTimeline.setIsHistorical(isLeft); + } + + /** + * @return true if the current room is an left one + */ + public boolean isLeft() { + return mIsLeft; + } + + //================================================================================ + // Sync events + //================================================================================ + + /** + * Manage list of ephemeral events + * + * @param events the ephemeral events + */ + private void handleEphemeralEvents(List events) { + for (Event event : events) { + + // ensure that the room Id is defined + event.roomId = getRoomId(); + + try { + if (Event.EVENT_TYPE_RECEIPT.equals(event.getType())) { + if (event.roomId != null) { + List senders = handleReceiptEvent(event); + + if (senders != null && !senders.isEmpty()) { + mDataHandler.onReceiptEvent(event.roomId, senders); + } + } + } else if (Event.EVENT_TYPE_TYPING.equals(event.getType())) { + JsonObject eventContent = event.getContentAsJsonObject(); + + if (eventContent.has("user_ids")) { + synchronized (mTypingUsers) { + mTypingUsers.clear(); + + List typingUsers = null; + + try { + typingUsers = (new Gson()).fromJson(eventContent.get("user_ids"), new TypeToken>() { + }.getType()); + } catch (Exception e) { + Log.e(LOG_TAG, "## handleEphemeralEvents() : exception " + e.getMessage(), e); + } + + if (typingUsers != null) { + mTypingUsers.addAll(typingUsers); + } + } + } + + mDataHandler.onLiveEvent(event, getState()); + } + } catch (Exception e) { + Log.e(LOG_TAG, "ephemeral event failed " + e.getMessage(), e); + } + } + } + + /** + * Handle the events of a joined room. + * + * @param roomSync the sync events list. + * @param isGlobalInitialSync true if the room is initialized by a global initial sync. + */ + public void handleJoinedRoomSync(RoomSync roomSync, boolean isGlobalInitialSync) { + if (null != mOnInitialSyncCallback) { + Log.d(LOG_TAG, "initial sync handleJoinedRoomSync " + getRoomId()); + } else { + Log.d(LOG_TAG, "handleJoinedRoomSync " + getRoomId()); + } + + mIsSyncing = true; + + synchronized (this) { + mTimeline.handleJoinedRoomSync(roomSync, isGlobalInitialSync); + RoomSummary roomSummary = getRoomSummary(); + if (roomSummary != null) { + roomSummary.setIsJoined(); + } + // ephemeral events + if ((null != roomSync.ephemeral) && (null != roomSync.ephemeral.events)) { + handleEphemeralEvents(roomSync.ephemeral.events); + } + + // Handle account data events (if any) + if ((null != roomSync.accountData) && (null != roomSync.accountData.events) && (roomSync.accountData.events.size() > 0)) { + if (isGlobalInitialSync) { + Log.d(LOG_TAG, "## handleJoinedRoomSync : received " + roomSync.accountData.events.size() + " account data events"); + } + + handleAccountDataEvents(roomSync.accountData.events); + } + } + + // the user joined the room + // With V2 sync, the server sends the events to init the room. + if ((null != mOnInitialSyncCallback) && isJoined()) { + Log.d(LOG_TAG, "handleJoinedRoomSync " + getRoomId() + " : the initial sync is done"); + final ApiCallback fOnInitialSyncCallback = mOnInitialSyncCallback; + + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + // to initialise the notification counters + markAllAsRead(null); + + try { + fOnInitialSyncCallback.onSuccess(null); + } catch (Exception e) { + Log.e(LOG_TAG, "handleJoinedRoomSync : onSuccess failed" + e.getMessage(), e); + } + } + }); + + mOnInitialSyncCallback = null; + } + + mIsSyncing = false; + + if (mRefreshUnreadAfterSync) { + if (!isGlobalInitialSync) { + refreshUnreadCounter(); + } // else -> it will be done at the end of the sync + mRefreshUnreadAfterSync = false; + } + } + + /** + * Handle the invitation room events + * + * @param invitedRoomSync the invitation room events. + */ + public void handleInvitedRoomSync(InvitedRoomSync invitedRoomSync) { + mTimeline.handleInvitedRoomSync(invitedRoomSync); + + RoomSummary roomSummary = getRoomSummary(); + + if (roomSummary != null) { + roomSummary.setIsInvited(); + } + } + + /** + * Store an outgoing event. + * + * @param event the event. + */ + public void storeOutgoingEvent(Event event) { + mTimeline.storeOutgoingEvent(event); + } + + /** + * Request events to the server. The local cache is not used. + * The events will not be saved in the local storage. + * + * @param token the token to go back from. + * @param paginationCount the number of events to retrieve. + * @param callback the onComplete callback + */ + public void requestServerRoomHistory(final String token, + final int paginationCount, + final ApiCallback callback) { + mDataHandler.getDataRetriever() + .requestServerRoomHistory(getRoomId(), token, paginationCount, mDataHandler.isLazyLoadingEnabled(), + new SimpleApiCallback(callback) { + @Override + public void onSuccess(TokensChunkEvents info) { + callback.onSuccess(info); + } + }); + } + + /** + * cancel any remote request + */ + public void cancelRemoteHistoryRequest() { + mDataHandler.getDataRetriever().cancelRemoteHistoryRequest(getRoomId()); + } + + //================================================================================ + // Getters / setters + //================================================================================ + + public String getRoomId() { + return getState().roomId; + } + + public void setAccountData(RoomAccountData accountData) { + mAccountData = accountData; + } + + public RoomAccountData getAccountData() { + return mAccountData; + } + + public RoomState getState() { + return mTimeline.getState(); + } + + public boolean isLeaving() { + return mIsLeaving; + } + + public void getMembersAsync(@NonNull final ApiCallback> callback) { + getState().getMembersAsync(callback); + } + + public void getDisplayableMembersAsync(@NonNull final ApiCallback> callback) { + getState().getDisplayableMembersAsync(callback); + } + + public EventTimeline getTimeline() { + return mTimeline; + } + + public void setTimeline(EventTimeline eventTimeline) { + mTimeline = eventTimeline; + } + + public void setReadyState(boolean isReady) { + mIsReady = isReady; + } + + public boolean isReady() { + return mIsReady; + } + + /** + * @return the list of active members in a room ie joined or invited ones. + */ + public void getActiveMembersAsync(@NonNull final ApiCallback> callback) { + getMembersAsync(new SimpleApiCallback>(callback) { + @Override + public void onSuccess(List members) { + List activeMembers = new ArrayList<>(); + String conferenceUserId = MXCallsManager.getConferenceUserId(getRoomId()); + + for (RoomMember member : members) { + if (!TextUtils.equals(member.getUserId(), conferenceUserId)) { + if (TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_JOIN) + || TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_INVITE)) { + activeMembers.add(member); + } + } + } + + callback.onSuccess(activeMembers); + } + }); + } + + /** + * Get the list of the members who have joined the room. + * + * @return the list the joined members of the room. + */ + public void getJoinedMembersAsync(final ApiCallback> callback) { + getMembersAsync(new SimpleApiCallback>(callback) { + @Override + public void onSuccess(List members) { + List joinedMembersList = new ArrayList<>(); + + for (RoomMember member : members) { + if (TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_JOIN)) { + joinedMembersList.add(member); + } + } + + callback.onSuccess(joinedMembersList); + } + }); + } + + @Nullable + public RoomMember getMember(String userId) { + return getState().getMember(userId); + } + + // member event caches + private final Map mMemberEventByEventId = new HashMap<>(); + + public void getMemberEvent(final String userId, final ApiCallback callback) { + final Event event; + final RoomMember member = getMember(userId); + + if ((null != member) && (null != member.getOriginalEventId())) { + event = mMemberEventByEventId.get(member.getOriginalEventId()); + + if (null == event) { + mDataHandler.getDataRetriever().getRoomsRestClient().getEvent(getRoomId(), member.getOriginalEventId(), new SimpleApiCallback(callback) { + @Override + public void onSuccess(Event event) { + if (null != event) { + mMemberEventByEventId.put(event.eventId, event); + } + callback.onSuccess(event); + } + }); + return; + } + } else { + event = null; + } + + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + callback.onSuccess(event); + } + }); + } + + public String getTopic() { + return getState().topic; + } + + public String getVisibility() { + return getState().visibility; + } + + /** + * @return true if the user is invited to the room + */ + public boolean isInvited() { + if (getRoomSummary() == null) { + return false; + } + + return getRoomSummary().isInvited(); + } + + /** + * @return true if the user has joined the room + */ + public boolean isJoined() { + if (getRoomSummary() == null) { + return false; + } + + return getRoomSummary().isJoined(); + } + + /** + * @return true is the user is a member of the room (invited or joined) + */ + public boolean isMember() { + return isJoined() || isInvited(); + } + + /** + * @return true if the user is invited in a direct chat room + */ + public boolean isDirectChatInvitation() { + if (isInvited()) { + // Is it an initial sync for this room ? + RoomState state = getState(); + + RoomMember selfMember = state.getMember(mMyUserId); + + if ((null != selfMember) && (null != selfMember.isDirect)) { + return selfMember.isDirect; + } + } + + return false; + } + + //================================================================================ + // Join + //================================================================================ + + /** + * Defines the initial sync callback + * + * @param callback the new callback. + */ + public void setOnInitialSyncCallback(ApiCallback callback) { + mOnInitialSyncCallback = callback; + } + + /** + * Join a room with an url to post before joined the room. + * + * @param alias the room alias + * @param thirdPartySignedUrl the thirdPartySigned url + * @param callback the callback + */ + public void joinWithThirdPartySigned(final String alias, final String thirdPartySignedUrl, final ApiCallback callback) { + if (null == thirdPartySignedUrl) { + join(alias, callback); + } else { + String url = thirdPartySignedUrl + "&mxid=" + mMyUserId; + UrlPostTask task = new UrlPostTask(); + + task.setListener(new UrlPostTask.IPostTaskListener() { + @Override + public void onSucceed(JsonObject object) { + Map map = null; + + try { + map = new Gson().fromJson(object, new TypeToken>() { + }.getType()); + } catch (Exception e) { + Log.e(LOG_TAG, "joinWithThirdPartySigned : Gson().fromJson failed" + e.getMessage(), e); + } + + if (null != map) { + Map joinMap = new HashMap<>(); + joinMap.put("third_party_signed", map); + join(alias, joinMap, callback); + } else { + join(callback); + } + } + + @Override + public void onError(String errorMessage) { + Log.d(LOG_TAG, "joinWithThirdPartySigned failed " + errorMessage); + + // cannot validate the url + // try without validating the url + join(callback); + } + }); + + // avoid crash if there are too many running task + try { + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url); + } catch (final Exception e) { + task.cancel(true); + Log.e(LOG_TAG, "joinWithThirdPartySigned : task.executeOnExecutor failed" + e.getMessage(), e); + + (new android.os.Handler(Looper.getMainLooper())).post(new Runnable() { + @Override + public void run() { + if (null != callback) { + callback.onUnexpectedError(e); + } + } + }); + } + } + } + + /** + * Join the room. If successful, the room's current state will be loaded before calling back onComplete. + * + * @param callback the callback for when done + */ + public void join(final ApiCallback callback) { + join(null, null, callback); + } + + /** + * Join the room. If successful, the room's current state will be loaded before calling back onComplete. + * + * @param roomAlias the room alias + * @param callback the callback for when done + */ + private void join(String roomAlias, ApiCallback callback) { + join(roomAlias, null, callback); + } + + /** + * Join the room. If successful, the room's current state will be loaded before calling back onComplete. + * + * @param roomAlias the room alias + * @param extraParams the join extra params + * @param callback the callback for when done + */ + private void join(final String roomAlias, final Map extraParams, final ApiCallback callback) { + Log.d(LOG_TAG, "Join the room " + getRoomId() + " with alias " + roomAlias); + + mDataHandler.getDataRetriever().getRoomsRestClient() + .joinRoom((null != roomAlias) ? roomAlias : getRoomId(), extraParams, new SimpleApiCallback(callback) { + @Override + public void onSuccess(final RoomResponse aResponse) { + try { + // the join request did not get the room initial history + if (!isJoined()) { + Log.d(LOG_TAG, "the room " + getRoomId() + " is joined but wait after initial sync"); + + // wait the server sends the events chunk before calling the callback + setOnInitialSyncCallback(callback); + } else { + Log.d(LOG_TAG, "the room " + getRoomId() + " is joined : the initial sync has been done"); + // to initialise the notification counters + markAllAsRead(null); + // already got the initial sync + callback.onSuccess(null); + } + } catch (Exception e) { + Log.e(LOG_TAG, "join exception " + e.getMessage(), e); + } + } + + @Override + public void onNetworkError(Exception e) { + Log.e(LOG_TAG, "join onNetworkError " + e.getMessage(), e); + callback.onNetworkError(e); + } + + @Override + public void onMatrixError(MatrixError e) { + Log.e(LOG_TAG, "join onMatrixError " + e.getMessage()); + + if (MatrixError.UNKNOWN.equals(e.errcode) && TextUtils.equals("No known servers", e.error)) { + // minging kludge until https://matrix.org/jira/browse/SYN-678 is fixed + // 'Error when trying to join an empty room should be more explicit + e.error = getStore().getContext().getString(R.string.room_error_join_failed_empty_room); + } + + // if the alias is not found + // try with the room id + if ((e.mStatus == 404) && !TextUtils.isEmpty(roomAlias)) { + Log.e(LOG_TAG, "Retry without the room alias"); + join(null, extraParams, callback); + return; + } + + callback.onMatrixError(e); + } + + @Override + public void onUnexpectedError(Exception e) { + Log.e(LOG_TAG, "join onUnexpectedError " + e.getMessage(), e); + callback.onUnexpectedError(e); + } + }); + } + + //================================================================================ + // Room info (liveState) update + //================================================================================ + + /** + * This class dispatches the error to the dedicated callbacks. + * If the operation succeeds, the room state is saved because calling the callback. + */ + private class RoomInfoUpdateCallback extends SimpleApiCallback { + private final ApiCallback mCallback; + + /** + * Constructor + */ + public RoomInfoUpdateCallback(ApiCallback callback) { + super(callback); + mCallback = callback; + } + + @Override + public void onSuccess(T info) { + getStore().storeLiveStateForRoom(getRoomId()); + + if (null != mCallback) { + mCallback.onSuccess(info); + } + } + } + + /** + * Update the power level of the user userId + * + * @param userId the user id + * @param powerLevel the new power level + * @param callback the callback with the created event + */ + public void updateUserPowerLevels(String userId, int powerLevel, ApiCallback callback) { + PowerLevels powerLevels = getState().getPowerLevels().deepCopy(); + powerLevels.setUserPowerLevel(userId, powerLevel); + mDataHandler.getDataRetriever().getRoomsRestClient().updatePowerLevels(getRoomId(), powerLevels, callback); + } + + /** + * Update the room's name. + * + * @param aRoomName the new name + * @param callback the async callback + */ + public void updateName(final String aRoomName, final ApiCallback callback) { + mDataHandler.getDataRetriever().getRoomsRestClient().updateRoomName(getRoomId(), aRoomName, new RoomInfoUpdateCallback(callback) { + @Override + public void onSuccess(Void info) { + getState().name = aRoomName; + super.onSuccess(info); + } + }); + } + + /** + * Update the room's topic. + * + * @param aTopic the new topic + * @param callback the async callback + */ + public void updateTopic(final String aTopic, final ApiCallback callback) { + mDataHandler.getDataRetriever().getRoomsRestClient().updateTopic(getRoomId(), aTopic, new RoomInfoUpdateCallback(callback) { + @Override + public void onSuccess(Void info) { + getState().topic = aTopic; + super.onSuccess(info); + } + }); + } + + /** + * Update the room's main alias. + * + * @param aCanonicalAlias the canonical alias + * @param callback the async callback + */ + public void updateCanonicalAlias(final String aCanonicalAlias, final ApiCallback callback) { + final String fCanonicalAlias = TextUtils.isEmpty(aCanonicalAlias) ? null : aCanonicalAlias; + + mDataHandler.getDataRetriever().getRoomsRestClient().updateCanonicalAlias(getRoomId(), fCanonicalAlias, new RoomInfoUpdateCallback(callback) { + @Override + public void onSuccess(Void info) { + getState().setCanonicalAlias(aCanonicalAlias); + super.onSuccess(info); + } + }); + } + + /** + * Provides the room aliases list. + * The result is never null. + * + * @return the room aliases list. + */ + public List getAliases() { + return getState().getAliases(); + } + + /** + * Remove a room alias. + * + * @param alias the alias to remove + * @param callback the async callback + */ + public void removeAlias(final String alias, final ApiCallback callback) { + final List updatedAliasesList = new ArrayList<>(getAliases()); + + // nothing to do + if (TextUtils.isEmpty(alias) || (updatedAliasesList.indexOf(alias) < 0)) { + if (null != callback) { + callback.onSuccess(null); + } + return; + } + + mDataHandler.getDataRetriever().getRoomsRestClient().removeRoomAlias(alias, new RoomInfoUpdateCallback(callback) { + @Override + public void onSuccess(Void info) { + getState().removeAlias(alias); + super.onSuccess(info); + } + }); + } + + /** + * Try to add an alias to the aliases list. + * + * @param alias the alias to add. + * @param callback the the async callback + */ + public void addAlias(final String alias, final ApiCallback callback) { + final List updatedAliasesList = new ArrayList<>(getAliases()); + + // nothing to do + if (TextUtils.isEmpty(alias) || (updatedAliasesList.indexOf(alias) >= 0)) { + if (null != callback) { + callback.onSuccess(null); + } + return; + } + + mDataHandler.getDataRetriever().getRoomsRestClient().setRoomIdByAlias(getRoomId(), alias, new RoomInfoUpdateCallback(callback) { + @Override + public void onSuccess(Void info) { + getState().addAlias(alias); + super.onSuccess(info); + } + }); + } + + /** + * Add a group to the related ones + * + * @param groupId the group id to add + * @param callback the asynchronous callback + */ + public void addRelatedGroup(final String groupId, final ApiCallback callback) { + List nextGroupIdsList = new ArrayList<>(getState().getRelatedGroups()); + + if (!nextGroupIdsList.contains(groupId)) { + nextGroupIdsList.add(groupId); + } + + updateRelatedGroups(nextGroupIdsList, callback); + } + + /** + * Remove a group id from the related ones. + * + * @param groupId the group id + * @param callback the asynchronous callback + */ + public void removeRelatedGroup(final String groupId, final ApiCallback callback) { + List nextGroupIdsList = new ArrayList<>(getState().getRelatedGroups()); + nextGroupIdsList.remove(groupId); + + updateRelatedGroups(nextGroupIdsList, callback); + } + + /** + * Update the related group ids list + * + * @param groupIds the new related groups + * @param callback the asynchronous callback + */ + public void updateRelatedGroups(final List groupIds, final ApiCallback callback) { + Map params = new HashMap<>(); + params.put("groups", groupIds); + + mDataHandler.getDataRetriever().getRoomsRestClient() + .sendStateEvent(getRoomId(), Event.EVENT_TYPE_STATE_RELATED_GROUPS, null, params, new SimpleApiCallback(callback) { + @Override + public void onSuccess(Void info) { + getState().groups = groupIds; + getDataHandler().getStore().storeLiveStateForRoom(getRoomId()); + + if (null != callback) { + callback.onSuccess(null); + } + } + }); + } + + + /** + * @return the room avatar URL. If there is no defined one, use the members one (1:1 chat only). + */ + @Nullable + public String getAvatarUrl() { + String res = getState().getAvatarUrl(); + + // detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat) + if (null == res) { + if (getNumberOfMembers() == 1 && !getState().getLoadedMembers().isEmpty()) { + res = getState().getLoadedMembers().get(0).getAvatarUrl(); + } else if (getNumberOfMembers() == 2 && getState().getLoadedMembers().size() > 1) { + RoomMember m1 = getState().getLoadedMembers().get(0); + RoomMember m2 = getState().getLoadedMembers().get(1); + + res = TextUtils.equals(m1.getUserId(), mMyUserId) ? m2.getAvatarUrl() : m1.getAvatarUrl(); + } + } + + return res; + } + + /** + * The call avatar is the same as the room avatar except there are only 2 JOINED members. + * In this case, it returns the avtar of the other joined member. + * + * @return the call avatar URL. + */ + @Nullable + public String getCallAvatarUrl() { + String avatarURL; + + if (getNumberOfMembers() == 2 && getState().getLoadedMembers().size() > 1) { + RoomMember m1 = getState().getLoadedMembers().get(0); + RoomMember m2 = getState().getLoadedMembers().get(1); + + // use other member avatar. + if (TextUtils.equals(mMyUserId, m1.getUserId())) { + avatarURL = m2.getAvatarUrl(); + } else { + avatarURL = m1.getAvatarUrl(); + } + } else { + // + avatarURL = getAvatarUrl(); + } + + return avatarURL; + } + + /** + * Update the room avatar URL. + * + * @param avatarUrl the new avatar URL + * @param callback the async callback + */ + public void updateAvatarUrl(final String avatarUrl, final ApiCallback callback) { + mDataHandler.getDataRetriever().getRoomsRestClient().updateAvatarUrl(getRoomId(), avatarUrl, new RoomInfoUpdateCallback(callback) { + @Override + public void onSuccess(Void info) { + getState().url = avatarUrl; + super.onSuccess(info); + } + }); + } + + /** + * Update the room's history visibility + * + * @param historyVisibility the visibility (should be one of RoomState.HISTORY_VISIBILITY_XX values) + * @param callback the async callback + */ + public void updateHistoryVisibility(final String historyVisibility, final ApiCallback callback) { + mDataHandler.getDataRetriever().getRoomsRestClient() + .updateHistoryVisibility(getRoomId(), historyVisibility, new RoomInfoUpdateCallback(callback) { + @Override + public void onSuccess(Void info) { + getState().history_visibility = historyVisibility; + super.onSuccess(info); + } + }); + } + + /** + * Update the directory's visibility + * + * @param visibility the visibility (should be one of RoomState.HISTORY_VISIBILITY_XX values) + * @param callback the async callback + */ + public void updateDirectoryVisibility(final String visibility, final ApiCallback callback) { + mDataHandler.getDataRetriever().getRoomsRestClient().updateDirectoryVisibility(getRoomId(), visibility, new RoomInfoUpdateCallback(callback) { + @Override + public void onSuccess(Void info) { + getState().visibility = visibility; + super.onSuccess(info); + } + }); + } + + /** + * Get the directory visibility of the room (see {@link #updateDirectoryVisibility(String, ApiCallback)}). + * The directory visibility indicates if the room is listed among the directory list. + * + * @param roomId the user Id. + * @param callback the callback returning the visibility response value. + */ + public void getDirectoryVisibility(final String roomId, final ApiCallback callback) { + RoomsRestClient roomRestApi = mDataHandler.getDataRetriever().getRoomsRestClient(); + + if (null != roomRestApi) { + roomRestApi.getDirectoryVisibility(roomId, new SimpleApiCallback(callback) { + @Override + public void onSuccess(RoomDirectoryVisibility roomDirectoryVisibility) { + RoomState currentRoomState = getState(); + if (null != currentRoomState) { + currentRoomState.visibility = roomDirectoryVisibility.visibility; + } + + if (null != callback) { + callback.onSuccess(roomDirectoryVisibility.visibility); + } + } + }); + } + } + + /** + * Update the join rule of the room. + * + * @param aRule the join rule: {@link RoomState#JOIN_RULE_PUBLIC} or {@link RoomState#JOIN_RULE_INVITE} + * @param aCallBackResp the async callback + */ + public void updateJoinRules(final String aRule, final ApiCallback aCallBackResp) { + mDataHandler.getDataRetriever().getRoomsRestClient().updateJoinRules(getRoomId(), aRule, new RoomInfoUpdateCallback(aCallBackResp) { + @Override + public void onSuccess(Void info) { + getState().join_rule = aRule; + super.onSuccess(info); + } + }); + } + + /** + * Update the guest access rule of the room. + * To deny guest access to the room, aGuestAccessRule must be set to {@link RoomState#GUEST_ACCESS_FORBIDDEN}. + * + * @param aGuestAccessRule the guest access rule: {@link RoomState#GUEST_ACCESS_CAN_JOIN} or {@link RoomState#GUEST_ACCESS_FORBIDDEN} + * @param callback the async callback + */ + public void updateGuestAccess(final String aGuestAccessRule, final ApiCallback callback) { + mDataHandler.getDataRetriever().getRoomsRestClient().updateGuestAccess(getRoomId(), aGuestAccessRule, new RoomInfoUpdateCallback(callback) { + @Override + public void onSuccess(Void info) { + getState().guest_access = aGuestAccessRule; + super.onSuccess(info); + } + }); + } + + //================================================================================ + // Read receipts events + //================================================================================ + + /** + * @return the call conference user id + */ + private String getCallConferenceUserId() { + if (null == mCallConferenceUserId) { + mCallConferenceUserId = MXCallsManager.getConferenceUserId(getRoomId()); + } + + return mCallConferenceUserId; + } + + /** + * Handle a receiptData. + * + * @param receiptData the receiptData. + * @return true if there a store update. + */ + public boolean handleReceiptData(ReceiptData receiptData) { + if (!TextUtils.equals(receiptData.userId, getCallConferenceUserId()) && (null != getStore())) { + boolean isUpdated = getStore().storeReceipt(receiptData, getRoomId()); + + // check oneself receipts + // if there is an update, it means that the messages have been read from another client + // it requires to update the summary to display valid information. + if (isUpdated && TextUtils.equals(mMyUserId, receiptData.userId)) { + RoomSummary summary = getStore().getSummary(getRoomId()); + + if (null != summary) { + summary.setReadReceiptEventId(receiptData.eventId); + getStore().flushSummary(summary); + } + + refreshUnreadCounter(); + } + + return isUpdated; + } else { + return false; + } + } + + /** + * Handle receipt event. + * Event content will contains the receipts dictionaries + *

+     * key   : $EventId
+     * value : dict key @UserId
+     *              value dict key "ts"
+     *                    dict value ts value
+     * 
+ *

+ * Example: + *

+     * {
+     *     "$1535657109773196ZjoWE:matrix.org": {
+     *         "m.read": {
+     *             "@slash_benoit:matrix.org": {
+     *                 "ts": 1535708570621
+     *             },
+     *             "@benoit.marty:matrix.org": {
+     *                 "ts": 1535657109472
+     *             }
+     *         }
+     *     }
+     * },
+     * 
+ * + * @param event the event receipts. + * @return the sender user IDs list. + */ + private List handleReceiptEvent(Event event) { + List senderIDs = new ArrayList<>(); + + try { + Type type = new TypeToken>>>>() { + }.getType(); + Map>>> receiptsDict = JsonUtils.getGson(false).fromJson(event.getContent(), type); + + for (String eventId : receiptsDict.keySet()) { + Map>> receiptDict = receiptsDict.get(eventId); + + for (String receiptType : receiptDict.keySet()) { + // only the read receipts are managed + if (TextUtils.equals(receiptType, "m.read")) { + Map> userIdsDict = receiptDict.get(receiptType); + + for (String userID : userIdsDict.keySet()) { + Map paramsDict = userIdsDict.get(userID); + + for (String paramName : paramsDict.keySet()) { + if (TextUtils.equals("ts", paramName)) { + Double value = (Double) paramsDict.get(paramName); + long ts = value.longValue(); + + if (handleReceiptData(new ReceiptData(userID, eventId, ts))) { + senderIDs.add(userID); + } + } + } + } + } + } + } + } catch (Exception e) { + Log.e(LOG_TAG, "handleReceiptEvent : failed" + e.getMessage(), e); + } + + return senderIDs; + } + + /** + * Clear the unread message counters + * + * @param summary the room summary + */ + private void clearUnreadCounters(RoomSummary summary) { + Log.d(LOG_TAG, "## clearUnreadCounters " + getRoomId()); + + // reset the notification count + getState().setHighlightCount(0); + getState().setNotificationCount(0); + + if (null != getStore()) { + getStore().storeLiveStateForRoom(getRoomId()); + + // flush the summary + if (null != summary) { + summary.setUnreadEventsCount(0); + summary.setHighlightCount(0); + summary.setNotificationCount(0); + getStore().flushSummary(summary); + } + + getStore().commit(); + } + } + + /** + * @return the read marker event id + */ + public String getReadMarkerEventId() { + if (null == getStore()) { + return null; + } + + RoomSummary summary = getStore().getSummary(getRoomId()); + + if (null != summary) { + return (null != summary.getReadMarkerEventId()) ? summary.getReadMarkerEventId() : summary.getReadReceiptEventId(); + } else { + return null; + } + } + + /** + * Mark all the messages as read. + * It also move the read marker to the latest known messages + * + * @param aRespCallback the asynchronous callback + * @return true if the request is sent, false otherwise + */ + public boolean markAllAsRead(final ApiCallback aRespCallback) { + return markAllAsRead(true, aRespCallback); + } + + /** + * Mark all the messages as read. + * It also move the read marker to the latest known messages if updateReadMarker is set to true + * + * @param updateReadMarker true to move the read marker to the latest known event + * @param aRespCallback the asynchronous callback + * @return true if the request is sent, false otherwise + */ + private boolean markAllAsRead(boolean updateReadMarker, final ApiCallback aRespCallback) { + final Event lastEvent = (null != getStore()) ? getStore().getLatestEvent(getRoomId()) : null; + boolean res = sendReadMarkers(updateReadMarker ? ((null != lastEvent) ? lastEvent.eventId : null) : getReadMarkerEventId(), null, aRespCallback); + + if (!res) { + RoomSummary summary = (null != getStore()) ? getStore().getSummary(getRoomId()) : null; + + if (null != summary) { + if ((0 != summary.getUnreadEventsCount()) + || (0 != summary.getHighlightCount()) + || (0 != summary.getNotificationCount())) { + Log.e(LOG_TAG, "## markAllAsRead() : the summary events counters should be cleared for " + getRoomId()); + + Event latestEvent = getStore().getLatestEvent(getRoomId()); + summary.setLatestReceivedEvent(latestEvent); + + if (null != latestEvent) { + summary.setReadReceiptEventId(latestEvent.eventId); + } else { + summary.setReadReceiptEventId(null); + } + + summary.setUnreadEventsCount(0); + summary.setHighlightCount(0); + summary.setNotificationCount(0); + getStore().flushSummary(summary); + } + } else { + Log.e(LOG_TAG, "## sendReadReceipt() : no summary for " + getRoomId()); + } + + if ((0 != getState().getNotificationCount()) || (0 != getState().getHighlightCount())) { + Log.e(LOG_TAG, "## markAllAsRead() : the notification messages count for " + getRoomId() + " should have been cleared"); + + getState().setNotificationCount(0); + getState().setHighlightCount(0); + + if (null != getStore()) { + getStore().storeLiveStateForRoom(getRoomId()); + } + } + } + + return res; + } + + /** + * Update the read marker event Id + * + * @param readMarkerEventId the read marker even id + */ + public void setReadMakerEventId(final String readMarkerEventId) { + RoomSummary summary = (null != getStore()) ? getStore().getSummary(getRoomId()) : null; + if (summary != null && !readMarkerEventId.equals(summary.getReadMarkerEventId())) { + sendReadMarkers(readMarkerEventId, summary.getReadReceiptEventId(), null); + } + } + + /** + * Send a read receipt to the latest known event + */ + public void sendReadReceipt() { + markAllAsRead(false, null); + } + + /** + * Send the read receipt to the latest room message id. + * + * @param event send a read receipt to a provided event + * @param aRespCallback asynchronous response callback + * @return true if the read receipt has been sent, false otherwise + */ + public boolean sendReadReceipt(Event event, final ApiCallback aRespCallback) { + String eventId = (null != event) ? event.eventId : null; + Log.d(LOG_TAG, "## sendReadReceipt() : eventId " + eventId + " in room " + getRoomId()); + return sendReadMarkers(null, eventId, aRespCallback); + } + + /** + * Forget the current read marker + * This will update the read marker to match the read receipt + * + * @param callback the asynchronous callback + */ + public void forgetReadMarker(final ApiCallback callback) { + final RoomSummary summary = (null != getStore()) ? getStore().getSummary(getRoomId()) : null; + final String currentReadReceipt = (null != summary) ? summary.getReadReceiptEventId() : null; + + if (null != summary) { + Log.d(LOG_TAG, "## forgetReadMarker() : update the read marker to " + currentReadReceipt + " in room " + getRoomId()); + summary.setReadMarkerEventId(currentReadReceipt); + getStore().flushSummary(summary); + } + + setReadMarkers(currentReadReceipt, currentReadReceipt, callback); + } + + /** + * Send the read markers + * + * @param aReadMarkerEventId the new read marker event id (if null use the latest known event id) + * @param aReadReceiptEventId the new read receipt event id (if null use the latest known event id) + * @param aRespCallback asynchronous response callback + * @return true if the request is sent, false otherwise + */ + public boolean sendReadMarkers(final String aReadMarkerEventId, final String aReadReceiptEventId, final ApiCallback aRespCallback) { + final Event lastEvent = (null != getStore()) ? getStore().getLatestEvent(getRoomId()) : null; + + // reported by GA + if (null == lastEvent) { + Log.e(LOG_TAG, "## sendReadMarkers(): no last event"); + return false; + } + + Log.d(LOG_TAG, "## sendReadMarkers(): readMarkerEventId " + aReadMarkerEventId + " readReceiptEventId " + aReadReceiptEventId + + " in room " + getRoomId()); + + boolean hasUpdate = false; + + String readMarkerEventId = aReadMarkerEventId; + if (!TextUtils.isEmpty(aReadMarkerEventId)) { + if (!MXPatterns.isEventId(aReadMarkerEventId)) { + Log.e(LOG_TAG, "## sendReadMarkers() : invalid event id " + readMarkerEventId); + // Read marker is invalid, ignore it + readMarkerEventId = null; + } else { + // Check if the read marker is updated + RoomSummary summary = getStore().getSummary(getRoomId()); + if ((null != summary) && !TextUtils.equals(readMarkerEventId, summary.getReadMarkerEventId())) { + // Make sure the new read marker event is newer than the current one + final Event newReadMarkerEvent = getStore().getEvent(readMarkerEventId, getRoomId()); + final Event currentReadMarkerEvent = getStore().getEvent(summary.getReadMarkerEventId(), getRoomId()); + if (newReadMarkerEvent == null || currentReadMarkerEvent == null + || newReadMarkerEvent.getOriginServerTs() > currentReadMarkerEvent.getOriginServerTs()) { + // Event is not in store (assume it is in the past), or is older than current one + Log.d(LOG_TAG, "## sendReadMarkers(): set new read marker event id " + readMarkerEventId + " in room " + getRoomId()); + summary.setReadMarkerEventId(readMarkerEventId); + getStore().flushSummary(summary); + hasUpdate = true; + } + } + } + } + + final String readReceiptEventId = (null == aReadReceiptEventId) ? lastEvent.eventId : aReadReceiptEventId; + // check if the read receipt event id is already read + if ((null != getStore()) && !getStore().isEventRead(getRoomId(), getDataHandler().getUserId(), readReceiptEventId)) { + // check if the event id update is allowed + if (handleReceiptData(new ReceiptData(mMyUserId, readReceiptEventId, System.currentTimeMillis()))) { + // Clear the unread counters if the latest message is displayed + // We don't try to compute the unread counters for oldest messages : + // ---> it would require too much time. + // The counters are cleared to avoid displaying invalid values + // when the device is offline. + // The read receipts will be sent later + // (asap there is a valid network connection) + if (TextUtils.equals(lastEvent.eventId, readReceiptEventId)) { + clearUnreadCounters(getStore().getSummary(getRoomId())); + } + hasUpdate = true; + } + } + + if (hasUpdate) { + setReadMarkers(readMarkerEventId, readReceiptEventId, aRespCallback); + } + + return hasUpdate; + } + + /** + * Send the request to update the read marker and read receipt. + * + * @param aReadMarkerEventId the read marker event id + * @param aReadReceiptEventId the read receipt event id + * @param callback the asynchronous callback + */ + private void setReadMarkers(final String aReadMarkerEventId, final String aReadReceiptEventId, final ApiCallback callback) { + Log.d(LOG_TAG, "## setReadMarkers(): readMarkerEventId " + aReadMarkerEventId + " readReceiptEventId " + aReadMarkerEventId); + + // check if the message ids are valid + final String readMarkerEventId = MXPatterns.isEventId(aReadMarkerEventId) ? aReadMarkerEventId : null; + final String readReceiptEventId = MXPatterns.isEventId(aReadReceiptEventId) ? aReadReceiptEventId : null; + + // if there is nothing to do + if (TextUtils.isEmpty(readMarkerEventId) && TextUtils.isEmpty(readReceiptEventId)) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (null != callback) { + callback.onSuccess(null); + } + } + }); + } else { + mDataHandler.getDataRetriever().getRoomsRestClient().sendReadMarker(getRoomId(), readMarkerEventId, readReceiptEventId, + new SimpleApiCallback(callback) { + @Override + public void onSuccess(Void info) { + if (null != callback) { + callback.onSuccess(info); + } + } + }); + } + } + + /** + * Check if an event has been read. + * + * @param eventId the event id + * @return true if the message has been read + */ + public boolean isEventRead(String eventId) { + if (null != getStore()) { + return getStore().isEventRead(getRoomId(), mMyUserId, eventId); + } else { + return false; + } + } + + //================================================================================ + // Unread event count management + //================================================================================ + + /** + * @return the number of unread messages that match the push notification rules. + */ + public int getNotificationCount() { + return getState().getNotificationCount(); + } + + /** + * @return the number of highlighted events. + */ + public int getHighlightCount() { + return getState().getHighlightCount(); + } + + /** + * refresh the unread events counts. + */ + public void refreshUnreadCounter() { + // avoid refreshing the unread counter while processing a bunch of messages. + if (!mIsSyncing) { + RoomSummary summary = (null != getStore()) ? getStore().getSummary(getRoomId()) : null; + + if (null != summary) { + int prevValue = summary.getUnreadEventsCount(); + int newValue = getStore().eventsCountAfter(getRoomId(), summary.getReadReceiptEventId()); + + if (prevValue != newValue) { + summary.setUnreadEventsCount(newValue); + getStore().flushSummary(summary); + } + } + } else { + // wait the sync end before computing is again + mRefreshUnreadAfterSync = true; + } + } + + //================================================================================ + // typing events + //================================================================================ + + // userIds list + @NonNull + private final List mTypingUsers = new ArrayList<>(); + + /** + * Get typing users + * + * @return the userIds list + */ + @NonNull + public List getTypingUsers() { + List typingUsers; + + synchronized (mTypingUsers) { + typingUsers = new ArrayList<>(mTypingUsers); + } + + return typingUsers; + } + + /** + * Send a typing notification + * + * @param isTyping typing status + * @param timeout the typing timeout + * @param callback asynchronous callback + */ + public void sendTypingNotification(boolean isTyping, int timeout, ApiCallback callback) { + // send the event only if the user has joined the room. + if (isJoined()) { + mDataHandler.getDataRetriever().getRoomsRestClient().sendTypingNotification(getRoomId(), mMyUserId, isTyping, timeout, callback); + } + } + + //================================================================================ + // Medias events + //================================================================================ + + /** + * Fill the locationInfo + * + * @param context the context + * @param locationMessage the location message + * @param thumbnailUri the thumbnail uri + * @param thumbMimeType the thumbnail mime type + */ + public static void fillLocationInfo(Context context, LocationMessage locationMessage, Uri thumbnailUri, String thumbMimeType) { + if (null != thumbnailUri) { + try { + locationMessage.thumbnail_url = thumbnailUri.toString(); + + ThumbnailInfo thumbInfo = new ThumbnailInfo(); + File thumbnailFile = new File(thumbnailUri.getPath()); + + ExifInterface exifMedia = new ExifInterface(thumbnailUri.getPath()); + String sWidth = exifMedia.getAttribute(ExifInterface.TAG_IMAGE_WIDTH); + String sHeight = exifMedia.getAttribute(ExifInterface.TAG_IMAGE_LENGTH); + + if (null != sWidth) { + thumbInfo.w = Integer.parseInt(sWidth); + } + + if (null != sHeight) { + thumbInfo.h = Integer.parseInt(sHeight); + } + + thumbInfo.size = Long.valueOf(thumbnailFile.length()); + thumbInfo.mimetype = thumbMimeType; + locationMessage.thumbnail_info = thumbInfo; + } catch (Exception e) { + Log.e(LOG_TAG, "fillLocationInfo : failed" + e.getMessage(), e); + } + } + } + + /** + * Fills the VideoMessage info. + * + * @param context Application context for the content resolver. + * @param videoMessage The VideoMessage to fill. + * @param fileUri The file uri. + * @param videoMimeType The mimeType + * @param thumbnailUri the thumbnail uri + * @param thumbMimeType the thumbnail mime type + */ + public static void fillVideoInfo(Context context, VideoMessage videoMessage, Uri fileUri, String videoMimeType, Uri thumbnailUri, String thumbMimeType) { + try { + VideoInfo videoInfo = new VideoInfo(); + File file = new File(fileUri.getPath()); + + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + retriever.setDataSource(file.getAbsolutePath()); + + Bitmap bmp = retriever.getFrameAtTime(); + videoInfo.h = bmp.getHeight(); + videoInfo.w = bmp.getWidth(); + videoInfo.mimetype = videoMimeType; + + try { + MediaPlayer mp = MediaPlayer.create(context, fileUri); + if (null != mp) { + videoInfo.duration = Long.valueOf(mp.getDuration()); + mp.release(); + } + } catch (Exception e) { + Log.e(LOG_TAG, "fillVideoInfo : MediaPlayer.create failed" + e.getMessage(), e); + } + videoInfo.size = file.length(); + + // thumbnail + if (null != thumbnailUri) { + videoInfo.thumbnail_url = thumbnailUri.toString(); + + ThumbnailInfo thumbInfo = new ThumbnailInfo(); + File thumbnailFile = new File(thumbnailUri.getPath()); + + ExifInterface exifMedia = new ExifInterface(thumbnailUri.getPath()); + String sWidth = exifMedia.getAttribute(ExifInterface.TAG_IMAGE_WIDTH); + String sHeight = exifMedia.getAttribute(ExifInterface.TAG_IMAGE_LENGTH); + + if (null != sWidth) { + thumbInfo.w = Integer.parseInt(sWidth); + } + + if (null != sHeight) { + thumbInfo.h = Integer.parseInt(sHeight); + } + + thumbInfo.size = Long.valueOf(thumbnailFile.length()); + thumbInfo.mimetype = thumbMimeType; + videoInfo.thumbnail_info = thumbInfo; + } + + videoMessage.info = videoInfo; + } catch (Exception e) { + Log.e(LOG_TAG, "fillVideoInfo : failed" + e.getMessage(), e); + } + } + + /** + * Fills the fileMessage fileInfo. + * + * @param context Application context for the content resolver. + * @param fileMessage The fileMessage to fill. + * @param fileUri The file uri. + * @param mimeType The mimeType + */ + public static void fillFileInfo(Context context, FileMessage fileMessage, Uri fileUri, String mimeType) { + try { + FileInfo fileInfo = new FileInfo(); + + String filename = fileUri.getPath(); + File file = new File(filename); + + fileInfo.mimetype = mimeType; + fileInfo.size = file.length(); + + fileMessage.info = fileInfo; + + } catch (Exception e) { + Log.e(LOG_TAG, "fillFileInfo : failed" + e.getMessage(), e); + } + } + + + /** + * Update or create an ImageInfo for an image uri. + * + * @param context Application context for the content resolver. + * @param anImageInfo the imageInfo to fill, null to create a new one + * @param imageUri The full size image uri. + * @param mimeType The image mimeType + * @return the filled image info + */ + public static ImageInfo getImageInfo(Context context, ImageInfo anImageInfo, Uri imageUri, String mimeType) { + ImageInfo imageInfo = (null == anImageInfo) ? new ImageInfo() : anImageInfo; + + try { + String filename = imageUri.getPath(); + File file = new File(filename); + + ExifInterface exifMedia = new ExifInterface(filename); + String sWidth = exifMedia.getAttribute(ExifInterface.TAG_IMAGE_WIDTH); + String sHeight = exifMedia.getAttribute(ExifInterface.TAG_IMAGE_LENGTH); + + // the image rotation is replaced by orientation + // imageInfo.rotation = ImageUtils.getRotationAngleForBitmap(context, imageUri); + imageInfo.orientation = ImageUtils.getOrientationForBitmap(context, imageUri); + + int width = 0; + int height = 0; + + // extract the Exif info + if ((null != sWidth) && (null != sHeight)) { + + if ((imageInfo.orientation == ExifInterface.ORIENTATION_TRANSPOSE) + || (imageInfo.orientation == ExifInterface.ORIENTATION_ROTATE_90) + || (imageInfo.orientation == ExifInterface.ORIENTATION_TRANSVERSE) + || (imageInfo.orientation == ExifInterface.ORIENTATION_ROTATE_270)) { + height = Integer.parseInt(sWidth); + width = Integer.parseInt(sHeight); + } else { + width = Integer.parseInt(sWidth); + height = Integer.parseInt(sHeight); + } + } + + // there is no exif info or the size is invalid + if ((0 == width) || (0 == height)) { + try { + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inJustDecodeBounds = true; + BitmapFactory.decodeFile(imageUri.getPath(), opts); + + // don't need to load the bitmap in memory + if ((opts.outHeight > 0) && (opts.outWidth > 0)) { + width = opts.outWidth; + height = opts.outHeight; + } + + } catch (Exception e) { + Log.e(LOG_TAG, "fillImageInfo : failed" + e.getMessage(), e); + } catch (OutOfMemoryError oom) { + Log.e(LOG_TAG, "fillImageInfo : oom", oom); + } + } + + // valid image size ? + if ((0 != width) || (0 != height)) { + imageInfo.w = width; + imageInfo.h = height; + } + + imageInfo.mimetype = mimeType; + imageInfo.size = file.length(); + } catch (Exception e) { + Log.e(LOG_TAG, "fillImageInfo : failed" + e.getMessage(), e); + imageInfo = null; + } + + return imageInfo; + } + + /** + * Fills the imageMessage imageInfo. + * + * @param context Application context for the content resolver. + * @param imageMessage The imageMessage to fill. + * @param imageUri The full size image uri. + * @param mimeType The image mimeType + */ + public static void fillImageInfo(Context context, ImageMessage imageMessage, Uri imageUri, String mimeType) { + imageMessage.info = getImageInfo(context, imageMessage.info, imageUri, mimeType); + } + + /** + * Fills the imageMessage imageInfo. + * + * @param context Application context for the content resolver. + * @param imageMessage The imageMessage to fill. + * @param thumbUri The thumbnail uri + * @param mimeType The image mimeType + */ + public static void fillThumbnailInfo(Context context, ImageMessage imageMessage, Uri thumbUri, String mimeType) { + ImageInfo imageInfo = getImageInfo(context, null, thumbUri, mimeType); + + if (null != imageInfo) { + if (null == imageMessage.info) { + imageMessage.info = new ImageInfo(); + } + + imageMessage.info.thumbnailInfo = new ThumbnailInfo(); + imageMessage.info.thumbnailInfo.w = imageInfo.w; + imageMessage.info.thumbnailInfo.h = imageInfo.h; + imageMessage.info.thumbnailInfo.size = imageInfo.size; + imageMessage.info.thumbnailInfo.mimetype = imageInfo.mimetype; + } + } + + //================================================================================ + // Call + //================================================================================ + + /** + * Test if a call can be performed in this room. + * + * @return true if a call can be performed. + */ + public boolean canPerformCall() { + return getNumberOfMembers() > 1; + } + + /** + * @return a list of callable members. + */ + public void callees(final ApiCallback> callback) { + getMembersAsync(new SimpleApiCallback>(callback) { + @Override + public void onSuccess(List info) { + List res = new ArrayList<>(); + + for (RoomMember m : info) { + if (RoomMember.MEMBERSHIP_JOIN.equals(m.membership) && !mMyUserId.equals(m.getUserId())) { + res.add(m); + } + } + + callback.onSuccess(res); + } + }); + } + + //================================================================================ + // Account data management + //================================================================================ + + /** + * Handle private user data events. + * + * @param accountDataEvents the account events. + */ + private void handleAccountDataEvents(List accountDataEvents) { + if ((null != accountDataEvents) && (accountDataEvents.size() > 0)) { + // manage the account events + for (Event accountDataEvent : accountDataEvents) { + String eventType = accountDataEvent.getType(); + + final RoomSummary summary = (null != getStore()) ? getStore().getSummary(getRoomId()) : null; + if (eventType.equals(Event.EVENT_TYPE_READ_MARKER)) { + if (summary != null) { + final Event event = JsonUtils.toEvent(accountDataEvent.getContent()); + if (null != event && !TextUtils.equals(event.eventId, summary.getReadMarkerEventId())) { + Log.d(LOG_TAG, "## handleAccountDataEvents() : update the read marker to " + event.eventId + " in room " + getRoomId()); + if (TextUtils.isEmpty(event.eventId)) { + Log.e(LOG_TAG, "## handleAccountDataEvents() : null event id " + accountDataEvent.getContent()); + } + summary.setReadMarkerEventId(event.eventId); + getStore().flushSummary(summary); + mDataHandler.onReadMarkerEvent(getRoomId()); + } + } + } else { + mAccountData.handleTagEvent(accountDataEvent); + if (Event.EVENT_TYPE_TAGS.equals(accountDataEvent.getType())) { + summary.setRoomTags(mAccountData.getKeys()); + getStore().flushSummary(summary); + mDataHandler.onRoomTagEvent(getRoomId()); + } else if (Event.EVENT_TYPE_URL_PREVIEW.equals(accountDataEvent.getType())) { + final JsonObject jsonObject = accountDataEvent.getContentAsJsonObject(); + if (jsonObject.has(AccountDataRestClient.ACCOUNT_DATA_KEY_URL_PREVIEW_DISABLE)) { + final boolean disabled = jsonObject.get(AccountDataRestClient.ACCOUNT_DATA_KEY_URL_PREVIEW_DISABLE).getAsBoolean(); + Set roomIdsWithoutURLPreview = mDataHandler.getStore().getRoomsWithoutURLPreviews(); + if (disabled) { + roomIdsWithoutURLPreview.add(getRoomId()); + } else { + roomIdsWithoutURLPreview.remove(getRoomId()); + } + + mDataHandler.getStore().setRoomsWithoutURLPreview(roomIdsWithoutURLPreview); + } + } + } + } + + if (null != getStore()) { + getStore().storeAccountData(getRoomId(), mAccountData); + } + } + } + + /** + * Add a tag to a room. + * Use this method to update the order of an existing tag. + * + * @param tag the new tag to add to the room. + * @param order the order. + * @param callback the operation callback + */ + private void addTag(String tag, Double order, final ApiCallback callback) { + // sanity check + if ((null != tag) && (null != order)) { + mDataHandler.getDataRetriever().getRoomsRestClient().addTag(getRoomId(), tag, order, callback); + } else { + if (null != callback) { + callback.onSuccess(null); + } + } + } + + /** + * Remove a tag to a room. + * + * @param tag the new tag to add to the room. + * @param callback the operation callback. + */ + private void removeTag(String tag, final ApiCallback callback) { + // sanity check + if (null != tag) { + mDataHandler.getDataRetriever().getRoomsRestClient().removeTag(getRoomId(), tag, callback); + } else { + if (null != callback) { + callback.onSuccess(null); + } + } + } + + /** + * Remove a tag and add another one. + * + * @param oldTag the tag to remove. + * @param newTag the new tag to add. Nil can be used. Then, no new tag will be added. + * @param newTagOrder the order of the new tag. + * @param callback the operation callback. + */ + public void replaceTag(final String oldTag, final String newTag, final Double newTagOrder, final ApiCallback callback) { + // remove tag + if ((null != oldTag) && (null == newTag)) { + removeTag(oldTag, callback); + } + // define a tag or define a new order + else if (((null == oldTag) && (null != newTag)) || TextUtils.equals(oldTag, newTag)) { + addTag(newTag, newTagOrder, callback); + } else { + removeTag(oldTag, new SimpleApiCallback(callback) { + @Override + public void onSuccess(Void info) { + addTag(newTag, newTagOrder, callback); + } + }); + } + } + + //============================================================================================================== + // URL preview + //============================================================================================================== + + /** + * Tells if the URL preview has been allowed by the user. + * + * @return @return true if allowed. + */ + public boolean isURLPreviewAllowedByUser() { + return !getDataHandler().getStore().getRoomsWithoutURLPreviews().contains(getRoomId()); + } + + /** + * Update the user enabled room url preview + * + * @param status the new status + * @param callback the asynchronous callback + */ + public void setIsURLPreviewAllowedByUser(boolean status, ApiCallback callback) { + mDataHandler.getDataRetriever().getRoomsRestClient().updateURLPreviewStatus(getRoomId(), status, callback); + } + + //============================================================================================================== + // Room events dispatcher + //============================================================================================================== + + /** + * Add an event listener to this room. Only events relative to the room will come down. + * + * @param eventListener the event listener to add + */ + public void addEventListener(final IMXEventListener eventListener) { + // sanity check + if (null == eventListener) { + Log.e(LOG_TAG, "addEventListener : eventListener is null"); + return; + } + + // GA crash : should never happen but got it. + if (null == mDataHandler) { + Log.e(LOG_TAG, "addEventListener : mDataHandler is null"); + return; + } + + // Create a global listener that we'll add to the data handler + IMXEventListener globalListener = new MXRoomEventListener(this, eventListener); + + mEventListeners.put(eventListener, globalListener); + + // GA crash + if (null != mDataHandler) { + mDataHandler.addListener(globalListener); + } + } + + /** + * Remove an event listener. + * + * @param eventListener the event listener to remove + */ + public void removeEventListener(IMXEventListener eventListener) { + // sanity check + if ((null != eventListener) && (null != mDataHandler)) { + mDataHandler.removeListener(mEventListeners.get(eventListener)); + mEventListeners.remove(eventListener); + } + } + + //============================================================================================================== + // Send methods + //============================================================================================================== + + /** + * Send an event content to the room. + * The event is updated with the data provided by the server + * The provided event contains the error description. + * + * @param event the message + * @param callback the callback with the created event + */ + public void sendEvent(final Event event, final ApiCallback callback) { + // wait that the room is synced before sending messages + if (!mIsReady || !isJoined()) { + mDataHandler.updateEventState(event, Event.SentState.WAITING_RETRY); + try { + callback.onNetworkError(null); + } catch (Exception e) { + Log.e(LOG_TAG, "sendEvent exception " + e.getMessage(), e); + } + return; + } + + final String prevEventId = event.eventId; + + final ApiCallback localCB = new ApiCallback() { + @Override + public void onSuccess(final CreatedEvent createdEvent) { + if (null != getStore()) { + // remove the tmp event + getStore().deleteEvent(event); + } + + // replace the tmp event id by the final one + boolean isReadMarkerUpdated = TextUtils.equals(getReadMarkerEventId(), event.eventId); + + // update the event with the server response + event.eventId = createdEvent.eventId; + event.originServerTs = System.currentTimeMillis(); + mDataHandler.updateEventState(event, Event.SentState.SENT); + + // the message echo is not yet echoed + if (null != getStore() && !getStore().doesEventExist(createdEvent.eventId, getRoomId())) { + getStore().storeLiveRoomEvent(event); + } + + // send the dedicated read receipt asap + markAllAsRead(isReadMarkerUpdated, null); + + if (null != getStore()) { + getStore().commit(); + } + mDataHandler.onEventSent(event, prevEventId); + + try { + callback.onSuccess(null); + } catch (Exception e) { + Log.e(LOG_TAG, "sendEvent exception " + e.getMessage(), e); + } + } + + @Override + public void onNetworkError(Exception e) { + event.unsentException = e; + mDataHandler.updateEventState(event, Event.SentState.UNDELIVERED); + try { + callback.onNetworkError(e); + } catch (Exception anException) { + Log.e(LOG_TAG, "sendEvent exception " + anException.getMessage(), anException); + } + } + + @Override + public void onMatrixError(MatrixError e) { + event.unsentMatrixError = e; + mDataHandler.updateEventState(event, Event.SentState.UNDELIVERED); + + if (MatrixError.isConfigurationErrorCode(e.errcode)) { + mDataHandler.onConfigurationError(e.errcode); + } else { + try { + callback.onMatrixError(e); + } catch (Exception anException) { + Log.e(LOG_TAG, "sendEvent exception " + anException.getMessage(), anException); + } + } + } + + @Override + public void onUnexpectedError(Exception e) { + event.unsentException = e; + mDataHandler.updateEventState(event, Event.SentState.UNDELIVERED); + try { + callback.onUnexpectedError(e); + } catch (Exception anException) { + Log.e(LOG_TAG, "sendEvent exception " + anException.getMessage(), anException); + } + } + }; + + if (isEncrypted() && (null != mDataHandler.getCrypto())) { + mDataHandler.updateEventState(event, Event.SentState.ENCRYPTING); + + // Store the "m.relates_to" data and remove them from event content before encrypting the event content + final JsonElement relatesTo; + + JsonObject contentAsJsonObject = event.getContentAsJsonObject(); + + if (contentAsJsonObject != null + && contentAsJsonObject.has("m.relates_to")) { + // Get a copy of "m.relates_to" data... + relatesTo = contentAsJsonObject.get("m.relates_to"); + + // ... and remove "m.relates_to" data from the content before encrypting it + contentAsJsonObject.remove("m.relates_to"); + } else { + relatesTo = null; + } + + // Encrypt the content before sending + mDataHandler.getCrypto() + .encryptEventContent(contentAsJsonObject, event.getType(), this, new ApiCallback() { + @Override + public void onSuccess(MXEncryptEventContentResult encryptEventContentResult) { + // update the event content with the encrypted data + event.type = encryptEventContentResult.mEventType; + + // Add the "m.relates_to" data to the encrypted event here + JsonObject encryptedContent = encryptEventContentResult.mEventContent.getAsJsonObject(); + if (relatesTo != null) { + encryptedContent.add("m.relates_to", relatesTo); + } + event.updateContent(encryptedContent); + mDataHandler.decryptEvent(event, null); + + // sending in progress + mDataHandler.updateEventState(event, Event.SentState.SENDING); + mDataHandler.getDataRetriever().getRoomsRestClient().sendEventToRoom(event.eventId, getRoomId(), + encryptEventContentResult.mEventType, encryptEventContentResult.mEventContent.getAsJsonObject(), localCB); + } + + @Override + public void onNetworkError(Exception e) { + event.unsentException = e; + mDataHandler.updateEventState(event, Event.SentState.UNDELIVERED); + + if (null != callback) { + callback.onNetworkError(e); + } + } + + @Override + public void onMatrixError(MatrixError e) { + // update the sent state if the message encryption failed because there are unknown devices. + if ((e instanceof MXCryptoError) && TextUtils.equals(((MXCryptoError) e).errcode, MXCryptoError.UNKNOWN_DEVICES_CODE)) { + event.mSentState = Event.SentState.FAILED_UNKNOWN_DEVICES; + } else { + event.mSentState = Event.SentState.UNDELIVERED; + } + event.unsentMatrixError = e; + mDataHandler.onEventSentStateUpdated(event); + + if (null != callback) { + callback.onMatrixError(e); + } + } + + @Override + public void onUnexpectedError(Exception e) { + event.unsentException = e; + mDataHandler.updateEventState(event, Event.SentState.UNDELIVERED); + + if (null != callback) { + callback.onUnexpectedError(e); + } + } + }); + } else { + mDataHandler.updateEventState(event, Event.SentState.SENDING); + + if (Event.EVENT_TYPE_MESSAGE.equals(event.getType())) { + mDataHandler.getDataRetriever().getRoomsRestClient() + .sendMessage(event.eventId, getRoomId(), JsonUtils.toMessage(event.getContent()), localCB); + } else { + mDataHandler.getDataRetriever().getRoomsRestClient() + .sendEventToRoom(event.eventId, getRoomId(), event.getType(), event.getContentAsJsonObject(), localCB); + } + } + } + + /** + * Cancel the event sending. + * Any media upload will be cancelled too. + * The event becomes undeliverable. + * + * @param event the message + */ + public void cancelEventSending(final Event event) { + if (null != event) { + if ((Event.SentState.UNSENT == event.mSentState) + || (Event.SentState.SENDING == event.mSentState) + || (Event.SentState.WAITING_RETRY == event.mSentState) + || (Event.SentState.ENCRYPTING == event.mSentState)) { + + // the message cannot be sent anymore + mDataHandler.updateEventState(event, Event.SentState.UNDELIVERED); + } + + List urls = event.getMediaUrls(); + MXMediasCache cache = mDataHandler.getMediasCache(); + + for (String url : urls) { + cache.cancelUpload(url); + cache.cancelDownload(cache.downloadIdFromUrl(url)); + } + } + } + + /** + * Redact an event from the room. + * + * @param eventId the event's id + * @param callback the callback with the redacted event + */ + public void redact(final String eventId, final ApiCallback callback) { + mDataHandler.getDataRetriever().getRoomsRestClient().redactEvent(getRoomId(), eventId, new SimpleApiCallback(callback) { + @Override + public void onSuccess(Event event) { + Event redactedEvent = (null != getStore()) ? getStore().getEvent(eventId, getRoomId()) : null; + + // test if the redacted event has been echoed + // it it was not echoed, the event must be pruned to remove useless data + // the room summary will be updated when the server will echo the redacted event + if ((null != redactedEvent) && ((null == redactedEvent.unsigned) || (null == redactedEvent.unsigned.redacted_because))) { + redactedEvent.prune(null); + getStore().storeLiveRoomEvent(redactedEvent); + getStore().commit(); + } + + if (null != callback) { + callback.onSuccess(redactedEvent); + } + } + }); + } + + /** + * Redact an event from the room. + * + * @param eventId the event's id + * @param score the score + * @param reason the redaction reason + * @param callback the callback with the created event + */ + public void report(String eventId, int score, String reason, ApiCallback callback) { + mDataHandler.getDataRetriever().getRoomsRestClient().reportEvent(getRoomId(), eventId, score, reason, callback); + } + + //================================================================================ + // Member actions + //================================================================================ + + /** + * Invite an user to this room. + * + * @param userId the user id + * @param callback the callback for when done + */ + public void invite(String userId, ApiCallback callback) { + if (null != userId) { + invite(Collections.singletonList(userId), callback); + } + } + + /** + * Invite an user to a room based on their email address to this room. + * + * @param email the email address + * @param callback the callback for when done + */ + public void inviteByEmail(String email, ApiCallback callback) { + if (null != email) { + invite(Collections.singletonList(email), callback); + } + } + + /** + * Invite users to this room. + * The identifiers are either ini Id or email address. + * + * @param identifiers the identifiers list + * @param callback the callback for when done + */ + public void invite(List identifiers, ApiCallback callback) { + if (null != identifiers) { + invite(identifiers.iterator(), callback); + } + } + + /** + * Invite some users to this room. + * + * @param identifiers the identifiers iterator + * @param callback the callback for when done + */ + private void invite(final Iterator identifiers, final ApiCallback callback) { + if (!identifiers.hasNext()) { + callback.onSuccess(null); + return; + } + + final ApiCallback localCallback = new SimpleApiCallback(callback) { + @Override + public void onSuccess(Void info) { + invite(identifiers, callback); + } + }; + + String identifier = identifiers.next(); + + if (android.util.Patterns.EMAIL_ADDRESS.matcher(identifier).matches()) { + mDataHandler.getDataRetriever().getRoomsRestClient().inviteByEmailToRoom(getRoomId(), identifier, localCallback); + } else { + mDataHandler.getDataRetriever().getRoomsRestClient().inviteUserToRoom(getRoomId(), identifier, localCallback); + } + } + + /** + * Leave the room. + * + * @param callback the callback for when done + */ + public void leave(final ApiCallback callback) { + mIsLeaving = true; + mDataHandler.onRoomInternalUpdate(getRoomId()); + + mDataHandler.getDataRetriever().getRoomsRestClient().leaveRoom(getRoomId(), new ApiCallback() { + @Override + public void onSuccess(Void info) { + if (mDataHandler.isAlive()) { + mIsLeaving = false; + + // delete references to the room + mDataHandler.deleteRoom(getRoomId()); + + if (null != getStore()) { + Log.d(LOG_TAG, "leave : commit"); + getStore().commit(); + } + + try { + callback.onSuccess(info); + } catch (Exception e) { + Log.e(LOG_TAG, "leave exception " + e.getMessage(), e); + } + + mDataHandler.onLeaveRoom(getRoomId()); + } + } + + @Override + public void onNetworkError(Exception e) { + mIsLeaving = false; + + try { + callback.onNetworkError(e); + } catch (Exception anException) { + Log.e(LOG_TAG, "leave exception " + anException.getMessage(), anException); + } + + mDataHandler.onRoomInternalUpdate(getRoomId()); + } + + @Override + public void onMatrixError(MatrixError e) { + // the room was not anymore defined server side + // race condition ? + if (e.mStatus == 404) { + onSuccess(null); + } else { + mIsLeaving = false; + + try { + callback.onMatrixError(e); + } catch (Exception anException) { + Log.e(LOG_TAG, "leave exception " + anException.getMessage(), anException); + } + + mDataHandler.onRoomInternalUpdate(getRoomId()); + } + } + + @Override + public void onUnexpectedError(Exception e) { + mIsLeaving = false; + + try { + callback.onUnexpectedError(e); + } catch (Exception anException) { + Log.e(LOG_TAG, "leave exception " + anException.getMessage(), anException); + } + + mDataHandler.onRoomInternalUpdate(getRoomId()); + } + }); + } + + /** + * Forget the room. + * + * @param callback the callback for when done + */ + public void forget(final ApiCallback callback) { + mDataHandler.getDataRetriever().getRoomsRestClient().forgetRoom(getRoomId(), new SimpleApiCallback(callback) { + @Override + public void onSuccess(Void info) { + if (mDataHandler.isAlive()) { + // don't call onSuccess.deleteRoom because it moves an existing room to historical store + IMXStore store = mDataHandler.getStore(getRoomId()); + + if (null != store) { + store.deleteRoom(getRoomId()); + store.commit(); + } + + try { + callback.onSuccess(info); + } catch (Exception e) { + Log.e(LOG_TAG, "forget exception " + e.getMessage(), e); + } + } + } + }); + } + + + /** + * Kick a user from the room. + * + * @param userId the user id + * @param callback the async callback + */ + public void kick(String userId, ApiCallback callback) { + mDataHandler.getDataRetriever().getRoomsRestClient().kickFromRoom(getRoomId(), userId, callback); + } + + /** + * Ban a user from the room. + * + * @param userId the user id + * @param reason ban reason + * @param callback the async callback + */ + public void ban(String userId, String reason, ApiCallback callback) { + BannedUser user = new BannedUser(); + user.userId = userId; + if (!TextUtils.isEmpty(reason)) { + user.reason = reason; + } + mDataHandler.getDataRetriever().getRoomsRestClient().banFromRoom(getRoomId(), user, callback); + } + + /** + * Unban a user. + * + * @param userId the user id + * @param callback the async callback + */ + public void unban(String userId, ApiCallback callback) { + BannedUser user = new BannedUser(); + user.userId = userId; + + mDataHandler.getDataRetriever().getRoomsRestClient().unbanFromRoom(getRoomId(), user, callback); + } + + //================================================================================ + // Encryption + //================================================================================ + + private ApiCallback mRoomEncryptionCallback; + + private final MXEventListener mEncryptionListener = new MXEventListener() { + @Override + public void onLiveEvent(Event event, RoomState roomState) { + if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_MESSAGE_ENCRYPTION)) { + if (null != mRoomEncryptionCallback) { + mRoomEncryptionCallback.onSuccess(null); + mRoomEncryptionCallback = null; + } + } + } + }; + + /** + * @return if the room content is encrypted + */ + public boolean isEncrypted() { + return getState().isEncrypted(); + } + + /** + * Enable the encryption. + * + * @param algorithm the used algorithm + * @param callback the asynchronous callback + */ + public void enableEncryptionWithAlgorithm(final String algorithm, final ApiCallback callback) { + // ensure that the crypto has been update + if (null != mDataHandler.getCrypto() && !TextUtils.isEmpty(algorithm)) { + Map params = new HashMap<>(); + params.put("algorithm", algorithm); + + if (null != callback) { + mRoomEncryptionCallback = callback; + addEventListener(mEncryptionListener); + } + + mDataHandler.getDataRetriever().getRoomsRestClient() + .sendStateEvent(getRoomId(), Event.EVENT_TYPE_MESSAGE_ENCRYPTION, null, params, new ApiCallback() { + @Override + public void onSuccess(Void info) { + // Wait for the event coming back from the hs + } + + @Override + public void onNetworkError(Exception e) { + if (null != callback) { + callback.onNetworkError(e); + removeEventListener(mEncryptionListener); + } + } + + @Override + public void onMatrixError(MatrixError e) { + if (null != callback) { + callback.onMatrixError(e); + removeEventListener(mEncryptionListener); + } + } + + @Override + public void onUnexpectedError(Exception e) { + if (null != callback) { + callback.onUnexpectedError(e); + removeEventListener(mEncryptionListener); + } + } + }); + } else if (null != callback) { + if (null == mDataHandler.getCrypto()) { + callback.onMatrixError(new MXCryptoError(MXCryptoError.ENCRYPTING_NOT_ENABLED_ERROR_CODE, + MXCryptoError.ENCRYPTING_NOT_ENABLED_REASON, MXCryptoError.ENCRYPTING_NOT_ENABLED_REASON)); + } else { + callback.onMatrixError(new MXCryptoError(MXCryptoError.MISSING_FIELDS_ERROR_CODE, + MXCryptoError.UNABLE_TO_ENCRYPT, MXCryptoError.MISSING_FIELDS_REASON)); + } + } + } + + //============================================================================================================== + // Room events helper + //============================================================================================================== + + private RoomMediaMessagesSender mRoomMediaMessagesSender; + + /** + * Init the mRoomMediaMessagesSender instance + */ + private void initRoomMediaMessagesSender() { + if (null == mRoomMediaMessagesSender) { + mRoomMediaMessagesSender = new RoomMediaMessagesSender(getStore().getContext(), mDataHandler, this); + } + } + + /** + * Send a text message asynchronously. + * + * @param text the unformatted text + * @param htmlFormattedText the HTML formatted text + * @param format the formatted text format + * @param listener the event creation listener + */ + public void sendTextMessage(String text, + String htmlFormattedText, + String format, + RoomMediaMessage.EventCreationListener listener) { + sendTextMessage(text, htmlFormattedText, format, null, Message.MSGTYPE_TEXT, listener); + } + + /** + * Send a text message asynchronously. + * + * @param text the unformatted text + * @param htmlFormattedText the HTML formatted text + * @param format the formatted text format + * @param replyToEvent the event to reply to, or null + * @param listener the event creation listener + */ + public void sendTextMessage(String text, + String htmlFormattedText, + String format, + @Nullable Event replyToEvent, + RoomMediaMessage.EventCreationListener listener) { + sendTextMessage(text, htmlFormattedText, format, replyToEvent, Message.MSGTYPE_TEXT, listener); + } + + /** + * Send an emote message asynchronously. + * + * @param text the unformatted text + * @param htmlFormattedText the HTML formatted text + * @param format the formatted text format + * @param listener the event creation listener + */ + public void sendEmoteMessage(String text, + String htmlFormattedText, + String format, + final RoomMediaMessage.EventCreationListener listener) { + sendTextMessage(text, htmlFormattedText, format, null, Message.MSGTYPE_EMOTE, listener); + } + + /** + * Send a text message asynchronously. + * + * @param text the unformatted text + * @param htmlFormattedText the HTML formatted text + * @param format the formatted text format + * @param replyToEvent the event to reply to (optional). Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment. + * @param msgType the message type + * @param listener the event creation listener + */ + private void sendTextMessage(String text, + String htmlFormattedText, + String format, + @Nullable Event replyToEvent, + String msgType, + final RoomMediaMessage.EventCreationListener listener) { + initRoomMediaMessagesSender(); + + RoomMediaMessage roomMediaMessage = new RoomMediaMessage(text, htmlFormattedText, format); + roomMediaMessage.setMessageType(msgType); + roomMediaMessage.setEventCreationListener(listener); + + if (canReplyTo(replyToEvent)) { + roomMediaMessage.setReplyToEvent(replyToEvent); + } + + mRoomMediaMessagesSender.send(roomMediaMessage); + } + + /** + * Indicate if replying to the provided event is supported. + * Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment, and for certain msgtype. + * + * @param replyToEvent the event to reply to + * @return true if it is possible to reply to this event + */ + public boolean canReplyTo(@Nullable Event replyToEvent) { + if (replyToEvent != null + && Event.EVENT_TYPE_MESSAGE.equals(replyToEvent.getType())) { + + // Cf. https://docs.google.com/document/d/1BPd4lBrooZrWe_3s_lHw_e-Dydvc7bXbm02_sV2k6Sc + String msgType = JsonUtils.getMessageMsgType(replyToEvent.getContentAsJsonObject()); + + if (msgType != null) { + switch (msgType) { + case Message.MSGTYPE_TEXT: + case Message.MSGTYPE_NOTICE: + case Message.MSGTYPE_EMOTE: + case Message.MSGTYPE_IMAGE: + case Message.MSGTYPE_VIDEO: + case Message.MSGTYPE_AUDIO: + case Message.MSGTYPE_FILE: + return true; + } + } + } + + return false; + } + + /** + * Send an media message asynchronously. + * + * @param roomMediaMessage the media message to send. + * @param maxThumbnailWidth the max thumbnail width + * @param maxThumbnailHeight the max thumbnail height + * @param listener the event creation listener + */ + public void sendMediaMessage(final RoomMediaMessage roomMediaMessage, + final int maxThumbnailWidth, + final int maxThumbnailHeight, + final RoomMediaMessage.EventCreationListener listener) { + initRoomMediaMessagesSender(); + + roomMediaMessage.setThumbnailSize(new Pair<>(maxThumbnailWidth, maxThumbnailHeight)); + roomMediaMessage.setEventCreationListener(listener); + + mRoomMediaMessagesSender.send(roomMediaMessage); + } + + /** + * Send a sticker message. + * + * @param event + * @param listener + */ + public void sendStickerMessage(Event event, final RoomMediaMessage.EventCreationListener listener) { + initRoomMediaMessagesSender(); + + RoomMediaMessage roomMediaMessage = new RoomMediaMessage(event); + roomMediaMessage.setMessageType(Event.EVENT_TYPE_STICKER); + roomMediaMessage.setEventCreationListener(listener); + + mRoomMediaMessagesSender.send(roomMediaMessage); + } + + //============================================================================================================== + // Unsent events management + //============================================================================================================== + + /** + * Provides the unsent messages list. + * + * @return the unsent events list + */ + public List getUnsentEvents() { + List unsent = new ArrayList<>(); + + if (null != getStore()) { + List undeliverableEvents = getStore().getUndeliveredEvents(getRoomId()); + List unknownDeviceEvents = getStore().getUnknownDeviceEvents(getRoomId()); + + if (null != undeliverableEvents) { + unsent.addAll(undeliverableEvents); + } + + if (null != unknownDeviceEvents) { + unsent.addAll(unknownDeviceEvents); + } + } + + return unsent; + } + + /** + * Delete an events list. + * + * @param events the events list + */ + public void deleteEvents(List events) { + if ((null != getStore()) && (null != events) && events.size() > 0) { + // reset the timestamp + for (Event event : events) { + getStore().deleteEvent(event); + } + + // update the summary + Event latestEvent = getStore().getLatestEvent(getRoomId()); + + // if there is an oldest event, use it to set a summary + if (latestEvent != null) { + if (RoomSummary.isSupportedEvent(latestEvent)) { + RoomSummary summary = getStore().getSummary(getRoomId()); + + if (null != summary) { + summary.setLatestReceivedEvent(latestEvent, getState()); + } else { + summary = new RoomSummary(null, latestEvent, getState(), mDataHandler.getUserId()); + } + + getStore().storeSummary(summary); + } + } + + getStore().commit(); + } + } + + /** + * Tell if room is Direct Chat + * + * @return true if is direct chat + */ + public boolean isDirect() { + return mDataHandler.getDirectChatRoomIdsList().contains(getRoomId()); + } + + @Nullable + public RoomSummary getRoomSummary() { + if (getDataHandler() == null) { + return null; + } + + if (getDataHandler().getStore() == null) { + return null; + } + + return getDataHandler().getStore().getSummary(getRoomId()); + } + + public int getNumberOfMembers() { + if (getDataHandler().isLazyLoadingEnabled()) { + return getNumberOfJoinedMembers() + getNumberOfInvitedMembers(); + } else { + return getState().getLoadedMembers().size(); + } + } + + public int getNumberOfJoinedMembers() { + if (getDataHandler().isLazyLoadingEnabled()) { + RoomSummary roomSummary = getRoomSummary(); + + if (roomSummary != null) { + return roomSummary.getNumberOfJoinedMembers(); + } else { + // Should not happen, fallback to loaded members + return getNumberOfLoadedJoinedMembers(); + } + } else { + return getNumberOfLoadedJoinedMembers(); + } + } + + private int getNumberOfLoadedJoinedMembers() { + int count = 0; + + for (RoomMember roomMember : getState().getLoadedMembers()) { + if (RoomMember.MEMBERSHIP_JOIN.equals(roomMember.membership)) { + count++; + } + } + + return count; + } + + public int getNumberOfInvitedMembers() { + if (getDataHandler().isLazyLoadingEnabled()) { + RoomSummary roomSummary = getRoomSummary(); + + if (roomSummary != null) { + return roomSummary.getNumberOfInvitedMembers(); + } else { + // Should not happen, fallback to loaded members + return getNumberOfLoadedInvitedMembers(); + } + } else { + return getNumberOfLoadedInvitedMembers(); + } + } + + private int getNumberOfLoadedInvitedMembers() { + int count = 0; + + for (RoomMember roomMember : getState().getLoadedMembers()) { + if (RoomMember.MEMBERSHIP_INVITE.equals(roomMember.membership)) { + count++; + } + } + + return count; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomAccountData.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomAccountData.java new file mode 100644 index 0000000000..8a0d1e52c4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomAccountData.java @@ -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 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 getKeys() { + if (hasTags()) { + return tags.keySet(); + } else { + return null; + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomEmailInvitation.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomEmailInvitation.java new file mode 100644 index 0000000000..bfeb6c3b8e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomEmailInvitation.java @@ -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 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"); + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomMediaMessage.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomMediaMessage.java new file mode 100755 index 0000000000..db07f4eb5e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomMediaMessage.java @@ -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 mThumbnailSize = new Pair<>(100, 100); + + // upload media upload listener + private transient IMXMediaUploadListener mMediaUploadListener; + + // event sending callback + private transient ApiCallback 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 size) { + mThumbnailSize = size; + } + + /** + * @return the thumbnail size. + */ + public Pair 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 callback) { + mEventSendingCallback = callback; + } + + /** + * @return the event sending callback. + */ + public ApiCallback 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 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 listRoomMediaMessages(Intent intent, ClassLoader loader) { + List 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 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 streams = (List) 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; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomMediaMessagesSender.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomMediaMessagesSender.java new file mode 100755 index 0000000000..0e114c5f99 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomMediaMessagesSender.java @@ -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 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() { + private ApiCallback getCallback() { + ApiCallback callback; + + synchronized (LOG_TAG) { + callback = mSendingRoomMediaMessage.getSendingCallback(); + mSendingRoomMediaMessage.setEventSendingCallback(null); + mSendingRoomMediaMessage = null; + } + + return callback; + } + + @Override + public void onSuccess(Void info) { + ApiCallback 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 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 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 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("^.*", ""); + } + + StringBuilder ret = new StringBuilder("
") + // "In reply to" + .append(mContext.getString(R.string.message_reply_to_prefix)) + .append(" "); + + if (isEmote) { + ret.append("* "); + } + + ret.append("") + // ${mxid} + .append(replyToEvent.sender) + .append("
") + .append(replyToFormattedBody) + .append("
") + .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 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 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; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomPreviewData.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomPreviewData.java new file mode 100644 index 0000000000..8c28e94b8c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomPreviewData.java @@ -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 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 apiCallback) { + mSession.getRoomsApiClient().initialSync(mRoomId, new ApiCallback() { + @Override + public void onSuccess(final RoomResponse roomResponse) { + AsyncTask task = new AsyncTask() { + @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; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomState.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomState.java new file mode 100644 index 0000000000..26eef40c47 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomState.java @@ -0,0 +1,1334 @@ +/* + * 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 android.text.TextUtils; + +import com.google.gson.JsonObject; + +import im.vector.matrix.android.internal.legacy.MXDataHandler; +import im.vector.matrix.android.internal.legacy.call.MXCallsManager; +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.model.Event; +import im.vector.matrix.android.internal.legacy.rest.model.PowerLevels; +import im.vector.matrix.android.internal.legacy.rest.model.RoomCreateContent; +import im.vector.matrix.android.internal.legacy.rest.model.RoomDirectoryVisibility; +import im.vector.matrix.android.internal.legacy.rest.model.RoomMember; +import im.vector.matrix.android.internal.legacy.rest.model.RoomPinnedEventsContent; +import im.vector.matrix.android.internal.legacy.rest.model.RoomTombstoneContent; +import im.vector.matrix.android.internal.legacy.rest.model.User; +import im.vector.matrix.android.internal.legacy.rest.model.pid.RoomThirdPartyInvite; +import im.vector.matrix.android.internal.legacy.util.JsonUtils; +import im.vector.matrix.android.internal.legacy.util.Log; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * The state of a room. + */ +public class RoomState implements Externalizable { + private static final String LOG_TAG = RoomState.class.getSimpleName(); + private static final long serialVersionUID = -6019932024524988201L; + + public static final String JOIN_RULE_PUBLIC = "public"; + public static final String JOIN_RULE_INVITE = "invite"; + + /** + * room access is granted to guests + */ + public static final String GUEST_ACCESS_CAN_JOIN = "can_join"; + /** + * room access is denied to guests + */ + public static final String GUEST_ACCESS_FORBIDDEN = "forbidden"; + + public static final String HISTORY_VISIBILITY_SHARED = "shared"; + public static final String HISTORY_VISIBILITY_INVITED = "invited"; + public static final String HISTORY_VISIBILITY_JOINED = "joined"; + public static final String HISTORY_VISIBILITY_WORLD_READABLE = "world_readable"; + + + // Public members used for JSON mapping + + // The room ID + public String roomId; + + // The power level of room members + private PowerLevels powerLevels; + + // The aliases + public List aliases; + + // The room aliases. The key is the domain. + private Map mRoomAliases = new HashMap<>(); + + // the aliases are defined for each home server url + private Map> mAliasesByDomain = new HashMap<>(); + + // merged from mAliasesByHomeServerUrl + private List mMergedAliasesList; + + // + private Map> mStateEvents = new HashMap<>(); + + // The canonical alias of the room. + private String canonicalAlias; + + // The name of the room as provided by the home server. + public String name; + + // The topic of the room. + public String topic; + + // The tombstone content if the room has been killed + private RoomTombstoneContent mRoomTombstoneContent; + + // The avatar url of the room. + public String url; + public String avatar_url; + + // the room create content + private RoomCreateContent mRoomCreateContent; + + // the room pinned events content + @Nullable + private RoomPinnedEventsContent mRoomPinnedEventsContent; + + // the join rule + public String join_rule; + + /** + * the guest access policy of the room + **/ + public String guest_access; + + // SPEC-134 + public String history_visibility; + + /** + * the room visibility in the directory list (i.e. public, private...) + **/ + public String visibility; + + // the encryption algorithm + public String algorithm; + + // group ids list which should be displayed + public List groups; + + /** + * The number of unread messages that match the push notification rules. + * It is based on the notificationCount field in /sync response. + */ + private int mNotificationCount; + + /** + * The number of highlighted unread messages (subset of notifications). + * It is based on the notificationCount field in /sync response. + */ + private int mHighlightCount; + + // the associated token + private String token; + + // the room members. May be a partial list if all members are not loaded yet, due to lazy loading + private final Map mMembers = new HashMap<>(); + + // true if all members are loaded + private boolean mAllMembersAreLoaded; + + private final List>> mGetAllMembersCallbacks = new ArrayList<>(); + + // the third party invite members + private final Map mThirdPartyInvites = new HashMap<>(); + + /** + * Cache for [self memberWithThirdPartyInviteToken]. + * The key is the 3pid invite token. + */ + private final Map mMembersWithThirdPartyInviteTokenCache = new HashMap<>(); + + /** + * Tell if the roomstate if a live one. + */ + private boolean mIsLive; + + // the unitary tests crash when MXDataHandler type is set. + // TODO Try to avoid this ^^ + private transient Object mDataHandler = null; + + // member display cache + private transient Map mMemberDisplayNameByUserId = new HashMap<>(); + + // get the guest access + // avoid the null case + public String getGuestAccess() { + if (null != guest_access) { + return guest_access; + } + + // retro compliancy + return RoomState.GUEST_ACCESS_FORBIDDEN; + } + + // get the history visibility + // avoid the null case + public String getHistoryVisibility() { + if (null != history_visibility) { + return history_visibility; + } + + // retro compliancy + return RoomState.HISTORY_VISIBILITY_SHARED; + } + + /** + * @return the state token + */ + public String getToken() { + return token; + } + + /** + * Update the token. + * + * @param token the new token + */ + public void setToken(String token) { + this.token = token; + } + + // avatar Url makes more sense than url. + public String getAvatarUrl() { + if (null != url) { + return url; + } else { + return avatar_url; + } + } + + /** + * @return the related group ids list (cannot be null) + */ + public List getRelatedGroups() { + return (null == groups) ? new ArrayList() : groups; + } + + /** + * @return a copy of the room members list. May be incomplete if the full list is not loaded yet + */ + public List getLoadedMembers() { + List res; + + synchronized (this) { + // make a copy to avoid concurrency modifications + res = new ArrayList<>(mMembers.values()); + } + + return res; + } + + /** + * Get the list of all the room members. Fetch from server if the full list is not loaded yet. + * + * @param callback The callback to get a copy of the room members list. + */ + public void getMembersAsync(ApiCallback> callback) { + if (areAllMembersLoaded()) { + List res; + + synchronized (this) { + // make a copy to avoid concurrency modifications + res = new ArrayList<>(mMembers.values()); + } + + callback.onSuccess(res); + } else { + boolean doTheRequest; + + synchronized (mGetAllMembersCallbacks) { + mGetAllMembersCallbacks.add(callback); + + doTheRequest = mGetAllMembersCallbacks.size() == 1; + } + + if (doTheRequest) { + // Load members from server + getDataHandler().getMembersAsync(roomId, new SimpleApiCallback>(callback) { + @Override + public void onSuccess(List info) { + Log.d(LOG_TAG, "getMembers has returned " + info.size() + " users."); + + IMXStore store = ((MXDataHandler) mDataHandler).getStore(); + List res; + + for (RoomMember member : info) { + // Do not erase already known members form the sync + if (getMember(member.getUserId()) == null) { + setMember(member.getUserId(), member); + + // Also create a User + if (store != null) { + store.updateUserWithRoomMemberEvent(member); + } + } + } + + synchronized (mGetAllMembersCallbacks) { + for (ApiCallback> apiCallback : mGetAllMembersCallbacks) { + // make a copy to avoid concurrency modifications + res = new ArrayList<>(mMembers.values()); + + apiCallback.onSuccess(res); + } + + mGetAllMembersCallbacks.clear(); + } + + mAllMembersAreLoaded = true; + } + }); + } + } + } + + /** + * Tell if all members has been loaded + * + * @return true if LazyLoading is Off, or if all members has been loaded + */ + private boolean areAllMembersLoaded() { + return mDataHandler != null + && (!((MXDataHandler) mDataHandler).isLazyLoadingEnabled() || mAllMembersAreLoaded); + } + + /** + * Force a fetch of the loaded members the next time they will be requested + */ + public void forceMembersRequest() { + mAllMembersAreLoaded = false; + } + + /** + * Provides the loaded states event list. + * The room member events are NOT included. + * + * @param types the allowed event types. + * @return the filtered state events list. + */ + public List getStateEvents(final Set types) { + final List filteredStateEvents = new ArrayList<>(); + final List stateEvents = new ArrayList<>(); + + // merge the values lists + Collection> currentStateEvents = mStateEvents.values(); + for (List eventsList : currentStateEvents) { + stateEvents.addAll(eventsList); + } + + if ((null != types) && !types.isEmpty()) { + for (Event stateEvent : stateEvents) { + if ((null != stateEvent.getType()) && types.contains(stateEvent.getType())) { + filteredStateEvents.add(stateEvent); + } + } + } else { + filteredStateEvents.addAll(stateEvents); + } + + return filteredStateEvents; + } + + + /** + * Provides the state events list. + * It includes the room member creation events (they are not loaded in memory by default). + * + * @param store the store in which the state events must be retrieved + * @param types the allowed event types. + * @param callback the asynchronous callback. + */ + public void getStateEvents(IMXStore store, final Set types, final ApiCallback> callback) { + if (null != store) { + final List stateEvents = new ArrayList<>(); + + Collection> currentStateEvents = mStateEvents.values(); + + for (List eventsList : currentStateEvents) { + stateEvents.addAll(eventsList); + } + + // retrieve the roomMember creation events + store.getRoomStateEvents(roomId, new SimpleApiCallback>() { + @Override + public void onSuccess(List events) { + stateEvents.addAll(events); + + final List filteredStateEvents = new ArrayList<>(); + + if ((null != types) && !types.isEmpty()) { + for (Event stateEvent : stateEvents) { + if ((null != stateEvent.getType()) && types.contains(stateEvent.getType())) { + filteredStateEvents.add(stateEvent); + } + } + } else { + filteredStateEvents.addAll(stateEvents); + } + + callback.onSuccess(filteredStateEvents); + } + }); + } + } + + /** + * @return a copy of the displayable members list. May be incomplete if the full list is not loaded yet + */ + public List getDisplayableLoadedMembers() { + List res = getLoadedMembers(); + + RoomMember conferenceUserId = getMember(MXCallsManager.getConferenceUserId(roomId)); + + if (null != conferenceUserId) { + res.remove(conferenceUserId); + } + + return res; + } + + /** + * Provides a list of displayable members. + * Some dummy members are created to internal stuff. + * + * @param callback The callback to get a copy of the displayable room members list. + */ + public void getDisplayableMembersAsync(final ApiCallback> callback) { + getMembersAsync(new SimpleApiCallback>(callback) { + @Override + public void onSuccess(List members) { + RoomMember conferenceUserId = getMember(MXCallsManager.getConferenceUserId(roomId)); + + if (null != conferenceUserId) { + List membersList = new ArrayList<>(members); + membersList.remove(conferenceUserId); + callback.onSuccess(membersList); + } else { + callback.onSuccess(members); + } + } + }); + } + + /** + * Tells if the room is a call conference one + * i.e. this room has been created to manage the call conference + * + * @return true if it is a call conference room. + */ + public boolean isConferenceUserRoom() { + return getDataHandler().getStore().getSummary(roomId).isConferenceUserRoom(); + } + + /** + * Set this room as a conference user room + * + * @param isConferenceUserRoom true when it is an user conference room. + */ + public void setIsConferenceUserRoom(boolean isConferenceUserRoom) { + getDataHandler().getStore().getSummary(roomId).setIsConferenceUserRoom(isConferenceUserRoom); + } + + /** + * Update the room member from its user id. + * + * @param userId the user id. + * @param member the new member value. + */ + private void setMember(String userId, RoomMember member) { + // Populate a basic user object if there is none + if (member.getUserId() == null) { + member.setUserId(userId); + } + synchronized (this) { + if (null != mMemberDisplayNameByUserId) { + mMemberDisplayNameByUserId.remove(userId); + } + mMembers.put(userId, member); + } + } + + /** + * Retrieve a room member from its user id. + * + * @param userId the user id. + * @return the linked member it exists. + */ + // TODO Change this? Can return null if all members are not loaded yet + @Nullable + public RoomMember getMember(String userId) { + RoomMember member; + + synchronized (this) { + member = mMembers.get(userId); + } + + if (member == null) { + // TODO LazyLoading + Log.e(LOG_TAG, "!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Null member '" + userId + "' !!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + + if (TextUtils.equals(getDataHandler().getUserId(), userId)) { + // This should never happen + Log.e(LOG_TAG, "!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Null current user '" + userId + "' !!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + } + } + + return member; + } + + /** + * Retrieve a room member from its original event id. + * It can return null if the lazy loading is enabled and if the member is not loaded yet. + * + * @param eventId the event id. + * @return the linked member if it exists and if it is loaded. + */ + @Nullable + public RoomMember getMemberByEventId(String eventId) { + RoomMember member = null; + + synchronized (this) { + for (RoomMember aMember : mMembers.values()) { + if (aMember.getOriginalEventId().equals(eventId)) { + member = aMember; + break; + } + } + } + + return member; + } + + /** + * Remove a member defines by its user id. + * + * @param userId the user id. + */ + public void removeMember(String userId) { + synchronized (this) { + mMembers.remove(userId); + // remove the cached display name + if (null != mMemberDisplayNameByUserId) { + mMemberDisplayNameByUserId.remove(userId); + } + } + } + + /** + * Retrieve a member from an invitation token. + * + * @param thirdPartyInviteToken the third party invitation token. + * @return the member it exists. + */ + public RoomMember memberWithThirdPartyInviteToken(String thirdPartyInviteToken) { + return mMembersWithThirdPartyInviteTokenCache.get(thirdPartyInviteToken); + } + + /** + * Retrieve a RoomThirdPartyInvite from its token. + * + * @param thirdPartyInviteToken the third party invitation token. + * @return the linked RoomThirdPartyInvite if it exists + */ + public RoomThirdPartyInvite thirdPartyInviteWithToken(String thirdPartyInviteToken) { + return mThirdPartyInvites.get(thirdPartyInviteToken); + } + + /** + * @return the third party invite list. + */ + public Collection thirdPartyInvites() { + return mThirdPartyInvites.values(); + } + + /** + * @return the power levels (it can be null). + */ + public PowerLevels getPowerLevels() { + if (null != powerLevels) { + return powerLevels.deepCopy(); + } else { + return null; + } + } + + /** + * Update the power levels. + * + * @param powerLevels the new power levels + */ + public void setPowerLevels(PowerLevels powerLevels) { + this.powerLevels = powerLevels; + } + + /** + * Update the linked dataHandler. + * + * @param dataHandler the new dataHandler + */ + public void setDataHandler(MXDataHandler dataHandler) { + mDataHandler = dataHandler; + } + + /** + * @return the user dataHandler + */ + public MXDataHandler getDataHandler() { + return (MXDataHandler) mDataHandler; + } + + /** + * Update the notified messages count. + * + * @param notificationCount the new notified messages count. + */ + public void setNotificationCount(int notificationCount) { + Log.d(LOG_TAG, "## setNotificationCount() : " + notificationCount + " room id " + roomId); + mNotificationCount = notificationCount; + } + + /** + * @return the notified messages count. + */ + public int getNotificationCount() { + return mNotificationCount; + } + + /** + * Update the highlighted messages count. + * + * @param highlightCount the new highlighted messages count. + */ + public void setHighlightCount(int highlightCount) { + Log.d(LOG_TAG, "## setHighlightCount() : " + highlightCount + " room id " + roomId); + mHighlightCount = highlightCount; + } + + /** + * @return the highlighted messages count. + */ + public int getHighlightCount() { + return mHighlightCount; + } + + /** + * Check if the user userId can back paginate. + * + * @param isJoined true is user is in the room + * @param isInvited true is user is invited to the room + * @return true if the user can back paginate. + */ + public boolean canBackPaginate(boolean isJoined, boolean isInvited) { + String visibility = TextUtils.isEmpty(history_visibility) ? HISTORY_VISIBILITY_SHARED : history_visibility; + + return isJoined + || visibility.equals(HISTORY_VISIBILITY_WORLD_READABLE) + || visibility.equals(HISTORY_VISIBILITY_SHARED) + || (visibility.equals(HISTORY_VISIBILITY_INVITED) && isInvited); + } + + /** + * Make a deep copy of this room state object. + * + * @return the copy + */ + public RoomState deepCopy() { + RoomState copy = new RoomState(); + copy.roomId = roomId; + copy.setPowerLevels((powerLevels == null) ? null : powerLevels.deepCopy()); + copy.aliases = (aliases == null) ? null : new ArrayList<>(aliases); + copy.mAliasesByDomain = new HashMap<>(mAliasesByDomain); + copy.canonicalAlias = canonicalAlias; + copy.name = name; + copy.topic = topic; + copy.url = url; + copy.mRoomCreateContent = mRoomCreateContent != null ? mRoomCreateContent.deepCopy() : null; + copy.mRoomPinnedEventsContent = mRoomPinnedEventsContent != null ? mRoomPinnedEventsContent.deepCopy() : null; + copy.join_rule = join_rule; + copy.guest_access = guest_access; + copy.history_visibility = history_visibility; + copy.visibility = visibility; + copy.token = token; + copy.groups = groups; + copy.mDataHandler = mDataHandler; + copy.mIsLive = mIsLive; + copy.mAllMembersAreLoaded = mAllMembersAreLoaded; + copy.algorithm = algorithm; + copy.mRoomAliases = new HashMap<>(mRoomAliases); + copy.mStateEvents = new HashMap<>(mStateEvents); + copy.mRoomTombstoneContent = mRoomTombstoneContent != null ? mRoomTombstoneContent.deepCopy() : null; + synchronized (this) { + Iterator it = mMembers.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry pair = (Map.Entry) it.next(); + copy.setMember(pair.getKey(), pair.getValue().deepCopy()); + } + + Collection keys = mThirdPartyInvites.keySet(); + for (String key : keys) { + copy.mThirdPartyInvites.put(key, mThirdPartyInvites.get(key).deepCopy()); + } + + keys = mMembersWithThirdPartyInviteTokenCache.keySet(); + for (String key : keys) { + copy.mMembersWithThirdPartyInviteTokenCache.put(key, mMembersWithThirdPartyInviteTokenCache.get(key).deepCopy()); + } + } + + return copy; + } + + /** + * @return the room canonical alias + */ + public String getCanonicalAlias() { + return canonicalAlias; + } + + /** + * Update the canonical alias of a room + * + * @param newCanonicalAlias the new canonical alias + */ + public void setCanonicalAlias(String newCanonicalAlias) { + canonicalAlias = newCanonicalAlias; + } + + /** + * Provides the aliases for any known domains + * + * @return the aliases list + */ + public List getAliases() { + if (null == mMergedAliasesList) { + mMergedAliasesList = new ArrayList<>(); + + for (String url : mAliasesByDomain.keySet()) { + mMergedAliasesList.addAll(mAliasesByDomain.get(url)); + } + + // ensure that the current aliases have been added. + // for example for the public rooms because there is no applystate call. + if (null != aliases) { + for (String anAlias : aliases) { + if (mMergedAliasesList.indexOf(anAlias) < 0) { + mMergedAliasesList.add(anAlias); + } + } + } + } + + return mMergedAliasesList; + } + + /** + * Provides the aliases by domain + * + * @return the aliases list map + */ + public Map> getAliasesByDomain() { + return new HashMap<>(mAliasesByDomain); + } + + /** + * Remove an alias. + * + * @param alias the alias to remove + */ + public void removeAlias(String alias) { + if (getAliases().indexOf(alias) >= 0) { + if (null != aliases) { + aliases.remove(alias); + } + + for (String host : mAliasesByDomain.keySet()) { + mAliasesByDomain.get(host).remove(alias); + } + + mMergedAliasesList = null; + } + } + + /** + * Add an alias. + * + * @param alias the alias to add + */ + public void addAlias(String alias) { + if (getAliases().indexOf(alias) < 0) { + // patch until the server echoes the alias addition. + mMergedAliasesList.add(alias); + } + } + + /** + * @return true if the room is encrypted + */ + public boolean isEncrypted() { + // When a client receives an m.room.encryption event as above, it should set a flag to indicate that messages sent in the room should be encrypted. + // This flag should not be cleared if a later m.room.encryption event changes the configuration. This is to avoid a situation where a MITM can simply + // ask participants to disable encryption. In short: once encryption is enabled in a room, it can never be disabled. + return null != algorithm; + } + + /** + * @return true if the room is versioned, it means that the room is obsolete. + * You can't interact with it anymore, but you can still browse the past messages. + */ + public boolean isVersioned() { + return mRoomTombstoneContent != null; + } + + /** + * @return the room tombstone content + */ + public RoomTombstoneContent getRoomTombstoneContent() { + return mRoomTombstoneContent; + } + + /** + * @return true if the room has a predecessor + */ + public boolean hasPredecessor() { + return mRoomCreateContent != null && mRoomCreateContent.hasPredecessor(); + } + + /** + * @return the room create content + */ + public RoomCreateContent getRoomCreateContent() { + return mRoomCreateContent; + } + + /** + * @return the room pinned events content + */ + @Nullable + public RoomPinnedEventsContent getRoomPinnedEventsContent() { + return mRoomPinnedEventsContent; + } + + /** + * @return the encryption algorithm + */ + public String encryptionAlgorithm() { + return TextUtils.isEmpty(algorithm) ? null : algorithm; + } + + /** + * Apply the given event (relevant for state changes) to our state. + * + * @param store the store to use + * @param event the event + * @param direction how the event should affect the state: Forwards for applying, backwards for un-applying (applying the previous state) + * @return true if the event is managed + */ + public boolean applyState(IMXStore store, Event event, EventTimeline.Direction direction) { + if (event.stateKey == null) { + return false; + } + + JsonObject contentToConsider = (direction == EventTimeline.Direction.FORWARDS) ? event.getContentAsJsonObject() : event.getPrevContentAsJsonObject(); + String eventType = event.getType(); + + try { + if (Event.EVENT_TYPE_STATE_ROOM_NAME.equals(eventType)) { + name = JsonUtils.toStateEvent(contentToConsider).name; + } else if (Event.EVENT_TYPE_STATE_ROOM_TOPIC.equals(eventType)) { + topic = JsonUtils.toStateEvent(contentToConsider).topic; + } else if (Event.EVENT_TYPE_STATE_ROOM_CREATE.equals(eventType)) { + mRoomCreateContent = JsonUtils.toRoomCreateContent(contentToConsider); + } else if (Event.EVENT_TYPE_STATE_ROOM_JOIN_RULES.equals(eventType)) { + join_rule = JsonUtils.toStateEvent(contentToConsider).joinRule; + } else if (Event.EVENT_TYPE_STATE_ROOM_GUEST_ACCESS.equals(eventType)) { + guest_access = JsonUtils.toStateEvent(contentToConsider).guestAccess; + } else if (Event.EVENT_TYPE_STATE_ROOM_ALIASES.equals(eventType)) { + if (!TextUtils.isEmpty(event.stateKey)) { + // backward compatibility + aliases = JsonUtils.toStateEvent(contentToConsider).aliases; + + // sanity check + if (null != aliases) { + mAliasesByDomain.put(event.stateKey, aliases); + mRoomAliases.put(event.stateKey, event); + } else { + mAliasesByDomain.put(event.stateKey, new ArrayList()); + } + } + } else if (Event.EVENT_TYPE_MESSAGE_ENCRYPTION.equals(eventType)) { + algorithm = JsonUtils.toStateEvent(contentToConsider).algorithm; + + // When a client receives an m.room.encryption event as above, it should set a flag to indicate that messages sent + // in the room should be encrypted. + // This flag should not be cleared if a later m.room.encryption event changes the configuration. This is to avoid + // a situation where a MITM can simply ask participants to disable encryption. In short: once encryption is enabled + // in a room, it can never be disabled. + if (null == algorithm) { + algorithm = ""; + } + } else if (Event.EVENT_TYPE_STATE_CANONICAL_ALIAS.equals(eventType)) { + // SPEC-125 + canonicalAlias = JsonUtils.toStateEvent(contentToConsider).canonicalAlias; + } else if (Event.EVENT_TYPE_STATE_HISTORY_VISIBILITY.equals(eventType)) { + // SPEC-134 + history_visibility = JsonUtils.toStateEvent(contentToConsider).historyVisibility; + } else if (Event.EVENT_TYPE_STATE_ROOM_AVATAR.equals(eventType)) { + url = JsonUtils.toStateEvent(contentToConsider).url; + } else if (Event.EVENT_TYPE_STATE_RELATED_GROUPS.equals(eventType)) { + groups = JsonUtils.toStateEvent(contentToConsider).groups; + } else if (Event.EVENT_TYPE_STATE_ROOM_MEMBER.equals(eventType)) { + RoomMember member = JsonUtils.toRoomMember(contentToConsider); + String userId = event.stateKey; + + if (null == userId) { + Log.e(LOG_TAG, "## applyState() : null stateKey in " + roomId); + } else if (null == member) { + // the member has already been removed + if (null == getMember(userId)) { + Log.e(LOG_TAG, "## applyState() : the user " + userId + " is not anymore a member of " + roomId); + return false; + } + removeMember(userId); + } else { + try { + member.setUserId(userId); + member.setOriginServerTs(event.getOriginServerTs()); + member.setOriginalEventId(event.eventId); + member.mSender = event.getSender(); + + if ((null != store) && (direction == EventTimeline.Direction.FORWARDS)) { + store.storeRoomStateEvent(roomId, event); + } + + RoomMember currentMember = getMember(userId); + + // check if the member is the same + // duplicated message ? + if (member.equals(currentMember)) { + Log.e(LOG_TAG, "## applyState() : seems being a duplicated event for " + userId + " in room " + roomId); + return false; + } + + // when a member leaves a room, his avatar / display name is not anymore provided + if (null != currentMember) { + if (TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_LEAVE) + || TextUtils.equals(member.membership, (RoomMember.MEMBERSHIP_BAN))) { + if (null == member.getAvatarUrl()) { + member.setAvatarUrl(currentMember.getAvatarUrl()); + } + + if (null == member.displayname) { + member.displayname = currentMember.displayname; + } + + // remove the cached display name + if (null != mMemberDisplayNameByUserId) { + mMemberDisplayNameByUserId.remove(userId); + } + + // test if the user has been kicked + if (!TextUtils.equals(event.getSender(), event.stateKey) + && TextUtils.equals(currentMember.membership, RoomMember.MEMBERSHIP_JOIN) + && TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_LEAVE)) { + member.membership = RoomMember.MEMBERSHIP_KICK; + } + } + } + + if ((direction == EventTimeline.Direction.FORWARDS) && (null != store)) { + store.updateUserWithRoomMemberEvent(member); + } + + // Cache room member event that is successor of a third party invite event + if (!TextUtils.isEmpty(member.getThirdPartyInviteToken())) { + mMembersWithThirdPartyInviteTokenCache.put(member.getThirdPartyInviteToken(), member); + } + } catch (Exception e) { + Log.e(LOG_TAG, "## applyState() - EVENT_TYPE_STATE_ROOM_MEMBER failed " + e.getMessage(), e); + } + + setMember(userId, member); + } + } else if (Event.EVENT_TYPE_STATE_ROOM_POWER_LEVELS.equals(eventType)) { + powerLevels = JsonUtils.toPowerLevels(contentToConsider); + } else if (Event.EVENT_TYPE_STATE_ROOM_THIRD_PARTY_INVITE.equals(event.getType())) { + if (null != contentToConsider) { + RoomThirdPartyInvite thirdPartyInvite = JsonUtils.toRoomThirdPartyInvite(contentToConsider); + + thirdPartyInvite.token = event.stateKey; + + if ((direction == EventTimeline.Direction.FORWARDS) && (null != store)) { + store.storeRoomStateEvent(roomId, event); + } + + if (!TextUtils.isEmpty(thirdPartyInvite.token)) { + mThirdPartyInvites.put(thirdPartyInvite.token, thirdPartyInvite); + } + } + } else if (Event.EVENT_TYPE_STATE_ROOM_TOMBSTONE.equals(eventType)) { + mRoomTombstoneContent = JsonUtils.toRoomTombstoneContent(contentToConsider); + } else if (Event.EVENT_TYPE_STATE_PINNED_EVENT.equals(eventType)) { + mRoomPinnedEventsContent = JsonUtils.toRoomPinnedEventsContent(contentToConsider); + } + // same the latest room state events + // excepts the membership ones + // they are saved elsewhere + if (!TextUtils.isEmpty(eventType) && !Event.EVENT_TYPE_STATE_ROOM_MEMBER.equals(eventType)) { + List eventsList = mStateEvents.get(eventType); + + if (null == eventsList) { + eventsList = new ArrayList<>(); + mStateEvents.put(eventType, eventsList); + } + + eventsList.add(event); + } + + } catch (Exception e) { + Log.e(LOG_TAG, "applyState failed with error " + e.getMessage(), e); + } + + return true; + } + + /** + * @return true if the room is a public one + */ + public boolean isPublic() { + return TextUtils.equals((null != visibility) ? visibility : join_rule, RoomDirectoryVisibility.DIRECTORY_VISIBILITY_PUBLIC); + } + + /** + * Return an unique display name of the member userId. + * + * @param userId the user id + * @return unique display name + */ + public String getMemberName(String userId) { + // sanity check + if (null == userId) { + return null; + } + + String displayName; + + synchronized (this) { + if (null == mMemberDisplayNameByUserId) { + mMemberDisplayNameByUserId = new HashMap<>(); + } + displayName = mMemberDisplayNameByUserId.get(userId); + } + + if (null != displayName) { + return displayName; + } + + // Get the user display name from the member list of the room + RoomMember member = getMember(userId); + + // Do not consider null display name + if ((null != member) && !TextUtils.isEmpty(member.displayname)) { + displayName = member.displayname; + + synchronized (this) { + List matrixIds = new ArrayList<>(); + + // Disambiguate users who have the same display name in the room + for (RoomMember aMember : mMembers.values()) { + if (displayName.equals(aMember.displayname)) { + matrixIds.add(aMember.getUserId()); + } + } + + // if several users have the same display name + // index it i.e bob () + if (matrixIds.size() > 1) { + displayName += " (" + userId + ")"; + } + } + } else if ((null != member) && TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_INVITE)) { + User user = ((MXDataHandler) mDataHandler).getUser(userId); + + if (null != user) { + displayName = user.displayname; + } + } + + if (null == displayName) { + // By default, use the user ID + displayName = userId; + } + + mMemberDisplayNameByUserId.put(userId, displayName); + + return displayName; + } + + @Override + public void readExternal(ObjectInput input) throws IOException, ClassNotFoundException { + if (input.readBoolean()) { + roomId = input.readUTF(); + } + + if (input.readBoolean()) { + powerLevels = (PowerLevels) input.readObject(); + } + + if (input.readBoolean()) { + aliases = (List) input.readObject(); + } + + List roomAliasesEvents = (List) input.readObject(); + for (Event e : roomAliasesEvents) { + mRoomAliases.put(e.stateKey, e); + } + + mAliasesByDomain = (Map>) input.readObject(); + + if (input.readBoolean()) { + mMergedAliasesList = (List) input.readObject(); + } + + Map> stateEvents = (Map>) input.readObject(); + if (null != stateEvents) { + mStateEvents = new HashMap<>(stateEvents); + } + + if (input.readBoolean()) { + canonicalAlias = input.readUTF(); + } + + if (input.readBoolean()) { + name = input.readUTF(); + } + + if (input.readBoolean()) { + topic = input.readUTF(); + } + + if (input.readBoolean()) { + url = input.readUTF(); + } + + if (input.readBoolean()) { + avatar_url = input.readUTF(); + } + + if (input.readBoolean()) { + mRoomCreateContent = (RoomCreateContent) input.readObject(); + } + + if (input.readBoolean()) { + mRoomPinnedEventsContent = (RoomPinnedEventsContent) input.readObject(); + } + + if (input.readBoolean()) { + join_rule = input.readUTF(); + } + + if (input.readBoolean()) { + guest_access = input.readUTF(); + } + + if (input.readBoolean()) { + history_visibility = input.readUTF(); + } + + if (input.readBoolean()) { + visibility = input.readUTF(); + } + + if (input.readBoolean()) { + algorithm = input.readUTF(); + } + + mNotificationCount = input.readInt(); + mHighlightCount = input.readInt(); + + if (input.readBoolean()) { + token = input.readUTF(); + } + + List members = (List) input.readObject(); + for (RoomMember r : members) { + mMembers.put(r.getUserId(), r); + } + + List invites = (List) input.readObject(); + for (RoomThirdPartyInvite i : invites) { + mThirdPartyInvites.put(i.token, i); + } + + List inviteTokens = (List) input.readObject(); + for (RoomMember r : inviteTokens) { + mMembersWithThirdPartyInviteTokenCache.put(r.getThirdPartyInviteToken(), r); + } + + mIsLive = input.readBoolean(); + + mAllMembersAreLoaded = input.readBoolean(); + + if (input.readBoolean()) { + groups = (List) input.readObject(); + } + + if (input.readBoolean()) { + mRoomTombstoneContent = (RoomTombstoneContent) input.readObject(); + } + } + + @Override + public void writeExternal(ObjectOutput output) throws IOException { + output.writeBoolean(null != roomId); + if (null != roomId) { + output.writeUTF(roomId); + } + + output.writeBoolean(null != powerLevels); + if (null != powerLevels) { + output.writeObject(powerLevels); + } + + output.writeBoolean(null != aliases); + if (null != aliases) { + output.writeObject(aliases); + } + + output.writeObject(new ArrayList<>(mRoomAliases.values())); + + output.writeObject(mAliasesByDomain); + + output.writeBoolean(null != mMergedAliasesList); + if (null != mMergedAliasesList) { + output.writeObject(mMergedAliasesList); + } + + output.writeObject(mStateEvents); + + output.writeBoolean(null != canonicalAlias); + if (null != canonicalAlias) { + output.writeUTF(canonicalAlias); + } + + output.writeBoolean(null != name); + if (null != name) { + output.writeUTF(name); + } + + output.writeBoolean(null != topic); + if (null != topic) { + output.writeUTF(topic); + } + + output.writeBoolean(null != url); + if (null != url) { + output.writeUTF(url); + } + + output.writeBoolean(null != avatar_url); + if (null != avatar_url) { + output.writeUTF(avatar_url); + } + + output.writeBoolean(null != mRoomCreateContent); + if (null != mRoomCreateContent) { + output.writeObject(mRoomCreateContent); + } + + output.writeBoolean(null != mRoomPinnedEventsContent); + if (null != mRoomPinnedEventsContent) { + output.writeObject(mRoomPinnedEventsContent); + } + + output.writeBoolean(null != join_rule); + if (null != join_rule) { + output.writeUTF(join_rule); + } + + output.writeBoolean(null != guest_access); + if (null != guest_access) { + output.writeUTF(guest_access); + } + + output.writeBoolean(null != history_visibility); + if (null != history_visibility) { + output.writeUTF(history_visibility); + } + + output.writeBoolean(null != visibility); + if (null != visibility) { + output.writeUTF(visibility); + } + + output.writeBoolean(null != algorithm); + if (null != algorithm) { + output.writeUTF(algorithm); + } + + output.writeInt(mNotificationCount); + output.writeInt(mHighlightCount); + + output.writeBoolean(null != token); + if (null != token) { + output.writeUTF(token); + } + + output.writeObject(new ArrayList<>(mMembers.values())); + output.writeObject(new ArrayList<>(mThirdPartyInvites.values())); + output.writeObject(new ArrayList<>(mMembersWithThirdPartyInviteTokenCache.values())); + + output.writeBoolean(mIsLive); + + output.writeBoolean(mAllMembersAreLoaded); + + output.writeBoolean(null != groups); + if (null != groups) { + output.writeObject(groups); + } + + output.writeBoolean(null != mRoomTombstoneContent); + if (null != mRoomTombstoneContent) { + output.writeObject(mRoomTombstoneContent); + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomSummary.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomSummary.java new file mode 100644 index 0000000000..51a9521e88 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomSummary.java @@ -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 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 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 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 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 getRoomTags() { + return mRoomTags; + } + + /** + * Update the room tags + * + * @param roomTags the room tags + */ + public void setRoomTags(final Set 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 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 getHeroes() { + return mHeroes; + } + + public int getNumberOfJoinedMembers() { + return mJoinedMembersCountFromSyncRoomSummary; + } + + public int getNumberOfInvitedMembers() { + return mInvitedMembersCountFromSyncRoomSummary; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomTag.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomTag.java new file mode 100644 index 0000000000..ec8d6f94f6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomTag.java @@ -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 roomTagsWithTagEvent(Event event) { + Map 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 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; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/comparator/Comparators.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/comparator/Comparators.java new file mode 100644 index 0000000000..2c8c874910 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/comparator/Comparators.java @@ -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 ascComparator = new Comparator() { + @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 descComparator = new Comparator() { + @Override + public int compare(DatedObject datedObject1, DatedObject datedObject2) { + return (int) (datedObject2.getDate() - datedObject1.getDate()); + } + }; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/comparator/RoomComparatorWithTag.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/comparator/RoomComparatorWithTag.java new file mode 100644 index 0000000000..f42b600f93 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/comparator/RoomComparatorWithTag.java @@ -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 { + + 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; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/cryptostore/IMXCryptoStore.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/cryptostore/IMXCryptoStore.java new file mode 100644 index 0000000000..fb6498537d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/cryptostore/IMXCryptoStore.java @@ -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 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 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 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 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 roomIds); + + /** + * Provides the rooms ids list in which the messages are not encrypted for the unverified devices. + * + * @return the room Ids list + */ + List getRoomsListBlacklistUnverifiedDevices(); + + /** + * @return the devices statuses map + */ + Map getDeviceTrackingStatuses(); + + /** + * Save the device statuses + * + * @param deviceTrackingStatuses the device tracking statuses + */ + void saveDeviceTrackingStatuses(Map 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 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 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 getPendingIncomingRoomKeyRequests(); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/cryptostore/MXFileCryptoStore.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/cryptostore/MXFileCryptoStore.java new file mode 100644 index 0000000000..7b66e5bfd8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/cryptostore/MXFileCryptoStore.java @@ -0,0 +1,1748 @@ +/* + * 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.cryptostore; + +import android.content.Context; +import android.os.Looper; +import android.text.TextUtils; + +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.MXOlmInboundGroupSession; +import im.vector.matrix.android.internal.legacy.crypto.data.MXOlmInboundGroupSession2; +import im.vector.matrix.android.internal.legacy.crypto.data.MXUsersDevicesMap; +import im.vector.matrix.android.internal.legacy.rest.model.login.Credentials; +import im.vector.matrix.android.internal.legacy.util.CompatUtil; +import im.vector.matrix.android.internal.legacy.util.ContentUtils; +import im.vector.matrix.android.internal.legacy.util.Log; +import org.matrix.olm.OlmAccount; +import org.matrix.olm.OlmSession; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +/** + * the crypto data store + */ +public class MXFileCryptoStore implements IMXCryptoStore { + private static final String LOG_TAG = MXFileCryptoStore.class.getSimpleName(); + + private static final int MXFILE_CRYPTO_VERSION = 1; + + private static final String MXFILE_CRYPTO_STORE_FOLDER = "MXFileCryptoStore"; + + private static final String MXFILE_CRYPTO_STORE_METADATA_FILE = "MXFileCryptoStore"; + private static final String MXFILE_CRYPTO_STORE_METADATA_FILE_TMP = "MXFileCryptoStore.tmp"; + + private static final String MXFILE_CRYPTO_STORE_ACCOUNT_FILE = "account"; + private static final String MXFILE_CRYPTO_STORE_ACCOUNT_FILE_TMP = "account.tmp"; + + private static final String MXFILE_CRYPTO_STORE_DEVICES_FOLDER = "devicesFolder"; + private static final String MXFILE_CRYPTO_STORE_DEVICES_FILE = "devices"; + private static final String MXFILE_CRYPTO_STORE_DEVICES_FILE_TMP = "devices.tmp"; + + private static final String MXFILE_CRYPTO_STORE_TRACKING_STATUSES_FILE = "trackingStatuses"; + private static final String MXFILE_CRYPTO_STORE_TRACKING_STATUSES_FILE_TMP = "trackingStatuses.tmp"; + + private static final String MXFILE_CRYPTO_STORE_ALGORITHMS_FILE = "roomsAlgorithms"; + private static final String MXFILE_CRYPTO_STORE_ALGORITHMS_FILE_TMP = "roomsAlgorithms.tmp"; + + private static final String MXFILE_CRYPTO_STORE_OLM_SESSIONS_FILE = "sessions"; + private static final String MXFILE_CRYPTO_STORE_OLM_SESSIONS_FILE_TMP = "sessions.tmp"; + private static final String MXFILE_CRYPTO_STORE_OLM_SESSIONS_FOLDER = "olmSessionsFolder"; + + private static final String MXFILE_CRYPTO_STORE_INBOUND_GROUP_SESSIONS_FILE = "inboundGroupSessions"; + private static final String MXFILE_CRYPTO_STORE_INBOUND_GROUP_SESSIONS_FILE_TMP = "inboundGroupSessions.tmp"; + private static final String MXFILE_CRYPTO_STORE_INBOUND_GROUP_SESSIONS_FOLDER = "inboundGroupSessionsFolder"; + + private static final String MXFILE_CRYPTO_STORE_OUTGOING_ROOM_KEY_REQUEST_FILE = "outgoingRoomKeyRequests"; + private static final String MXFILE_CRYPTO_STORE_OUTGOING_ROOM_KEY_REQUEST_FILE_TMP = "outgoingRoomKeyRequests.tmp"; + + private static final String MXFILE_CRYPTO_STORE_INCOMING_ROOM_KEY_REQUESTS_FILE = "incomingRoomKeyRequests"; + private static final String MXFILE_CRYPTO_STORE_INCOMING_ROOM_KEY_REQUESTS_FILE_TMP = "incomingRoomKeyRequests.tmp"; + + // The credentials used for this store + private Credentials mCredentials; + + // Meta data about the store + private MXFileCryptoStoreMetaData2 mMetaData; + + // The olm account + private OlmAccount mOlmAccount; + + // All users devices keys + private MXUsersDevicesMap mUsersDevicesInfoMap; + private final Object mUsersDevicesInfoMapLock = new Object(); + + // The algorithms used in rooms + private Map mRoomsAlgorithms; + + // the tracking statuses + private Map mTrackingStatuses; + + // The olm sessions ( -> ( -> ) + private Map> mOlmSessions; + private static final Object mOlmSessionsLock = new Object(); + + // The inbound group megolm sessions ( -> ( -> ) + private Map> mInboundGroupSessions; + private final Object mInboundGroupSessionsLock = new Object(); + + private final Map, OutgoingRoomKeyRequest> mOutgoingRoomKeyRequests = new HashMap<>(); + + // userId -> deviceId -> [keyRequest] + private Map>> mPendingIncomingRoomKeyRequests; + + // The path of the MXFileCryptoStore folder + private File mStoreFile; + + private File mMetaDataFile; + private File mMetaDataFileTmp; + + private File mAccountFile; + private File mAccountFileTmp; + + private File mDevicesFolder; + private File mDevicesFile; + private File mDevicesFileTmp; + + private File mAlgorithmsFile; + private File mAlgorithmsFileTmp; + + private File mTrackingStatusesFile; + private File mTrackingStatusesFileTmp; + + private File mOlmSessionsFile; + private File mOlmSessionsFileTmp; + private File mOlmSessionsFolder; + + private File mInboundGroupSessionsFile; + private File mInboundGroupSessionsFileTmp; + private File mInboundGroupSessionsFolder; + + private File mOutgoingRoomKeyRequestsFile; + private File mOutgoingRoomKeyRequestsFileTmp; + + private File mIncomingRoomKeyRequestsFile; + private File mIncomingRoomKeyRequestsFileTmp; + + // tell if the store is corrupted + private boolean mIsCorrupted = false; + + // tell if the store is ready + private boolean mIsReady = false; + + private Context mContext; + + // True if file encryption is enabled + private final boolean mEnableFileEncryption; + + /** + * Constructor + * + * @param enableFileEncryption set to true to enable file encryption. + */ + public MXFileCryptoStore(boolean enableFileEncryption) { + mEnableFileEncryption = enableFileEncryption; + } + + @Override + public void initWithCredentials(Context context, Credentials credentials) { + mCredentials = credentials; + + mStoreFile = new File(new File(context.getApplicationContext().getFilesDir(), MXFILE_CRYPTO_STORE_FOLDER), mCredentials.userId); + + mMetaDataFile = new File(mStoreFile, MXFILE_CRYPTO_STORE_METADATA_FILE); + mMetaDataFileTmp = new File(mStoreFile, MXFILE_CRYPTO_STORE_METADATA_FILE_TMP); + + mAccountFile = new File(mStoreFile, MXFILE_CRYPTO_STORE_ACCOUNT_FILE); + mAccountFileTmp = new File(mStoreFile, MXFILE_CRYPTO_STORE_ACCOUNT_FILE_TMP); + + mDevicesFolder = new File(mStoreFile, MXFILE_CRYPTO_STORE_DEVICES_FOLDER); + mDevicesFile = new File(mStoreFile, MXFILE_CRYPTO_STORE_DEVICES_FILE); + mDevicesFileTmp = new File(mStoreFile, MXFILE_CRYPTO_STORE_DEVICES_FILE_TMP); + + mAlgorithmsFile = new File(mStoreFile, MXFILE_CRYPTO_STORE_ALGORITHMS_FILE); + mAlgorithmsFileTmp = new File(mStoreFile, MXFILE_CRYPTO_STORE_ALGORITHMS_FILE_TMP); + + mTrackingStatusesFile = new File(mStoreFile, MXFILE_CRYPTO_STORE_TRACKING_STATUSES_FILE); + mTrackingStatusesFileTmp = new File(mStoreFile, MXFILE_CRYPTO_STORE_TRACKING_STATUSES_FILE_TMP); + + // backward compatibility : the sessions used to be stored in an unique file + mOlmSessionsFile = new File(mStoreFile, MXFILE_CRYPTO_STORE_OLM_SESSIONS_FILE); + mOlmSessionsFileTmp = new File(mStoreFile, MXFILE_CRYPTO_STORE_OLM_SESSIONS_FILE_TMP); + // each session is now stored in a dedicated file + mOlmSessionsFolder = new File(mStoreFile, MXFILE_CRYPTO_STORE_OLM_SESSIONS_FOLDER); + + mInboundGroupSessionsFile = new File(mStoreFile, MXFILE_CRYPTO_STORE_INBOUND_GROUP_SESSIONS_FILE); + mInboundGroupSessionsFileTmp = new File(mStoreFile, MXFILE_CRYPTO_STORE_INBOUND_GROUP_SESSIONS_FILE_TMP); + mInboundGroupSessionsFolder = new File(mStoreFile, MXFILE_CRYPTO_STORE_INBOUND_GROUP_SESSIONS_FOLDER); + + mOutgoingRoomKeyRequestsFile = new File(mStoreFile, MXFILE_CRYPTO_STORE_OUTGOING_ROOM_KEY_REQUEST_FILE); + mOutgoingRoomKeyRequestsFileTmp = new File(mStoreFile, MXFILE_CRYPTO_STORE_OUTGOING_ROOM_KEY_REQUEST_FILE_TMP); + + mIncomingRoomKeyRequestsFile = new File(mStoreFile, MXFILE_CRYPTO_STORE_INCOMING_ROOM_KEY_REQUESTS_FILE); + mIncomingRoomKeyRequestsFileTmp = new File(mStoreFile, MXFILE_CRYPTO_STORE_INCOMING_ROOM_KEY_REQUESTS_FILE_TMP); + + // Build default metadata + if ((null == mMetaData) + && (null != credentials.homeServer) + && (null != credentials.userId) + && (null != credentials.accessToken)) { + mMetaData = new MXFileCryptoStoreMetaData2(mCredentials.userId, mCredentials.deviceId, MXFILE_CRYPTO_VERSION); + } + + mUsersDevicesInfoMap = new MXUsersDevicesMap<>(); + mRoomsAlgorithms = new HashMap<>(); + mTrackingStatuses = new HashMap<>(); + mOlmSessions = new HashMap<>(); + mInboundGroupSessions = new HashMap<>(); + + mContext = context; + } + + @Override + public boolean hasData() { + boolean result = mStoreFile.exists(); + + if (result) { + // User ids match. Check device ids + loadMetaData(); + + if (null != mMetaData) { + result = TextUtils.isEmpty(mMetaData.mDeviceId) + || TextUtils.equals(mCredentials.deviceId, mMetaData.mDeviceId); + } + } + + return result; + } + + + @Override + public boolean isCorrupted() { + return mIsCorrupted; + } + + @Override + public void deleteStore() { + // delete the dedicated directories + try { + ContentUtils.deleteDirectory(mStoreFile); + } catch (Exception e) { + Log.e(LOG_TAG, "deleteStore failed " + e.getMessage(), e); + } + } + + @Override + public void open() { + if (mIsReady) { + Log.e(LOG_TAG, "## open() : the store is already opened"); + } else { + mMetaData = null; + + loadMetaData(); + + // Check if + if (null == mMetaData) { + resetData(); + } + // Check store version + else if (MXFILE_CRYPTO_VERSION != mMetaData.mVersion) { + Log.e(LOG_TAG, "## open() : New MXFileCryptoStore version detected"); + resetData(); + } + // Check credentials + // The device id may not have been provided in credentials. + // Check it only if provided, else trust the stored one. + else if (!TextUtils.equals(mMetaData.mUserId, mCredentials.userId) + || ((null != mCredentials.deviceId) && !TextUtils.equals(mCredentials.deviceId, mMetaData.mDeviceId))) { + Log.e(LOG_TAG, "## open() : Credentials do not match"); + resetData(); + } + + // If metaData is still defined, we can load rooms data + if (null != mMetaData) { + preloadCryptoData(); + } + + // Else, if credentials is valid, create and store it + if ((null == mMetaData) + && (null != mCredentials.homeServer) + && (null != mCredentials.userId) + && (null != mCredentials.accessToken)) { + mMetaData = new MXFileCryptoStoreMetaData2(mCredentials.userId, mCredentials.deviceId, MXFILE_CRYPTO_VERSION); + mIsReady = true; + // flush the metadata + saveMetaData(); + } else { + mIsReady = true; + } + } + } + + @Override + public void storeDeviceId(String deviceId) { + if (!mIsReady) { + Log.e(LOG_TAG, "## storeDeviceId() : the store is not ready"); + return; + } + + mMetaData.mDeviceId = deviceId; + saveMetaData(); + } + + @Override + public String getDeviceId() { + if (!mIsReady) { + Log.e(LOG_TAG, "## getDeviceId() : the store is not ready"); + return null; + } + + return mMetaData.mDeviceId; + } + + /** + * Store a serializable object into a dedicated file. + * + * @param object the object to write. + * @param folder the folder + * @param filename the filename + * @param description the object description + * @return true if the operation succeeds + */ + private boolean storeObject(Object object, File folder, String filename, String description) { + if (!mIsReady) { + Log.e(LOG_TAG, "## storeObject() : the store is not ready"); + return false; + } + + // sanity checks + if ((null == object) || (null == folder) || (null == filename)) { + Log.e(LOG_TAG, "## storeObject() : invalid parameters"); + return false; + } + + // ensure that the folder exists + // it should always exist but it happened + if (!folder.exists()) { + if (!folder.mkdirs()) { + Log.e(LOG_TAG, "Cannot create the folder " + folder); + } + } + + return storeObject(object, new File(folder, filename), description); + } + + /** + * Store a serializable object into a dedicated file. + * + * @param object the object to write. + * @param file the file + * @param description the object description + * @return true if the operation succeeds + */ + private boolean storeObject(Object object, File file, String description) { + if (!mIsReady) { + Log.e(LOG_TAG, "## storeObject() : the store is not ready"); + return false; + } + + if (Thread.currentThread() == Looper.getMainLooper().getThread()) { + Log.e(LOG_TAG, "## storeObject() : should not be called in the UI thread " + description); + } + + boolean succeed = false; + + synchronized (LOG_TAG) { + try { + long t0 = System.currentTimeMillis(); + + if (file.exists()) { + file.delete(); + } + + FileOutputStream fos = new FileOutputStream(file); + OutputStream cos; + if (mEnableFileEncryption) { + cos = CompatUtil.createCipherOutputStream(fos, mContext); + } else { + cos = fos; + } + GZIPOutputStream gz = CompatUtil.createGzipOutputStream(cos); + ObjectOutputStream out = new ObjectOutputStream(gz); + + out.writeObject(object); + out.flush(); + out.close(); + + succeed = true; + Log.d(LOG_TAG, "## storeObject () : " + description + " done in " + (System.currentTimeMillis() - t0) + " ms"); + + } catch (OutOfMemoryError oom) { + Log.e(LOG_TAG, "storeObject failed : " + description + " -- " + oom.getMessage(), oom); + } catch (Exception e) { + Log.e(LOG_TAG, "storeObject failed : " + description + " -- " + e.getMessage(), e); + } + } + + return succeed; + } + + /** + * Save the metadata into the crypto file store + */ + private void saveMetaData() { + if (mMetaDataFileTmp.exists()) { + mMetaDataFileTmp.delete(); + } + + if (mMetaDataFile.exists()) { + mMetaDataFile.renameTo(mMetaDataFileTmp); + } + + if (storeObject(mMetaData, mMetaDataFile, "saveMetaData")) { + if (mMetaDataFileTmp.exists()) { + mMetaDataFileTmp.delete(); + } + } else { + if (mMetaDataFileTmp.exists()) { + mMetaDataFileTmp.renameTo(mMetaDataFile); + } + } + } + + @Override + public void storeAccount(OlmAccount account) { + if (!mIsReady) { + Log.e(LOG_TAG, "## storeAccount() : the store is not ready"); + return; + } + + mOlmAccount = account; + + if (mAccountFileTmp.exists()) { + mAccountFileTmp.delete(); + } + + if (mAccountFile.exists()) { + mAccountFile.renameTo(mAccountFileTmp); + } + + if (storeObject(mOlmAccount, mAccountFile, "storeAccount")) { + if (mAccountFileTmp.exists()) { + mAccountFileTmp.delete(); + } + } else { + if (mAccountFileTmp.exists()) { + mAccountFileTmp.renameTo(mAccountFile); + } + } + } + + @Override + public OlmAccount getAccount() { + if (!mIsReady) { + Log.e(LOG_TAG, "## getAccount() : the store is not ready"); + return null; + } + + return mOlmAccount; + } + + /** + * Load the user devices from the filesystem + * if it is not yet done. + * + * @param userId the user id. + */ + private void loadUserDevices(String userId) { + if (!TextUtils.isEmpty(userId)) { + boolean alreadyDone; + + synchronized (mUsersDevicesInfoMapLock) { + alreadyDone = mUsersDevicesInfoMap.getMap().containsKey(userId); + } + + if (!alreadyDone) { + File devicesFile = new File(mDevicesFolder, userId); + + if (devicesFile.exists()) { + long t0 = System.currentTimeMillis(); + + // clear the corrupted flag + mIsCorrupted = false; + + Object devicesMapAsVoid = loadObject(devicesFile, "load devices of " + userId); + + if (null != devicesMapAsVoid) { + try { + synchronized (mUsersDevicesInfoMapLock) { + mUsersDevicesInfoMap.setObjects((Map) devicesMapAsVoid, userId); + } + } catch (Exception e) { + Log.e(LOG_TAG, "## loadUserDevices : mUsersDevicesInfoMap.setObjects failed " + e.getMessage(), e); + mIsCorrupted = true; + } + } + + // something was wrong (loadObject set this boolean) + if (mIsCorrupted) { + Log.e(LOG_TAG, "## loadUserDevices : failed to load the device of " + userId); + + // delete the corrupted file + devicesFile.delete(); + // it is not a blocking thing + mIsCorrupted = false; + } else { + Log.d(LOG_TAG, "## loadUserDevices : Load the devices of " + userId + " in " + (System.currentTimeMillis() - t0) + "ms"); + } + } + } + } + } + + @Override + public void storeUserDevice(String userId, MXDeviceInfo device) { + if (!mIsReady) { + Log.e(LOG_TAG, "## storeUserDevice() : the store is not ready"); + return; + } + + final Map devicesMap; + + loadUserDevices(userId); + + synchronized (mUsersDevicesInfoMapLock) { + mUsersDevicesInfoMap.setObject(device, userId, device.deviceId); + devicesMap = new HashMap<>(mUsersDevicesInfoMap.getMap().get(userId)); + } + + storeObject(devicesMap, mDevicesFolder, userId, "storeUserDevice " + userId + " with " + devicesMap.size() + " devices"); + } + + @Override + public MXDeviceInfo getUserDevice(String deviceId, String userId) { + if (!mIsReady) { + Log.e(LOG_TAG, "## getUserDevice() : the store is not ready"); + return null; + } + + MXDeviceInfo deviceInfo; + + loadUserDevices(userId); + + synchronized (mUsersDevicesInfoMapLock) { + deviceInfo = mUsersDevicesInfoMap.getObject(deviceId, userId); + } + + return deviceInfo; + } + + @Override + public void storeUserDevices(String userId, Map devices) { + if (!mIsReady) { + Log.e(LOG_TAG, "## storeUserDevices() : the store is not ready"); + return; + } + + synchronized (mUsersDevicesInfoMapLock) { + mUsersDevicesInfoMap.setObjects(devices, userId); + } + + storeObject(devices, mDevicesFolder, userId, "storeUserDevice " + userId); + } + + @Override + public Map getUserDevices(String userId) { + if (!mIsReady) { + Log.e(LOG_TAG, "## getUserDevices() : the store is not ready"); + return null; + } + + if (null != userId) { + Map devicesMap; + + loadUserDevices(userId); + + synchronized (mUsersDevicesInfoMapLock) { + devicesMap = mUsersDevicesInfoMap.getMap().get(userId); + } + + return devicesMap; + } else { + return null; + } + } + + @Override + public void storeRoomAlgorithm(String roomId, String algorithm) { + if (!mIsReady) { + Log.e(LOG_TAG, "## storeRoomAlgorithm() : the store is not ready"); + return; + } + + if ((null != roomId) && (null != algorithm)) { + mRoomsAlgorithms.put(roomId, algorithm); + + // delete the previous tmp + if (mAlgorithmsFileTmp.exists()) { + mAlgorithmsFileTmp.delete(); + } + + // copy the existing file + if (mAlgorithmsFile.exists()) { + mAlgorithmsFile.renameTo(mAlgorithmsFileTmp); + } + + if (storeObject(mRoomsAlgorithms, mAlgorithmsFile, "storeAlgorithmForRoom - in background")) { + // remove the tmp file + if (mAlgorithmsFileTmp.exists()) { + mAlgorithmsFileTmp.delete(); + } + } else { + if (mAlgorithmsFileTmp.exists()) { + mAlgorithmsFileTmp.renameTo(mAlgorithmsFile); + } + } + } + } + + @Override + public String getRoomAlgorithm(String roomId) { + if (!mIsReady) { + Log.e(LOG_TAG, "## getRoomAlgorithm() : the store is not ready"); + return null; + } + + if (null != roomId) { + return mRoomsAlgorithms.get(roomId); + } + + return null; + } + + @Override + public int getDeviceTrackingStatus(String userId, int defaultValue) { + if (!mIsReady) { + Log.e(LOG_TAG, "## getDeviceTrackingStatus() : the store is not ready"); + return defaultValue; + } + + if ((null != userId) && mTrackingStatuses.containsKey(userId)) { + return mTrackingStatuses.get(userId); + } else { + return defaultValue; + } + } + + @Override + public Map getDeviceTrackingStatuses() { + if (!mIsReady) { + Log.e(LOG_TAG, "## getDeviceTrackingStatuses() : the store is not ready"); + return null; + } + + return new HashMap<>(mTrackingStatuses); + } + + /** + * Save the tracking statuses map + */ + private void saveDeviceTrackingStatuses() { + // delete the previous tmp + if (mTrackingStatusesFileTmp.exists()) { + mTrackingStatusesFileTmp.delete(); + } + + // copy the existing file + if (mTrackingStatusesFile.exists()) { + mTrackingStatusesFile.renameTo(mTrackingStatusesFileTmp); + } + + if (storeObject(mTrackingStatuses, mTrackingStatusesFile, "saveDeviceTrackingStatus - in background")) { + // remove the tmp file + if (mTrackingStatusesFileTmp.exists()) { + mTrackingStatusesFileTmp.delete(); + } + } else { + if (mTrackingStatusesFileTmp.exists()) { + mTrackingStatusesFileTmp.renameTo(mTrackingStatusesFile); + } + } + } + + @Override + public void saveDeviceTrackingStatuses(Map deviceTrackingStatuses) { + if (!mIsReady) { + Log.e(LOG_TAG, "## saveDeviceTrackingStatuses() : the store is not ready"); + return; + } + + mTrackingStatuses.clear(); + mTrackingStatuses.putAll(deviceTrackingStatuses); + saveDeviceTrackingStatuses(); + } + + @Override + public void storeSession(final OlmSession olmSession, final String deviceKey) { + if (!mIsReady) { + Log.e(LOG_TAG, "## storeSession() : the store is not ready"); + return; + } + + String sessionIdentifier = null; + + if (null != olmSession) { + try { + sessionIdentifier = olmSession.sessionIdentifier(); + } catch (Exception e) { + Log.e(LOG_TAG, "## storeSession : session.sessionIdentifier() failed " + e.getMessage(), e); + } + } + + if ((null != deviceKey) && (null != sessionIdentifier)) { + synchronized (mOlmSessionsLock) { + if (!mOlmSessions.containsKey(deviceKey)) { + mOlmSessions.put(deviceKey, new HashMap()); + } + + OlmSession prevOlmSession = mOlmSessions.get(deviceKey).get(sessionIdentifier); + + // test if the session is a new one + if (olmSession != prevOlmSession) { + if (null != prevOlmSession) { + prevOlmSession.releaseSession(); + } + mOlmSessions.get(deviceKey).put(sessionIdentifier, olmSession); + } + } + + final File keyFolder = new File(mOlmSessionsFolder, encodeFilename(deviceKey)); + + if (!keyFolder.exists()) { + keyFolder.mkdir(); + } + + storeObject(olmSession, keyFolder, encodeFilename(sessionIdentifier), "Store olm session " + deviceKey + " " + sessionIdentifier); + } + } + + @Override + public Map getDeviceSessions(String deviceKey) { + if (!mIsReady) { + Log.e(LOG_TAG, "## storeSession() : the store is not ready"); + return null; + } + + if (null != deviceKey) { + Map map; + + synchronized (mOlmSessionsLock) { + map = mOlmSessions.get(deviceKey); + } + + return map; + } + + return null; + } + + @Override + public void removeInboundGroupSession(String sessionId, String senderKey) { + if (!mIsReady) { + Log.e(LOG_TAG, "## removeInboundGroupSession() : the store is not ready"); + return; + } + + if ((null != sessionId) && (null != senderKey)) { + synchronized (mInboundGroupSessionsLock) { + if (mInboundGroupSessions.containsKey(senderKey)) { + MXOlmInboundGroupSession2 session = mInboundGroupSessions.get(senderKey).get(sessionId); + + if (null != session) { + mInboundGroupSessions.get(senderKey).remove(sessionId); + + File senderKeyFolder = new File(mInboundGroupSessionsFolder, encodeFilename(session.mSenderKey)); + + if (senderKeyFolder.exists()) { + File inboundSessionFile = new File(senderKeyFolder, encodeFilename(sessionId)); + + if (!inboundSessionFile.delete()) { + Log.e(LOG_TAG, "## removeInboundGroupSession() : fail to remove the sessionid " + sessionId); + } + } + + // release the memory + session.mSession.releaseSession(); + } + } + } + } + } + + @Override + public void storeInboundGroupSession(final MXOlmInboundGroupSession2 session) { + if (!mIsReady) { + Log.e(LOG_TAG, "## storeInboundGroupSession() : the store is not ready"); + return; + } + + String sessionIdentifier = null; + + if ((null != session) && (null != session.mSenderKey) && (null != session.mSession)) { + try { + sessionIdentifier = session.mSession.sessionIdentifier(); + } catch (Exception e) { + Log.e(LOG_TAG, "## storeInboundGroupSession() : sessionIdentifier failed " + e.getMessage(), e); + } + } + + if (null != sessionIdentifier) { + synchronized (mInboundGroupSessionsLock) { + if (!mInboundGroupSessions.containsKey(session.mSenderKey)) { + mInboundGroupSessions.put(session.mSenderKey, new HashMap()); + } + + MXOlmInboundGroupSession2 curSession = mInboundGroupSessions.get(session.mSenderKey).get(sessionIdentifier); + + if (curSession != session) { + // release memory + if (null != curSession) { + curSession.mSession.releaseSession(); + } + // update the map + mInboundGroupSessions.get(session.mSenderKey).put(sessionIdentifier, session); + } + } + + Log.d(LOG_TAG, "## storeInboundGroupSession() : store session " + sessionIdentifier); + + File senderKeyFolder = new File(mInboundGroupSessionsFolder, encodeFilename(session.mSenderKey)); + + if (!senderKeyFolder.exists()) { + senderKeyFolder.mkdir(); + } + + storeObject(session, senderKeyFolder, encodeFilename(sessionIdentifier), "storeInboundGroupSession - in background"); + } + } + + @Override + public MXOlmInboundGroupSession2 getInboundGroupSession(String sessionId, String senderKey) { + if (!mIsReady) { + Log.e(LOG_TAG, "## getInboundGroupSession() : the store is not ready"); + return null; + } + + if ((null != sessionId) && (null != senderKey) && mInboundGroupSessions.containsKey(senderKey)) { + MXOlmInboundGroupSession2 session = null; + + try { + synchronized (mInboundGroupSessionsLock) { + session = mInboundGroupSessions.get(senderKey).get(sessionId); + } + } catch (Exception e) { + // it should never happen + // MXOlmInboundGroupSession has been replaced by MXOlmInboundGroupSession2 + // but it seems that the application code is not properly updated (JIT issue) ? + Log.e(LOG_TAG, "## getInboundGroupSession() failed " + e.getMessage(), e); + } + + return session; + } + return null; + } + + @Override + public List getInboundGroupSessions() { + if (!mIsReady) { + Log.e(LOG_TAG, "## getInboundGroupSessions() : the store is not ready"); + return null; + } + + List inboundGroupSessions = new ArrayList<>(); + + synchronized (mInboundGroupSessionsLock) { + for (String senderKey : mInboundGroupSessions.keySet()) { + inboundGroupSessions.addAll(mInboundGroupSessions.get(senderKey).values()); + } + } + + return inboundGroupSessions; + } + + @Override + public void close() { + // release JNI objects + List olmSessions = new ArrayList<>(); + Collection> sessionValues = mOlmSessions.values(); + + for (Map value : sessionValues) { + olmSessions.addAll(value.values()); + } + + for (OlmSession olmSession : olmSessions) { + olmSession.releaseSession(); + } + mOlmSessions.clear(); + + List groupSessions = new ArrayList<>(); + Collection> groupSessionsValues = mInboundGroupSessions.values(); + + for (Map map : groupSessionsValues) { + groupSessions.addAll(map.values()); + } + + for (MXOlmInboundGroupSession2 groupSession : groupSessions) { + if (null != groupSession.mSession) { + groupSession.mSession.releaseSession(); + } + } + mInboundGroupSessions.clear(); + } + + @Override + public void setGlobalBlacklistUnverifiedDevices(boolean block) { + if (!mIsReady) { + Log.e(LOG_TAG, "## setGlobalBlacklistUnverifiedDevices() : the store is not ready"); + return; + } + + mMetaData.mGlobalBlacklistUnverifiedDevices = block; + saveMetaData(); + } + + @Override + public boolean getGlobalBlacklistUnverifiedDevices() { + if (!mIsReady) { + Log.e(LOG_TAG, "## getGlobalBlacklistUnverifiedDevices() : the store is not ready"); + return false; + } + + return mMetaData.mGlobalBlacklistUnverifiedDevices; + } + + @Override + public void setRoomsListBlacklistUnverifiedDevices(List roomIds) { + if (!mIsReady) { + Log.e(LOG_TAG, "## setRoomsListBlacklistUnverifiedDevices() : the store is not ready"); + return; + } + + mMetaData.mBlacklistUnverifiedDevicesRoomIdsList = roomIds; + saveMetaData(); + } + + @Override + public List getRoomsListBlacklistUnverifiedDevices() { + if (!mIsReady) { + Log.e(LOG_TAG, "## getRoomsListBlacklistUnverifiedDevices() : the store is not ready"); + return null; + } + + if (null == mMetaData.mBlacklistUnverifiedDevicesRoomIdsList) { + return new ArrayList<>(); + } else { + return new ArrayList<>(mMetaData.mBlacklistUnverifiedDevicesRoomIdsList); + } + } + + /** + * save the outgoing room key requests. + */ + private void saveOutgoingRoomKeyRequests() { + if (mOutgoingRoomKeyRequestsFileTmp.exists()) { + mOutgoingRoomKeyRequestsFileTmp.delete(); + } + + if (mOutgoingRoomKeyRequestsFile.exists()) { + mOutgoingRoomKeyRequestsFile.renameTo(mOutgoingRoomKeyRequestsFileTmp); + } + + if (storeObject(mOutgoingRoomKeyRequests, mOutgoingRoomKeyRequestsFile, "saveOutgoingRoomKeyRequests")) { + if (mOutgoingRoomKeyRequestsFileTmp.exists()) { + mOutgoingRoomKeyRequestsFileTmp.delete(); + } + } else { + if (mOutgoingRoomKeyRequestsFileTmp.exists()) { + mOutgoingRoomKeyRequestsFileTmp.renameTo(mOutgoingRoomKeyRequestsFile); + } + } + } + + @Override + public OutgoingRoomKeyRequest getOutgoingRoomKeyRequest(Map requestBody) { + if (null != requestBody) { + return mOutgoingRoomKeyRequests.get(requestBody); + } + + return null; + } + + @Override + public OutgoingRoomKeyRequest getOrAddOutgoingRoomKeyRequest(OutgoingRoomKeyRequest request) { + // sanity check + if ((null == request) || (null == request.mRequestBody)) { + return null; + } + + // already known + if (mOutgoingRoomKeyRequests.containsKey(request.mRequestBody)) { + Log.d(LOG_TAG, "## getOrAddOutgoingRoomKeyRequest() : `already have key request outstanding for " + request.getRoomId() + " / " + + request.getSessionId() + " not sending another"); + return mOutgoingRoomKeyRequests.get(request.mRequestBody); + } else { + mOutgoingRoomKeyRequests.put(request.mRequestBody, request); + saveOutgoingRoomKeyRequests(); + return request; + } + } + + /** + * Retrieve a OutgoingRoomKeyRequest from a transaction id. + * + * @param txId the transaction id. + * @return the matched OutgoingRoomKeyRequest or null + */ + private OutgoingRoomKeyRequest getOutgoingRoomKeyRequestByTxId(String txId) { + if (null != txId) { + Collection requests = mOutgoingRoomKeyRequests.values(); + + for (OutgoingRoomKeyRequest request : requests) { + if (TextUtils.equals(request.mRequestId, txId)) { + return request; + } + } + } + + return null; + } + + /** + * Look for room key requests by state. + * + * @param states the states + * @return an OutgoingRoomKeyRequest or null + */ + @Override + public OutgoingRoomKeyRequest getOutgoingRoomKeyRequestByState(Set states) { + Collection requests = mOutgoingRoomKeyRequests.values(); + + for (OutgoingRoomKeyRequest request : requests) { + if (states.contains(request.mState)) { + return request; + } + } + + return null; + } + + @Override + public void updateOutgoingRoomKeyRequest(OutgoingRoomKeyRequest req) { + if (null != req) { + saveOutgoingRoomKeyRequests(); + } + } + + @Override + public void deleteOutgoingRoomKeyRequest(String transactionId) { + OutgoingRoomKeyRequest request = getOutgoingRoomKeyRequestByTxId(transactionId); + + if (null != request) { + mOutgoingRoomKeyRequests.remove(request.mRequestBody); + saveOutgoingRoomKeyRequests(); + } + } + + /** + * Reset the crypto store data + */ + private void resetData() { + close(); + + // ensure there is background writings while deleting the store + synchronized (LOG_TAG) { + deleteStore(); + } + + if (!mStoreFile.exists()) { + mStoreFile.mkdirs(); + } + + if (!mDevicesFolder.exists()) { + mDevicesFolder.mkdirs(); + } + + if (!mOlmSessionsFolder.exists()) { + mOlmSessionsFolder.mkdir(); + } + + if (!mInboundGroupSessionsFolder.exists()) { + mInboundGroupSessionsFolder.mkdirs(); + } + + mMetaData = null; + } + + /** + * Load a file from the crypto store + * + * @param file the file to read + * @param description the operation description + * @return the read object, null if it fails + */ + private Object loadObject(File file, String description) { + Object object = null; + + + if (file.exists()) { + try { + // the files are now zipped to reduce saving time + FileInputStream fis = new FileInputStream(file); + InputStream cis; + if (mEnableFileEncryption) { + cis = CompatUtil.createCipherInputStream(fis, mContext); + + if (cis == null) { + // fallback to unencrypted stream for backward compatibility + Log.i(LOG_TAG, "## loadObject() : failed to read encrypted, fallback to unencrypted read"); + fis.close(); + cis = new FileInputStream(file); + } + } else { + cis = fis; + } + + GZIPInputStream gz = new GZIPInputStream(cis); + ObjectInputStream ois = new ObjectInputStream(gz); + + object = ois.readObject(); + ois.close(); + } catch (Exception e) { + Log.e(LOG_TAG, description + "failed : " + e.getMessage() + " step 1", e); + + // if the zip deflating fails, try to use the former file saving method + try { + FileInputStream fis2 = new FileInputStream(file); + ObjectInputStream out = new ObjectInputStream(fis2); + + object = out.readObject(); + out.close(); + } catch (Exception subEx) { + // warn that some file loading fails + mIsCorrupted = true; + Log.e(LOG_TAG, description + "failed : " + subEx.getMessage() + " step 2", subEx); + } + } + } + return object; + } + + + /** + * Load the metadata from the store + */ + private void loadMetaData() { + Object metadataAsVoid; + + if (mMetaDataFileTmp.exists()) { + metadataAsVoid = loadObject(mMetaDataFileTmp, "loadMetadata"); + } else { + metadataAsVoid = loadObject(mMetaDataFile, "loadMetadata"); + } + + if (null != metadataAsVoid) { + try { + if (metadataAsVoid instanceof MXFileCryptoStoreMetaData2) { + mMetaData = (MXFileCryptoStoreMetaData2) metadataAsVoid; + } else { + mMetaData = new MXFileCryptoStoreMetaData2((MXFileCryptoStoreMetaData) metadataAsVoid); + } + } catch (Exception e) { + mIsCorrupted = true; + Log.e(LOG_TAG, "## loadMetadata() : metadata has been corrupted " + e.getMessage(), e); + } + } + } + + /** + * Preload the crypto data + */ + private void preloadCryptoData() { + Log.d(LOG_TAG, "## preloadCryptoData() starts"); + + long t0 = System.currentTimeMillis(); + Object olmAccountAsVoid; + + if (mAccountFileTmp.exists()) { + olmAccountAsVoid = loadObject(mAccountFileTmp, "preloadCryptoData - mAccountFile - tmp"); + } else { + olmAccountAsVoid = loadObject(mAccountFile, "preloadCryptoData - mAccountFile"); + } + + if (null != olmAccountAsVoid) { + try { + mOlmAccount = (OlmAccount) olmAccountAsVoid; + } catch (Exception e) { + mIsCorrupted = true; + Log.e(LOG_TAG, "## preloadCryptoData() - invalid mAccountFile " + e.getMessage(), e); + } + } + + Log.d(LOG_TAG, "## preloadCryptoData() : load mOlmAccount in " + (System.currentTimeMillis() - t0) + " ms"); + + // previous store format + if (!mDevicesFolder.exists()) { + Object usersDevicesInfoMapAsVoid; + + // if the tmp exists, it means that the latest file backup has been killed / stopped + if (mDevicesFileTmp.exists()) { + usersDevicesInfoMapAsVoid = loadObject(mDevicesFileTmp, "preloadCryptoData - mUsersDevicesInfoMap - tmp"); + } else { + usersDevicesInfoMapAsVoid = loadObject(mDevicesFile, "preloadCryptoData - mUsersDevicesInfoMap"); + } + + if (null != usersDevicesInfoMapAsVoid) { + try { + MXUsersDevicesMap objectAsMap = (MXUsersDevicesMap) usersDevicesInfoMapAsVoid; + mUsersDevicesInfoMap = new MXUsersDevicesMap<>(objectAsMap.getMap()); + } catch (Exception e) { + mIsCorrupted = true; + Log.e(LOG_TAG, "## preloadCryptoData() - invalid mUsersDevicesInfoMap " + e.getMessage(), e); + } + } else { + mIsCorrupted = false; + } + + mDevicesFolder.mkdirs(); + + if (null != mUsersDevicesInfoMap) { + Map> map = mUsersDevicesInfoMap.getMap(); + + Set userIds = map.keySet(); + + for (String userId : userIds) { + storeObject(map.get(userId), mDevicesFolder, userId, "convert devices map of " + userId); + } + + mDevicesFileTmp.delete(); + mDevicesFile.delete(); + } + } else { + // the user devices are loaded on demand + mUsersDevicesInfoMap = new MXUsersDevicesMap<>(); + } + + long t2 = System.currentTimeMillis(); + int algoSize = 0; + + Object algorithmsAsVoid; + + if (mAlgorithmsFileTmp.exists()) { + algorithmsAsVoid = loadObject(mAlgorithmsFileTmp, "preloadCryptoData - mRoomsAlgorithms - tmp"); + } else { + algorithmsAsVoid = loadObject(mAlgorithmsFile, "preloadCryptoData - mRoomsAlgorithms"); + } + + if (null != algorithmsAsVoid) { + try { + Map algorithmsMap = (Map) algorithmsAsVoid; + mRoomsAlgorithms = new HashMap<>(algorithmsMap); + algoSize = mRoomsAlgorithms.size(); + } catch (Exception e) { + mIsCorrupted = true; + Log.e(LOG_TAG, "## preloadCryptoData() - invalid mAlgorithmsFile " + e.getMessage(), e); + } + } + Log.d(LOG_TAG, "## preloadCryptoData() : load mRoomsAlgorithms (" + algoSize + " algos) in " + (System.currentTimeMillis() - t2) + " ms"); + + Object trackingStatusesAsVoid; + + if (mTrackingStatusesFileTmp.exists()) { + trackingStatusesAsVoid = loadObject(mTrackingStatusesFileTmp, "preloadCryptoData - mTrackingStatuses - tmp"); + } else { + trackingStatusesAsVoid = loadObject(mTrackingStatusesFile, "preloadCryptoData - mTrackingStatuses"); + } + + if (null != trackingStatusesAsVoid) { + try { + mTrackingStatuses = new HashMap<>((Map) trackingStatusesAsVoid); + } catch (Exception e) { + Log.e(LOG_TAG, "## preloadCryptoData() - invalid mTrackingStatuses " + e.getMessage(), e); + } + } + + File outgoingRequestFile; + if (mOutgoingRoomKeyRequestsFileTmp.exists()) { + outgoingRequestFile = mOutgoingRoomKeyRequestsFileTmp; + } else { + outgoingRequestFile = mOutgoingRoomKeyRequestsFile; + } + + if (outgoingRequestFile.exists()) { + Object requestsAsVoid = loadObject(outgoingRequestFile, "get outgoing key request"); + try { + if (null != requestsAsVoid) { + mOutgoingRoomKeyRequests.putAll((Map, OutgoingRoomKeyRequest>) requestsAsVoid); + } + } catch (Exception e) { + Log.e(LOG_TAG, "## preloadCryptoData() : mOutgoingRoomKeyRequests init failed " + e.getMessage(), e); + } + } + + if (mOlmSessionsFolder.exists()) { + long t3 = System.currentTimeMillis(); + + mOlmSessions = new HashMap<>(); + + String[] olmSessionFiles = mOlmSessionsFolder.list(); + + if (null != olmSessionFiles) { + // build mOlmSessions for the file system + for (int i = 0; i < olmSessionFiles.length; i++) { + String deviceKey = olmSessionFiles[i]; + + Map olmSessionSubMap = new HashMap<>(); + + File sessionsDeviceFolder = new File(mOlmSessionsFolder, deviceKey); + String[] sessionIds = sessionsDeviceFolder.list(); + + if (null != sessionIds) { + for (int j = 0; j < sessionIds.length; j++) { + String sessionId = sessionIds[j]; + OlmSession olmSession = (OlmSession) loadObject(new File(sessionsDeviceFolder, sessionId), "load the olmSession " + + deviceKey + " " + sessionId); + + if (null != olmSession) { + olmSessionSubMap.put(decodeFilename(sessionId), olmSession); + } + } + } + mOlmSessions.put(decodeFilename(deviceKey), olmSessionSubMap); + } + + Log.d(LOG_TAG, "## preloadCryptoData() : load " + olmSessionFiles.length + " olmsessions in " + (System.currentTimeMillis() - t3) + " ms"); + } + } else { + Object olmSessionsAsVoid; + + if (mOlmSessionsFileTmp.exists()) { + olmSessionsAsVoid = loadObject(mOlmSessionsFileTmp, "preloadCryptoData - mOlmSessions - tmp"); + } else { + olmSessionsAsVoid = loadObject(mOlmSessionsFile, "preloadCryptoData - mOlmSessions"); + } + + if (null != olmSessionsAsVoid) { + try { + Map> olmSessionMap = (Map>) olmSessionsAsVoid; + + mOlmSessions = new HashMap<>(); + + for (String key : olmSessionMap.keySet()) { + mOlmSessions.put(key, new HashMap<>(olmSessionMap.get(key))); + } + + // convert to the new format + if (!mOlmSessionsFolder.mkdir()) { + Log.e(LOG_TAG, "Cannot create the folder " + mOlmSessionsFolder); + } + + for (String key : olmSessionMap.keySet()) { + Map submap = olmSessionMap.get(key); + + File submapFolder = new File(mOlmSessionsFolder, encodeFilename(key)); + + if (!submapFolder.mkdir()) { + Log.e(LOG_TAG, "Cannot create the folder " + submapFolder); + } + + for (String sessionId : submap.keySet()) { + storeObject(submap.get(sessionId), submapFolder, encodeFilename(sessionId), "Convert olmSession " + key + " " + sessionId); + } + } + } catch (Exception e) { + mIsCorrupted = true; + Log.e(LOG_TAG, "## preloadCryptoData() - invalid mSessionsFile " + e.getMessage(), e); + } + + mOlmSessionsFileTmp.delete(); + mOlmSessionsFile.delete(); + } + } + + if (mInboundGroupSessionsFolder.exists()) { + long t4 = System.currentTimeMillis(); + mInboundGroupSessions = new HashMap<>(); + + int count = 0; + + String[] keysFolder = mInboundGroupSessionsFolder.list(); + + if (null != keysFolder) { + for (int i = 0; i < keysFolder.length; i++) { + File keyFolder = new File(mInboundGroupSessionsFolder, keysFolder[i]); + + Map submap = new HashMap<>(); + + String[] sessionIds = keyFolder.list(); + + if (null != sessionIds) { + for (int j = 0; j < sessionIds.length; j++) { + File inboundSessionFile = new File(keyFolder, sessionIds[j]); + try { + Object inboundSessionAsVoid = loadObject(inboundSessionFile, "load inboundsession " + sessionIds[j] + " "); + MXOlmInboundGroupSession2 inboundSession; + + if ((null != inboundSessionAsVoid) && (inboundSessionAsVoid instanceof MXOlmInboundGroupSession)) { + inboundSession = new MXOlmInboundGroupSession2((MXOlmInboundGroupSession) inboundSessionAsVoid); + } else { + inboundSession = (MXOlmInboundGroupSession2) inboundSessionAsVoid; + } + + if (null != inboundSession) { + submap.put(decodeFilename(sessionIds[j]), inboundSession); + } else { + Log.e(LOG_TAG, "## preloadCryptoData() : delete " + inboundSessionFile); + inboundSessionFile.delete(); + mIsCorrupted = false; + } + count++; + } catch (Exception e) { + Log.e(LOG_TAG, "## preloadCryptoData() - invalid mInboundGroupSessions " + e.getMessage(), e); + } + } + } + + mInboundGroupSessions.put(decodeFilename(keysFolder[i]), submap); + } + } + + Log.d(LOG_TAG, "## preloadCryptoData() : load " + count + " inboundGroupSessions in " + (System.currentTimeMillis() - t4) + " ms"); + } else { + Object inboundGroupSessionsAsVoid; + + if (mInboundGroupSessionsFileTmp.exists()) { + inboundGroupSessionsAsVoid = loadObject(mInboundGroupSessionsFileTmp, "preloadCryptoData - mInboundGroupSessions - tmp"); + } else { + inboundGroupSessionsAsVoid = loadObject(mInboundGroupSessionsFile, "preloadCryptoData - mInboundGroupSessions"); + } + + if (null != inboundGroupSessionsAsVoid) { + try { + Map> inboundGroupSessionsMap + = (Map>) inboundGroupSessionsAsVoid; + + mInboundGroupSessions = new HashMap<>(); + + for (String key : inboundGroupSessionsMap.keySet()) { + mInboundGroupSessions.put(key, new HashMap<>(inboundGroupSessionsMap.get(key))); + } + } catch (Exception e) { + mIsCorrupted = true; + Log.e(LOG_TAG, "## preloadCryptoData() - invalid mInboundGroupSessions " + e.getMessage(), e); + } + + if (!mInboundGroupSessionsFolder.mkdirs()) { + Log.e(LOG_TAG, "Cannot create the folder " + mInboundGroupSessionsFolder); + } + + // convert to the new format + for (String key : mInboundGroupSessions.keySet()) { + File keyFolder = new File(mInboundGroupSessionsFolder, encodeFilename(key)); + + if (!keyFolder.mkdirs()) { + Log.e(LOG_TAG, "Cannot create the folder " + keyFolder); + } + + Map inboundMaps = mInboundGroupSessions.get(key); + + for (String sessionId : inboundMaps.keySet()) { + storeObject(inboundMaps.get(sessionId), keyFolder, encodeFilename(sessionId), "Convert inboundsession"); + } + } + } + + mInboundGroupSessionsFileTmp.delete(); + mInboundGroupSessionsFile.delete(); + } + + if ((null == mOlmAccount) && (mUsersDevicesInfoMap.getMap().size() > 0)) { + mIsCorrupted = true; + Log.e(LOG_TAG, "## preloadCryptoData() - there is no account but some devices are defined"); + } + } + + final private static char[] hexArray = "0123456789ABCDEF".toCharArray(); + + /** + * Encode the provided filename + * + * @param filename the filename to encode + * @return the encoded filename + */ + private static String encodeFilename(String filename) { + if (null == filename) { + return null; + } + + try { + byte[] bytes = filename.getBytes("UTF-8"); + char[] hexChars = new char[bytes.length * 2]; + + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } catch (Exception e) { + Log.e(LOG_TAG, "## encodeFilename() - failed " + e.getMessage(), e); + } + + return filename; + } + + /** + * Decode an encoded filename. + * + * @param encodedFilename the encoded filename + * @return the decodec filename + */ + private static String decodeFilename(String encodedFilename) { + if (null == encodedFilename) { + return null; + } + + int length = encodedFilename.length(); + + byte[] bytes = new byte[length / 2]; + + for (int i = 0; i < length; i += 2) { + bytes[i / 2] = (byte) ((Character.digit(encodedFilename.charAt(i), 16) << 4) + + Character.digit(encodedFilename.charAt(i + 1), 16)); + } + + try { + return new String(bytes, "UTF-8"); + } catch (Exception e) { + Log.e(LOG_TAG, "## decodeFilename() - failed " + e.getMessage(), e); + } + + return encodedFilename; + } + + + /** + * Tells if an IncomingRoomKeyRequest instance is valid + * + * @param incomingRoomKeyRequest the incomingRoomKeyRequest instance + * @return true if it is valid + */ + private boolean isValidIncomingRoomKeyRequest(IncomingRoomKeyRequest incomingRoomKeyRequest) { + return (null != incomingRoomKeyRequest) + && !TextUtils.isEmpty(incomingRoomKeyRequest.mUserId) + && !TextUtils.isEmpty(incomingRoomKeyRequest.mDeviceId) + && !TextUtils.isEmpty(incomingRoomKeyRequest.mRequestId); + } + + @Override + public IncomingRoomKeyRequest getIncomingRoomKeyRequest(String userId, String deviceId, String requestId) { + // sanity checks + if (TextUtils.isEmpty(userId) || TextUtils.isEmpty(deviceId) || TextUtils.isEmpty(requestId)) { + return null; + } + + if (!mPendingIncomingRoomKeyRequests.containsKey(userId)) { + return null; + } + + if (!mPendingIncomingRoomKeyRequests.get(userId).containsKey(deviceId)) { + return null; + } + + List pendingRequests = mPendingIncomingRoomKeyRequests.get(userId).get(deviceId); + + for (IncomingRoomKeyRequest request : pendingRequests) { + if (TextUtils.equals(requestId, request.mRequestId)) { + return request; + } + } + + return null; + } + + @Override + public List getPendingIncomingRoomKeyRequests() { + loadIncomingRoomKeyRequests(); + + List list = new ArrayList<>(); + + // userId -> deviceId -> [keyRequest] + Set userIds = mPendingIncomingRoomKeyRequests.keySet(); + + for (String userId : userIds) { + Set deviceIds = mPendingIncomingRoomKeyRequests.get(userId).keySet(); + for (String deviceId : deviceIds) { + list.addAll(mPendingIncomingRoomKeyRequests.get(userId).get(deviceId)); + } + } + + return list; + } + + /** + * Add an incomingRoomKeyRequest. + * + * @param incomingRoomKeyRequest the incomingRoomKeyRequest request + */ + private void addIncomingRoomKeyRequest(IncomingRoomKeyRequest incomingRoomKeyRequest) { + String userId = incomingRoomKeyRequest.mUserId; + String deviceId = incomingRoomKeyRequest.mDeviceId; + + if (!mPendingIncomingRoomKeyRequests.containsKey(userId)) { + mPendingIncomingRoomKeyRequests.put(userId, new HashMap>()); + } + + if (!mPendingIncomingRoomKeyRequests.get(userId).containsKey(deviceId)) { + mPendingIncomingRoomKeyRequests.get(userId).put(deviceId, new ArrayList()); + } + + mPendingIncomingRoomKeyRequests.get(userId).get(deviceId).add(incomingRoomKeyRequest); + } + + @Override + public void storeIncomingRoomKeyRequest(IncomingRoomKeyRequest incomingRoomKeyRequest) { + loadIncomingRoomKeyRequests(); + + // invalid or already stored + if (!isValidIncomingRoomKeyRequest(incomingRoomKeyRequest) + || (null != getIncomingRoomKeyRequest(incomingRoomKeyRequest.mUserId, incomingRoomKeyRequest.mDeviceId, incomingRoomKeyRequest.mRequestId))) { + return; + } + + addIncomingRoomKeyRequest(incomingRoomKeyRequest); + saveIncomingRoomKeyRequests(); + } + + @Override + public void deleteIncomingRoomKeyRequest(IncomingRoomKeyRequest incomingRoomKeyRequest) { + loadIncomingRoomKeyRequests(); + + if (!isValidIncomingRoomKeyRequest(incomingRoomKeyRequest)) { + return; + } + + IncomingRoomKeyRequest request = getIncomingRoomKeyRequest(incomingRoomKeyRequest.mUserId, + incomingRoomKeyRequest.mDeviceId, incomingRoomKeyRequest.mRequestId); + + if (null == request) { + return; + } + + String userId = incomingRoomKeyRequest.mUserId; + String deviceId = incomingRoomKeyRequest.mDeviceId; + + mPendingIncomingRoomKeyRequests.get(userId).get(deviceId).remove(request); + + if (mPendingIncomingRoomKeyRequests.get(userId).get(deviceId).isEmpty()) { + mPendingIncomingRoomKeyRequests.get(userId).remove(deviceId); + } + + if (mPendingIncomingRoomKeyRequests.get(userId).isEmpty()) { + mPendingIncomingRoomKeyRequests.remove(userId); + } + + saveIncomingRoomKeyRequests(); + } + + /** + * Save the incoming key requests + */ + private void saveIncomingRoomKeyRequests() { + // delete the previous tmp + if (mIncomingRoomKeyRequestsFileTmp.exists()) { + mIncomingRoomKeyRequestsFileTmp.delete(); + } + + // copy the existing file + if (mIncomingRoomKeyRequestsFile.exists()) { + mIncomingRoomKeyRequestsFile.renameTo(mIncomingRoomKeyRequestsFileTmp); + } + + if (storeObject(getPendingIncomingRoomKeyRequests(), mIncomingRoomKeyRequestsFile, "savedIncomingRoomKeyRequests - in background")) { + // remove the tmp file + if (mIncomingRoomKeyRequestsFileTmp.exists()) { + mIncomingRoomKeyRequestsFileTmp.delete(); + } + } else { + if (mIncomingRoomKeyRequestsFileTmp.exists()) { + mIncomingRoomKeyRequestsFileTmp.renameTo(mIncomingRoomKeyRequestsFile); + } + } + } + + /** + * Load the incoming key requests + */ + private void loadIncomingRoomKeyRequests() { + if (null == mPendingIncomingRoomKeyRequests) { + Object requestsAsVoid; + + if (mIncomingRoomKeyRequestsFileTmp.exists()) { + requestsAsVoid = loadObject(mIncomingRoomKeyRequestsFileTmp, "loadIncomingRoomKeyRequests - tmp"); + } else { + requestsAsVoid = loadObject(mIncomingRoomKeyRequestsFile, "loadIncomingRoomKeyRequests"); + } + + List requests = new ArrayList<>(); + + if (null != requestsAsVoid) { + try { + requests = (List) requestsAsVoid; + } catch (Exception e) { + mIncomingRoomKeyRequestsFileTmp.delete(); + mIncomingRoomKeyRequestsFile.delete(); + } + } + + mPendingIncomingRoomKeyRequests = new HashMap<>(); + + for (IncomingRoomKeyRequest request : requests) { + addIncomingRoomKeyRequest(request); + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/cryptostore/MXFileCryptoStoreMetaData.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/cryptostore/MXFileCryptoStoreMetaData.java new file mode 100644 index 0000000000..19dbdf63f0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/cryptostore/MXFileCryptoStoreMetaData.java @@ -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; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/cryptostore/MXFileCryptoStoreMetaData2.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/cryptostore/MXFileCryptoStoreMetaData2.java new file mode 100644 index 0000000000..ad1dee99a6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/cryptostore/MXFileCryptoStoreMetaData2.java @@ -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 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<>(); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/metrics/MetricsListener.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/metrics/MetricsListener.java new file mode 100644 index 0000000000..da483353f9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/metrics/MetricsListener.java @@ -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); + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/IMXStore.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/IMXStore.java new file mode 100644 index 0000000000..428edcce36 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/IMXStore.java @@ -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 thirdPartyIdentifiers(); + + /** + * Update the third party identifiers list. + * + * @param identifiers the identifiers list + */ + void setThirdPartyIdentifiers(List identifiers); + + /** + * Update the ignored user ids list. + * + * @param users the user ids list + */ + void setIgnoredUserIdsList(List users); + + /** + * Update the direct chat rooms list + * + * @param directChatRoomsDict the direct chats map + */ + void setDirectChatRoomsDict(Map> directChatRoomsDict); + + /** + * @return the known rooms list + */ + Collection 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 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 getIgnoredUserIdsList(); + + /** + * @return the direct chats rooms list + */ + Map> 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 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) + + /** + *

Retrieve a list of all the room summaries stored.

+ * Typically this method will be called when generating a 'Recent Activity' list. + * + * @return A collection of room summaries. + */ + Collection 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> 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 getLatestUnsentEvents(String roomId); + + /** + * Return the list of undelivered events + * + * @param roomId the room id + * @return list of undelivered events + */ + List getUndeliveredEvents(String roomId); + + /** + * Return the list of unknown device events. + * + * @param roomId the room id + * @return list of unknown device events + */ + List 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 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 unreadEvents(String roomId, List 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 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 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 roomIds); + + /** + * Set the user widgets + */ + void setUserWidgets(Map contentDict); + + /** + * Get the user widgets + */ + Map getUserWidgets(); + + /** + * @return the room ids list which don't have URL preview enabled + */ + Set 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 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); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/IMXStoreListener.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/IMXStoreListener.java new file mode 100644 index 0000000000..3f26bd798e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/IMXStoreListener.java @@ -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); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/MXFileStore.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/MXFileStore.java new file mode 100644 index 0000000000..e474c41f17 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/MXFileStore.java @@ -0,0 +1,2607 @@ +/* + * 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.os.HandlerThread; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import im.vector.matrix.android.internal.legacy.HomeServerConnectionConfig; +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.RoomState; +import im.vector.matrix.android.internal.legacy.data.RoomSummary; +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.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 im.vector.matrix.android.internal.legacy.util.CompatUtil; +import im.vector.matrix.android.internal.legacy.util.ContentUtils; +import im.vector.matrix.android.internal.legacy.util.Log; +import im.vector.matrix.android.internal.legacy.util.MXOsHandler; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +/** + * An in-file IMXStore. + */ +public class MXFileStore extends MXMemoryStore { + private static final String LOG_TAG = MXFileStore.class.getSimpleName(); + + // some constant values + private static final int MXFILE_VERSION = 22; + + // ensure that there is enough messages to fill a tablet screen + private static final int MAX_STORED_MESSAGES_COUNT = 50; + + private static final String MXFILE_STORE_FOLDER = "MXFileStore"; + private static final String MXFILE_STORE_METADATA_FILE_NAME = "MXFileStore"; + + private static final String MXFILE_STORE_GZ_ROOMS_MESSAGES_FOLDER = "messages_gz"; + private static final String MXFILE_STORE_ROOMS_TOKENS_FOLDER = "tokens"; + private static final String MXFILE_STORE_GZ_ROOMS_STATE_FOLDER = "state_gz"; + private static final String MXFILE_STORE_GZ_ROOMS_STATE_EVENTS_FOLDER = "state_rooms_events"; + private static final String MXFILE_STORE_ROOMS_SUMMARY_FOLDER = "summary"; + private static final String MXFILE_STORE_ROOMS_RECEIPT_FOLDER = "receipts"; + private static final String MXFILE_STORE_ROOMS_ACCOUNT_DATA_FOLDER = "accountData"; + private static final String MXFILE_STORE_USER_FOLDER = "users"; + private static final String MXFILE_STORE_GROUPS_FOLDER = "groups"; + + // the data is read from the file system + private boolean mIsReady = false; + + // tell if the post processing has been done + private boolean mIsPostProcessingDone = false; + + // the read receipts are ready + private boolean mAreReceiptsReady = false; + + // the store is currently opening + private boolean mIsOpening = false; + + // List of rooms to save on [MXStore commit] + // filled with roomId + private Set mRoomsToCommitForMessages; + private Set mRoomsToCommitForStates; + //private Set mRoomsToCommitForStatesEvents; + private Set mRoomsToCommitForSummaries; + private Set mRoomsToCommitForAccountData; + private Set mRoomsToCommitForReceipts; + private Set mUserIdsToCommit; + private Set mGroupsToCommit; + + // Flag to indicate metaData needs to be store + private boolean mMetaDataHasChanged = false; + + // The path of the MXFileStore folders + private File mStoreFolderFile = null; + private File mGzStoreRoomsMessagesFolderFile = null; + private File mStoreRoomsTokensFolderFile = null; + private File mGzStoreRoomsStateFolderFile = null; + private File mGzStoreRoomsStateEventsFolderFile = null; + private File mStoreRoomsSummaryFolderFile = null; + private File mStoreRoomsMessagesReceiptsFolderFile = null; + private File mStoreRoomsAccountDataFolderFile = null; + private File mStoreUserFolderFile = null; + private File mStoreGroupsFolderFile = null; + + // the background thread + private HandlerThread mHandlerThread = null; + private MXOsHandler mFileStoreHandler = null; + + private boolean mIsKilled = false; + + private boolean mIsNewStorage = false; + + private boolean mAreUsersLoaded = false; + + private long mPreloadTime = 0; + + // the read receipts are asynchronously loaded + // keep a list of the remaining receipts to load + private final List mRoomReceiptsToLoad = new ArrayList<>(); + + // store some stats + private final Map mStoreStats = new HashMap<>(); + + // True if file encryption is enabled + private final boolean mEnableFileEncryption; + + /** + * Create the file store dirtrees + */ + private void createDirTree(String userId) { + // data path + // MXFileStore/userID/ + // MXFileStore/userID/MXFileStore + // MXFileStore/userID/MXFileStore/Messages/ + // MXFileStore/userID/MXFileStore/Tokens/ + // MXFileStore/userID/MXFileStore/States/ + // MXFileStore/userID/MXFileStore/Summaries/ + // MXFileStore/userID/MXFileStore/receipt//receipts + // MXFileStore/userID/MXFileStore/accountData/ + // MXFileStore/userID/MXFileStore/users/ + // MXFileStore/userID/MXFileStore/groups/ + + // create the dirtree + mStoreFolderFile = new File(new File(mContext.getApplicationContext().getFilesDir(), MXFILE_STORE_FOLDER), userId); + + if (!mStoreFolderFile.exists()) { + mStoreFolderFile.mkdirs(); + } + + mGzStoreRoomsMessagesFolderFile = new File(mStoreFolderFile, MXFILE_STORE_GZ_ROOMS_MESSAGES_FOLDER); + if (!mGzStoreRoomsMessagesFolderFile.exists()) { + mGzStoreRoomsMessagesFolderFile.mkdirs(); + } + + mStoreRoomsTokensFolderFile = new File(mStoreFolderFile, MXFILE_STORE_ROOMS_TOKENS_FOLDER); + if (!mStoreRoomsTokensFolderFile.exists()) { + mStoreRoomsTokensFolderFile.mkdirs(); + } + + mGzStoreRoomsStateFolderFile = new File(mStoreFolderFile, MXFILE_STORE_GZ_ROOMS_STATE_FOLDER); + if (!mGzStoreRoomsStateFolderFile.exists()) { + mGzStoreRoomsStateFolderFile.mkdirs(); + } + + mGzStoreRoomsStateEventsFolderFile = new File(mStoreFolderFile, MXFILE_STORE_GZ_ROOMS_STATE_EVENTS_FOLDER); + if (!mGzStoreRoomsStateEventsFolderFile.exists()) { + mGzStoreRoomsStateEventsFolderFile.mkdirs(); + } + + mStoreRoomsSummaryFolderFile = new File(mStoreFolderFile, MXFILE_STORE_ROOMS_SUMMARY_FOLDER); + if (!mStoreRoomsSummaryFolderFile.exists()) { + mStoreRoomsSummaryFolderFile.mkdirs(); + } + + mStoreRoomsMessagesReceiptsFolderFile = new File(mStoreFolderFile, MXFILE_STORE_ROOMS_RECEIPT_FOLDER); + if (!mStoreRoomsMessagesReceiptsFolderFile.exists()) { + mStoreRoomsMessagesReceiptsFolderFile.mkdirs(); + } + + mStoreRoomsAccountDataFolderFile = new File(mStoreFolderFile, MXFILE_STORE_ROOMS_ACCOUNT_DATA_FOLDER); + if (!mStoreRoomsAccountDataFolderFile.exists()) { + mStoreRoomsAccountDataFolderFile.mkdirs(); + } + + mStoreUserFolderFile = new File(mStoreFolderFile, MXFILE_STORE_USER_FOLDER); + if (!mStoreUserFolderFile.exists()) { + mStoreUserFolderFile.mkdirs(); + } + + mStoreGroupsFolderFile = new File(mStoreFolderFile, MXFILE_STORE_GROUPS_FOLDER); + if (!mStoreGroupsFolderFile.exists()) { + mStoreGroupsFolderFile.mkdirs(); + } + } + + /** + * Constructor + * + * @param hsConfig the expected credentials + * @param enableFileEncryption set to true to enable file encryption. + * @param context the context. + */ + public MXFileStore(HomeServerConnectionConfig hsConfig, boolean enableFileEncryption, Context context) { + setContext(context); + + mEnableFileEncryption = enableFileEncryption; + + mIsReady = false; + mCredentials = hsConfig.getCredentials(); + + mHandlerThread = new HandlerThread("MXFileStoreBackgroundThread_" + mCredentials.userId, Thread.MIN_PRIORITY); + + createDirTree(mCredentials.userId); + + // updated data + mRoomsToCommitForMessages = new HashSet<>(); + mRoomsToCommitForStates = new HashSet<>(); + //mRoomsToCommitForStatesEvents = new HashSet<>(); + mRoomsToCommitForSummaries = new HashSet<>(); + mRoomsToCommitForAccountData = new HashSet<>(); + mRoomsToCommitForReceipts = new HashSet<>(); + mUserIdsToCommit = new HashSet<>(); + mGroupsToCommit = new HashSet<>(); + + // check if the metadata file exists and if it is valid + loadMetaData(); + + if (null == mMetadata) { + deleteAllData(true); + } + + // create the metadata file if it does not exist + // either there is no store + // or the store was not properly initialised (the application crashed during the initialsync) + if ((null == mMetadata) || (null == mMetadata.mAccessToken)) { + mIsNewStorage = true; + mIsOpening = true; + mHandlerThread.start(); + mFileStoreHandler = new MXOsHandler(mHandlerThread.getLooper()); + + mMetadata = new MXFileStoreMetaData(); + mMetadata.mUserId = mCredentials.userId; + mMetadata.mAccessToken = mCredentials.accessToken; + mMetadata.mVersion = MXFILE_VERSION; + mMetaDataHasChanged = true; + saveMetaData(); + + mEventStreamToken = null; + + mIsOpening = false; + // nothing to load so ready to work + mIsReady = true; + mAreReceiptsReady = true; + } + } + + /** + * Killed the background thread. + * + * @param isKilled killed status + */ + private void setIsKilled(boolean isKilled) { + synchronized (this) { + mIsKilled = isKilled; + } + } + + /** + * @return true if the background thread is killed. + */ + private boolean isKilled() { + boolean isKilled; + + synchronized (this) { + isKilled = mIsKilled; + } + + return isKilled; + } + + /** + * 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. + */ + @Override + public void commit() { + // Save data only if metaData exists + if ((null != mMetadata) && (null != mMetadata.mAccessToken) && !isKilled()) { + Log.d(LOG_TAG, "++ Commit"); + saveUsers(); + saveGroups(); + saveRoomsMessages(); + saveRoomStates(); + saveRoomStatesEvents(); + saveSummaries(); + saveRoomsAccountData(); + saveReceipts(); + saveMetaData(); + Log.d(LOG_TAG, "-- Commit"); + } + } + + /** + * Open the store. + */ + @Override + public void open() { + super.open(); + final long fLoadTimeT0 = System.currentTimeMillis(); + + // avoid concurrency call. + synchronized (this) { + if (!mIsReady && !mIsOpening && (null != mMetadata) && (null != mHandlerThread)) { + mIsOpening = true; + + Log.e(LOG_TAG, "Open the store."); + + // creation the background handler. + if (null == mFileStoreHandler) { + // avoid already started exception + // never succeeded to reproduce but it was reported in GA. + try { + mHandlerThread.start(); + } catch (IllegalThreadStateException e) { + Log.e(LOG_TAG, "mHandlerThread is already started.", e); + // already started + return; + } + mFileStoreHandler = new MXOsHandler(mHandlerThread.getLooper()); + } + + Runnable r = new Runnable() { + @Override + public void run() { + mFileStoreHandler.post(new Runnable() { + public void run() { + Log.e(LOG_TAG, "Open the store in the background thread."); + + String errorDescription = null; + boolean succeed = (mMetadata.mVersion == MXFILE_VERSION) + && TextUtils.equals(mMetadata.mUserId, mCredentials.userId) + && TextUtils.equals(mMetadata.mAccessToken, mCredentials.accessToken); + + if (!succeed) { + errorDescription = "Invalid store content"; + Log.e(LOG_TAG, errorDescription); + } + + if (succeed) { + succeed &= loadRoomsMessages(); + if (!succeed) { + errorDescription = "loadRoomsMessages fails"; + Log.e(LOG_TAG, errorDescription); + } else { + Log.d(LOG_TAG, "loadRoomsMessages succeeds"); + } + } + + if (succeed) { + succeed &= loadGroups(); + if (!succeed) { + errorDescription = "loadGroups fails"; + Log.e(LOG_TAG, errorDescription); + } else { + Log.d(LOG_TAG, "loadGroups succeeds"); + } + } + + if (succeed) { + succeed &= loadRoomsState(); + + if (!succeed) { + errorDescription = "loadRoomsState fails"; + Log.e(LOG_TAG, errorDescription); + } else { + Log.d(LOG_TAG, "loadRoomsState succeeds"); + long t0 = System.currentTimeMillis(); + Log.d(LOG_TAG, "Retrieve the users from the roomstate"); + + Collection rooms = getRooms(); + + for (Room room : rooms) { + Collection members = room.getState().getLoadedMembers(); + for (RoomMember member : members) { + updateUserWithRoomMemberEvent(member); + } + } + + long delta = System.currentTimeMillis() - t0; + Log.d(LOG_TAG, "Retrieve " + mUsers.size() + " users with the room states in " + delta + " ms"); + mStoreStats.put("Retrieve users", delta); + } + } + + if (succeed) { + succeed &= loadSummaries(); + + if (!succeed) { + errorDescription = "loadSummaries fails"; + Log.e(LOG_TAG, errorDescription); + } else { + Log.d(LOG_TAG, "loadSummaries succeeds"); + + // Check if the room summaries match to existing rooms. + // We could have more rooms than summaries because + // some of them are hidden. + // For example, the conference calls create a dummy room to manage + // the call events. + // check also if the user is a member of the room + // https://github.com/vector-im/riot-android/issues/1302 + + for (String roomId : mRoomSummaries.keySet()) { + Room room = getRoom(roomId); + + if (null == room) { + succeed = false; + Log.e(LOG_TAG, "loadSummaries : the room " + roomId + " does not exist"); + } else if (null == room.getMember(mCredentials.userId)) { + //succeed = false; + Log.e(LOG_TAG, "loadSummaries) : a summary exists for the roomId " + + roomId + " but the user is not anymore a member"); + } + } + } + } + + if (succeed) { + succeed &= loadRoomsAccountData(); + + if (!succeed) { + errorDescription = "loadRoomsAccountData fails"; + Log.e(LOG_TAG, errorDescription); + } else { + Log.d(LOG_TAG, "loadRoomsAccountData succeeds"); + } + } + + // do not expect having empty list + // assume that something is corrupted + if (!succeed) { + Log.e(LOG_TAG, "Fail to open the store in background"); + + // delete all data set mMetadata to null + // backup it to restore it + // the behaviour should be the same as first login + MXFileStoreMetaData tmpMetadata = mMetadata; + + deleteAllData(true); + + mRoomsToCommitForMessages = new HashSet<>(); + mRoomsToCommitForStates = new HashSet<>(); + //mRoomsToCommitForStatesEvents = new HashSet<>(); + mRoomsToCommitForSummaries = new HashSet<>(); + mRoomsToCommitForReceipts = new HashSet<>(); + + mMetadata = tmpMetadata; + + // reported by GA + // i don't see which path could have triggered this issue + // mMetadata should only be null at file store loading + if (null == mMetadata) { + mMetadata = new MXFileStoreMetaData(); + mMetadata.mUserId = mCredentials.userId; + mMetadata.mAccessToken = mCredentials.accessToken; + mMetaDataHasChanged = true; + } else { + mMetadata.mEventStreamToken = null; + } + mMetadata.mVersion = MXFILE_VERSION; + + // the event stream token is put to zero to ensure ta + mEventStreamToken = null; + mAreReceiptsReady = true; + } else { + Log.d(LOG_TAG, "++ store stats"); + Set roomIds = mRoomEvents.keySet(); + + for (String roomId : roomIds) { + Room room = getRoom(roomId); + + if ((null != room) && (null != room.getState())) { + int membersCount = room.getState().getLoadedMembers().size(); + int eventsCount = mRoomEvents.get(roomId).size(); + + Log.d(LOG_TAG, " room " + roomId + + " : (lazy loaded) membersCount " + membersCount + + " - eventsCount " + eventsCount); + } + } + + Log.d(LOG_TAG, "-- store stats"); + } + + // post processing + Log.d(LOG_TAG, "## open() : post processing."); + dispatchPostProcess(mCredentials.userId); + mIsPostProcessingDone = true; + + synchronized (this) { + mIsReady = true; + } + mIsOpening = false; + + if (!succeed && !mIsNewStorage) { + Log.e(LOG_TAG, "The store is corrupted."); + dispatchOnStoreCorrupted(mCredentials.userId, errorDescription); + } else { + // extract the room states + mRoomReceiptsToLoad.addAll(listFiles(mStoreRoomsMessagesReceiptsFolderFile.list())); + mPreloadTime = System.currentTimeMillis() - fLoadTimeT0; + if (mMetricsListener != null) { + mMetricsListener.onStorePreloaded(mPreloadTime); + } + + Log.d(LOG_TAG, "The store is opened."); + dispatchOnStoreReady(mCredentials.userId); + + // load the following items with delay + // theses items are not required to be ready + + // load the receipts + loadReceipts(); + + // load the users + loadUsers(); + } + } + }); + } + }; + + Thread t = new Thread(r); + t.start(); + } else if (mIsReady) { + Runnable r = new Runnable() { + @Override + public void run() { + mFileStoreHandler.post(new Runnable() { + @Override + public void run() { + // should never happen + if (!mIsPostProcessingDone && !mIsNewStorage) { + Log.e(LOG_TAG, "## open() : is ready but the post processing was not yet done : please wait...."); + return; + } else { + if (!mIsPostProcessingDone) { + Log.e(LOG_TAG, "## open() : is ready but the post processing was not yet done."); + dispatchPostProcess(mCredentials.userId); + mIsPostProcessingDone = true; + } else { + Log.e(LOG_TAG, "## open() when ready : the post processing is already done."); + } + dispatchOnStoreReady(mCredentials.userId); + mPreloadTime = System.currentTimeMillis() - fLoadTimeT0; + if (mMetricsListener != null) { + mMetricsListener.onStorePreloaded(mPreloadTime); + } + } + } + }); + } + }; + + Thread t = new Thread(r); + t.start(); + } + + } + } + + /** + * Check if the read receipts are ready to be used. + * + * @return true if they are ready. + */ + @Override + public boolean areReceiptsReady() { + boolean res; + + synchronized (this) { + res = mAreReceiptsReady; + } + + return res; + } + + /** + * Provides the store preload time in milliseconds. + * + * @return the store preload time in milliseconds. + */ + @Override + public long getPreloadTime() { + return mPreloadTime; + } + + /** + * Provides some store stats + * + * @return the store stats + */ + public Map getStats() { + return mStoreStats; + } + + /** + * Close the store. + * Any pending operation must be complete in this call. + */ + @Override + public void close() { + Log.d(LOG_TAG, "Close the store"); + + super.close(); + setIsKilled(true); + if (null != mHandlerThread) { + mHandlerThread.quit(); + } + mHandlerThread = null; + } + + /** + * Clear the store. + * Any pending operation must be complete in this call. + */ + @Override + public void clear() { + Log.d(LOG_TAG, "Clear the store"); + super.clear(); + deleteAllData(false); + } + + /** + * Clear the filesystem storage. + * + * @param init true to init the filesystem dirtree + */ + private void deleteAllData(boolean init) { + // delete the dedicated directories + try { + ContentUtils.deleteDirectory(mStoreFolderFile); + if (init) { + createDirTree(mCredentials.userId); + } + } catch (Exception e) { + Log.e(LOG_TAG, "deleteAllData failed " + e.getMessage(), e); + } + + if (init) { + initCommon(); + } + mMetadata = null; + mEventStreamToken = null; + mAreUsersLoaded = true; + } + + /** + * Indicate if the MXStore implementation stores data permanently. + * Permanent storage allows the SDK to make less requests at the startup. + * + * @return true if permanent. + */ + @Override + public boolean isPermanent() { + return true; + } + + /** + * Check if the initial load is performed. + * + * @return true if it is ready. + */ + @Override + public boolean isReady() { + synchronized (this) { + return mIsReady; + } + } + + /** + * @return true if the store is corrupted. + */ + @Override + public boolean isCorrupted() { + return false; + } + + /** + * Delete a directory with its content + * + * @param directory the base directory + * @return the cache file size + */ + private long directorySize(File directory) { + long directorySize = 0; + + if (directory.exists()) { + File[] files = directory.listFiles(); + + if (null != files) { + for (int i = 0; i < files.length; i++) { + if (files[i].isDirectory()) { + directorySize += directorySize(files[i]); + } else { + directorySize += files[i].length(); + } + } + } + } + + return directorySize; + } + + /** + * Returns to disk usage size in bytes. + * + * @return disk usage size + */ + @Override + public long diskUsage() { + return directorySize(mStoreFolderFile); + } + + /** + * Set the event stream token. + * + * @param token the event stream token + */ + @Override + public void setEventStreamToken(String token) { + Log.d(LOG_TAG, "Set token to " + token); + super.setEventStreamToken(token); + mMetaDataHasChanged = true; + } + + @Override + public boolean setDisplayName(String displayName, long ts) { + return mMetaDataHasChanged = super.setDisplayName(displayName, ts); + } + + @Override + public boolean setAvatarURL(String avatarURL, long ts) { + return mMetaDataHasChanged = super.setAvatarURL(avatarURL, ts); + } + + @Override + public void setThirdPartyIdentifiers(List identifiers) { + // privacy + //Log.d(LOG_TAG, "Set setThirdPartyIdentifiers to " + identifiers); + Log.d(LOG_TAG, "Set setThirdPartyIdentifiers"); + mMetaDataHasChanged = true; + super.setThirdPartyIdentifiers(identifiers); + } + + @Override + public void setIgnoredUserIdsList(List users) { + Log.d(LOG_TAG, "## setIgnoredUsers() : " + users); + mMetaDataHasChanged = true; + super.setIgnoredUserIdsList(users); + } + + @Override + public void setDirectChatRoomsDict(Map> directChatRoomsDict) { + Log.d(LOG_TAG, "## setDirectChatRoomsDict()"); + mMetaDataHasChanged = true; + super.setDirectChatRoomsDict(directChatRoomsDict); + } + + @Override + public void storeUser(User user) { + if (!TextUtils.equals(mCredentials.userId, user.user_id)) { + mUserIdsToCommit.add(user.user_id); + } + super.storeUser(user); + } + + @Override + public void flushRoomEvents(String roomId) { + super.flushRoomEvents(roomId); + + mRoomsToCommitForMessages.add(roomId); + + if ((null != mMetadata) && (null != mMetadata.mAccessToken) && !isKilled()) { + saveRoomsMessages(); + } + } + + @Override + public void storeRoomEvents(String roomId, TokensChunkEvents tokensChunkEvents, EventTimeline.Direction direction) { + boolean canStore = true; + + // do not flush the room messages file + // when the user reads the room history and the events list size reaches its max size. + if (direction == EventTimeline.Direction.BACKWARDS) { + LinkedHashMap events = mRoomEvents.get(roomId); + + if (null != events) { + canStore = (events.size() < MAX_STORED_MESSAGES_COUNT); + + if (!canStore) { + Log.d(LOG_TAG, "storeRoomEvents : do not flush because reaching the max size"); + } + } + } + + super.storeRoomEvents(roomId, tokensChunkEvents, direction); + + if (canStore) { + mRoomsToCommitForMessages.add(roomId); + } + } + + /** + * Store a live room event. + * + * @param event The event to be stored. + */ + @Override + public void storeLiveRoomEvent(Event event) { + super.storeLiveRoomEvent(event); + mRoomsToCommitForMessages.add(event.roomId); + } + + @Override + public void deleteEvent(Event event) { + super.deleteEvent(event); + mRoomsToCommitForMessages.add(event.roomId); + } + + /** + * Delete the room messages and token files. + * + * @param roomId the room id. + */ + private void deleteRoomMessagesFiles(String roomId) { + // messages list + File messagesListFile = new File(mGzStoreRoomsMessagesFolderFile, roomId); + + // remove the files + if (messagesListFile.exists()) { + try { + messagesListFile.delete(); + } catch (Exception e) { + Log.e(LOG_TAG, "deleteRoomMessagesFiles - messagesListFile failed " + e.getMessage(), e); + } + } + + File tokenFile = new File(mStoreRoomsTokensFolderFile, roomId); + if (tokenFile.exists()) { + try { + tokenFile.delete(); + } catch (Exception e) { + Log.e(LOG_TAG, "deleteRoomMessagesFiles - tokenFile failed " + e.getMessage(), e); + } + } + } + + @Override + public void deleteRoom(String roomId) { + Log.d(LOG_TAG, "deleteRoom " + roomId); + + super.deleteRoom(roomId); + deleteRoomMessagesFiles(roomId); + deleteRoomStateFile(roomId); + deleteRoomSummaryFile(roomId); + deleteRoomReceiptsFile(roomId); + deleteRoomAccountDataFile(roomId); + } + + @Override + public void deleteAllRoomMessages(String roomId, boolean keepUnsent) { + Log.d(LOG_TAG, "deleteAllRoomMessages " + roomId); + + super.deleteAllRoomMessages(roomId, keepUnsent); + if (!keepUnsent) { + deleteRoomMessagesFiles(roomId); + } + + deleteRoomSummaryFile(roomId); + + mRoomsToCommitForMessages.add(roomId); + mRoomsToCommitForSummaries.add(roomId); + } + + @Override + public void storeLiveStateForRoom(String roomId) { + super.storeLiveStateForRoom(roomId); + mRoomsToCommitForStates.add(roomId); + } + + //================================================================================ + // Summary management + //================================================================================ + + @Override + public void flushSummary(RoomSummary summary) { + super.flushSummary(summary); + mRoomsToCommitForSummaries.add(summary.getRoomId()); + + if ((null != mMetadata) && (null != mMetadata.mAccessToken) && !isKilled()) { + saveSummaries(); + } + } + + @Override + public void flushSummaries() { + super.flushSummaries(); + + // add any existing roomid to the list to save all + mRoomsToCommitForSummaries.addAll(mRoomSummaries.keySet()); + + if ((null != mMetadata) && (null != mMetadata.mAccessToken) && !isKilled()) { + saveSummaries(); + } + } + + @Override + public void storeSummary(RoomSummary summary) { + super.storeSummary(summary); + + if ((null != summary) && (null != summary.getRoomId()) && !mRoomsToCommitForSummaries.contains(summary.getRoomId())) { + mRoomsToCommitForSummaries.add(summary.getRoomId()); + } + } + + //================================================================================ + // users management + //================================================================================ + + /** + * Flush users list + */ + private void saveUsers() { + if (!mAreUsersLoaded) { + // please wait + return; + } + + // some updated rooms ? + if ((mUserIdsToCommit.size() > 0) && (null != mFileStoreHandler)) { + // get the list + final Set fUserIds = mUserIdsToCommit; + mUserIdsToCommit = new HashSet<>(); + + try { + final Set fUsers; + + synchronized (mUsers) { + fUsers = new HashSet<>(mUsers.values()); + } + + Runnable r = new Runnable() { + @Override + public void run() { + mFileStoreHandler.post(new Runnable() { + public void run() { + if (!isKilled()) { + Log.d(LOG_TAG, "saveUsers " + fUserIds.size() + " users (" + fUsers.size() + " known ones)"); + + long start = System.currentTimeMillis(); + + // the users are split into groups to save time + Map> usersGroups = new HashMap<>(); + + // finds the group for each updated user + for (String userId : fUserIds) { + User user; + + synchronized (mUsers) { + user = mUsers.get(userId); + } + + if (null != user) { + int hashCode = user.getStorageHashKey(); + + if (!usersGroups.containsKey(hashCode)) { + usersGroups.put(hashCode, new ArrayList()); + } + } + } + + // gather the user to the dedicated group if they need to be updated + for (User user : fUsers) { + if (usersGroups.containsKey(user.getStorageHashKey())) { + usersGroups.get(user.getStorageHashKey()).add(user); + } + } + + // save the groups + for (int hashKey : usersGroups.keySet()) { + writeObject("saveUser " + hashKey, new File(mStoreUserFolderFile, hashKey + ""), usersGroups.get(hashKey)); + } + + Log.d(LOG_TAG, "saveUsers done in " + (System.currentTimeMillis() - start) + " ms"); + } + } + }); + } + }; + + Thread t = new Thread(r); + t.start(); + } catch (OutOfMemoryError oom) { + Log.e(LOG_TAG, "saveUser : cannot clone the users list" + oom.getMessage(), oom); + } + } + } + + /** + * Load the user information from the filesystem.. + */ + private void loadUsers() { + List filenames = listFiles(mStoreUserFolderFile.list()); + long start = System.currentTimeMillis(); + + List users = new ArrayList<>(); + + // list the files + for (String filename : filenames) { + File messagesListFile = new File(mStoreUserFolderFile, filename); + Object usersAsVoid = readObject("loadUsers " + filename, messagesListFile); + + if (null != usersAsVoid) { + try { + users.addAll((List) usersAsVoid); + } catch (Exception e) { + Log.e(LOG_TAG, "loadUsers failed : " + e.toString(), e); + } + } + } + + // update the hash map + for (User user : users) { + synchronized (mUsers) { + User currentUser = mUsers.get(user.user_id); + + if ((null == currentUser) || // not defined + currentUser.isRetrievedFromRoomMember() || // tmp user until retrieved it + (currentUser.getLatestPresenceTs() < user.getLatestPresenceTs())) // newer presence + { + mUsers.put(user.user_id, user); + } + } + } + + long delta = (System.currentTimeMillis() - start); + Log.e(LOG_TAG, "loadUsers (" + filenames.size() + " files) : retrieve " + mUsers.size() + " users in " + delta + "ms"); + mStoreStats.put("loadUsers", delta); + + mAreUsersLoaded = true; + + // save any pending save + saveUsers(); + } + + //================================================================================ + // Room messages management + //================================================================================ + + /** + * Computes the saved events map to reduce storage footprint. + * + * @param roomId the room id + * @return the saved eventMap + */ + private LinkedHashMap getSavedEventsMap(String roomId) { + LinkedHashMap eventsMap; + + synchronized (mRoomEventsLock) { + eventsMap = mRoomEvents.get(roomId); + } + + List eventsList; + + synchronized (mRoomEventsLock) { + eventsList = new ArrayList<>(eventsMap.values()); + } + + int startIndex = 0; + + // try to reduce the number of stored messages + // it does not make sense to keep the full history. + + // the method consists in saving messages until finding the oldest known token. + // At initial sync, it is not saved so keep the whole history. + // if the user back paginates, the token is stored in the event. + // if some messages are received, the token is stored in the event. + if (eventsList.size() > MAX_STORED_MESSAGES_COUNT) { + // search backward the first known token + for (startIndex = eventsList.size() - MAX_STORED_MESSAGES_COUNT; !eventsList.get(startIndex).hasToken() && (startIndex > 0); startIndex--) + ; + + if (startIndex > 0) { + Log.d(LOG_TAG, "## getSavedEveventsMap() : " + roomId + " reduce the number of messages " + eventsList.size() + + " -> " + (eventsList.size() - startIndex)); + } + } + + LinkedHashMap savedEvents = new LinkedHashMap<>(); + + for (int index = startIndex; index < eventsList.size(); index++) { + Event event = eventsList.get(index); + savedEvents.put(event.eventId, event); + } + + return savedEvents; + } + + private void saveRoomMessages(String roomId) { + LinkedHashMap eventsHash; + synchronized (mRoomEventsLock) { + eventsHash = mRoomEvents.get(roomId); + } + + String token = mRoomTokens.get(roomId); + + // the list exists ? + if ((null != eventsHash) && (null != token)) { + long t0 = System.currentTimeMillis(); + + LinkedHashMap savedEventsMap = getSavedEventsMap(roomId); + + if (!writeObject("saveRoomsMessage " + roomId, new File(mGzStoreRoomsMessagesFolderFile, roomId), savedEventsMap)) { + return; + } + + if (!writeObject("saveRoomsMessage " + roomId, new File(mStoreRoomsTokensFolderFile, roomId), token)) { + return; + } + + Log.d(LOG_TAG, "saveRoomsMessage (" + roomId + ") : " + savedEventsMap.size() + " messages saved in " + (System.currentTimeMillis() - t0) + " ms"); + } else { + deleteRoomMessagesFiles(roomId); + } + } + + /** + * Flush updates rooms messages list files. + */ + private void saveRoomsMessages() { + // some updated rooms ? + if ((mRoomsToCommitForMessages.size() > 0) && (null != mFileStoreHandler)) { + // get the list + final Set fRoomsToCommitForMessages = mRoomsToCommitForMessages; + mRoomsToCommitForMessages = new HashSet<>(); + + Runnable r = new Runnable() { + @Override + public void run() { + mFileStoreHandler.post(new Runnable() { + public void run() { + if (!isKilled()) { + long start = System.currentTimeMillis(); + + for (String roomId : fRoomsToCommitForMessages) { + saveRoomMessages(roomId); + } + + Log.d(LOG_TAG, "saveRoomsMessages : " + fRoomsToCommitForMessages.size() + " rooms in " + + (System.currentTimeMillis() - start) + " ms"); + } + } + }); + } + }; + + Thread t = new Thread(r); + t.start(); + } + } + + /** + * Load room messages from the filesystem. + * + * @param roomId the room id. + * @return true if succeed. + */ + private boolean loadRoomMessages(final String roomId) { + boolean succeeded = true; + boolean shouldSave = false; + LinkedHashMap events = null; + + File messagesListFile = new File(mGzStoreRoomsMessagesFolderFile, roomId); + + if (messagesListFile.exists()) { + Object eventsAsVoid = readObject("events " + roomId, messagesListFile); + + if (null != eventsAsVoid) { + try { + events = (LinkedHashMap) eventsAsVoid; + } catch (Exception e) { + Log.e(LOG_TAG, "loadRoomMessages " + roomId + "failed : " + e.getMessage(), e); + return false; + } + + if (events.size() > (2 * MAX_STORED_MESSAGES_COUNT)) { + Log.d(LOG_TAG, "## loadRoomMessages() : the room " + roomId + " has " + events.size() + + " stored events : we need to find a way to reduce it."); + } + + // finalizes the deserialization + for (Event event : events.values()) { + // if a message was not sent, mark it as UNDELIVERED + if ((event.mSentState == Event.SentState.UNSENT) + || (event.mSentState == Event.SentState.SENDING) + || (event.mSentState == Event.SentState.WAITING_RETRY) + || (event.mSentState == Event.SentState.ENCRYPTING)) { + event.mSentState = Event.SentState.UNDELIVERED; + shouldSave = true; + } + } + } else { + return false; + } + } + + // succeeds to extract the message list + if (null != events) { + // create the room object + final Room room = new Room(getDataHandler(), this, roomId); + // do not wait that the live state update + room.setReadyState(true); + storeRoom(room); + + mRoomEvents.put(roomId, events); + } + + if (shouldSave) { + saveRoomMessages(roomId); + } + + return succeeded; + } + + /** + * Load the room token from the file system. + * + * @param roomId the room id. + * @return true if it succeeds. + */ + private boolean loadRoomToken(final String roomId) { + boolean succeed = true; + + Room room = getRoom(roomId); + + // should always be true + if (null != room) { + String token = null; + + try { + File messagesListFile = new File(mStoreRoomsTokensFolderFile, roomId); + Object tokenAsVoid = readObject("loadRoomToken " + roomId, messagesListFile); + + if (null == tokenAsVoid) { + succeed = false; + } else { + token = (String) tokenAsVoid; + + // check if the oldest event has a token. + LinkedHashMap eventsHash = mRoomEvents.get(roomId); + if ((null != eventsHash) && (eventsHash.size() > 0)) { + Event event = eventsHash.values().iterator().next(); + + // the room history could have been reduced to save memory + // so, if the oldest messages has a token, use it instead of the stored token. + if (null != event.mToken) { + token = event.mToken; + } + } + } + } catch (Exception e) { + succeed = false; + Log.e(LOG_TAG, "loadRoomToken failed : " + e.toString(), e); + } + + if (null != token) { + mRoomTokens.put(roomId, token); + } else { + deleteRoom(roomId); + } + } else { + try { + File messagesListFile = new File(mStoreRoomsTokensFolderFile, roomId); + messagesListFile.delete(); + } catch (Exception e) { + Log.e(LOG_TAG, "loadRoomToken failed with error " + e.getMessage(), e); + } + } + + return succeed; + } + + /** + * Load room messages from the filesystem. + * + * @return true if the operation succeeds. + */ + private boolean loadRoomsMessages() { + boolean succeed = true; + + try { + // extract the messages list + List filenames = listFiles(mGzStoreRoomsMessagesFolderFile.list()); + + long start = System.currentTimeMillis(); + + for (String filename : filenames) { + if (succeed) { + succeed &= loadRoomMessages(filename); + } + } + + if (succeed) { + long delta = (System.currentTimeMillis() - start); + Log.d(LOG_TAG, "loadRoomMessages : " + filenames.size() + " rooms in " + delta + " ms"); + mStoreStats.put("loadRoomMessages", delta); + } + + // extract the tokens list + filenames = listFiles(mStoreRoomsTokensFolderFile.list()); + + start = System.currentTimeMillis(); + + for (String filename : filenames) { + if (succeed) { + succeed &= loadRoomToken(filename); + } + } + + if (succeed) { + Log.d(LOG_TAG, "loadRoomToken : " + filenames.size() + " rooms in " + (System.currentTimeMillis() - start) + " ms"); + } + + } catch (Exception e) { + succeed = false; + Log.e(LOG_TAG, "loadRoomToken failed : " + e.getMessage(), e); + } + + return succeed; + } + + //================================================================================ + // Room states management + //================================================================================ + + // waiting that the rooms state events are loaded + private Map> mPendingRoomStateEvents = new HashMap<>(); + + @Override + public void storeRoomStateEvent(final String roomId, final Event event) { + /*boolean isAlreadyLoaded = true; + + synchronized (mRoomStateEventsByRoomId) { + isAlreadyLoaded = mRoomStateEventsByRoomId.containsKey(roomId); + } + + if (isAlreadyLoaded) { + super.storeRoomStateEvent(roomId, event); + mRoomsToCommitForStatesEvents.add(roomId); + return; + } + + boolean isRequestPending = false; + + synchronized (mPendingRoomStateEvents) { + // a loading is already in progress + if (mPendingRoomStateEvents.containsKey(roomId)) { + mPendingRoomStateEvents.get(roomId).add(event); + isRequestPending = true; + } + } + + if (isRequestPending) { + return; + } + + synchronized (mPendingRoomStateEvents) { + List events = new ArrayList(); + events.add(event); + mPendingRoomStateEvents.put(roomId, events); + } + + getRoomStateEvents(roomId, new SimpleApiCallback>() { + @Override + public void onSuccess(List events) { + List pendingEvents; + + synchronized (mPendingRoomStateEvents) { + pendingEvents = mPendingRoomStateEvents.get(roomId); + mPendingRoomStateEvents.remove(roomId); + } + + // add them by now + for (Event event : pendingEvents) { + storeRoomStateEvent(roomId, event); + } + } + });*/ + } + + /** + * Save the room state. + * + * @param roomId the room id. + */ + private void saveRoomStateEvents(final String roomId) { + /*Log.d(LOG_TAG, "++ saveRoomStateEvents " + roomId); + + File roomStateFile = new File(mGzStoreRoomsStateEventsFolderFile, roomId); + Map eventsMap = mRoomStateEventsByRoomId.get(roomId); + + if (null != eventsMap) { + List events = new ArrayList<>(eventsMap.values()); + + long start1 = System.currentTimeMillis(); + writeObject("saveRoomStateEvents " + roomId, roomStateFile, events); + Log.d(LOG_TAG, "saveRoomStateEvents " + roomId + " :" + events.size() + " events : " + (System.currentTimeMillis() - start1) + " ms"); + } else { + Log.d(LOG_TAG, "-- saveRoomStateEvents " + roomId + " : empty list"); + }*/ + } + + /** + * Flush the room state events files. + */ + private void saveRoomStatesEvents() { + /*if ((mRoomsToCommitForStatesEvents.size() > 0) && (null != mFileStoreHandler)) { + // get the list + final Set fRoomsToCommitForStatesEvents = new HashSet<>(mRoomsToCommitForStatesEvents); + mRoomsToCommitForStatesEvents = new HashSet<>(); + + Runnable r = new Runnable() { + @Override + public void run() { + mFileStoreHandler.post(new Runnable() { + public void run() { + if (!isKilled()) { + long start = System.currentTimeMillis(); + + for (String roomId : fRoomsToCommitForStatesEvents) { + saveRoomStateEvents(roomId); + } + + Log.d(LOG_TAG, "saveRoomStatesEvents : " + fRoomsToCommitForStatesEvents.size() + " rooms in " + + (System.currentTimeMillis() - start) + " ms"); + } + } + }); + } + }; + + Thread t = new Thread(r); + t.start(); + }*/ + } + + @Override + public void getRoomStateEvents(final String roomId, final ApiCallback> callback) { + boolean isAlreadyLoaded = true; + + /*synchronized (mRoomStateEventsByRoomId) { + isAlreadyLoaded = mRoomStateEventsByRoomId.containsKey(roomId); + }*/ + + if (isAlreadyLoaded) { + super.getRoomStateEvents(roomId, callback); + return; + } + + /*Runnable r = new Runnable() { + @Override + public void run() { + mFileStoreHandler.post(new Runnable() { + public void run() { + if (!isKilled()) { + File statesEventsFile = new File(mGzStoreRoomsStateEventsFolderFile, roomId); + Map eventsMap = new HashMap<>(); + List eventsList = new ArrayList<>(); + + long start = System.currentTimeMillis(); + + if ((null != statesEventsFile) && statesEventsFile.exists()) { + try { + Object eventsListAsVoid = readObject("getRoomStateEvents", statesEventsFile); + + if (null != eventsListAsVoid) { + List events = (List) eventsListAsVoid; + + for (Event event : events) { + eventsMap.put(event.stateKey, event); + } + } + } catch (Exception e) { + Log.e(LOG_TAG, "getRoomStateEvents failed : " + e.getMessage(), e); + } + } + + synchronized (mRoomStateEventsByRoomId) { + mRoomStateEventsByRoomId.put(roomId, eventsMap); + } + + Log.d(LOG_TAG, "getRoomStateEvents : retrieve " + eventsList.size() + " events in " + (System.currentTimeMillis() - start) + " ms"); + callback.onSuccess(eventsList); + } + } + }); + } + }; + + Thread t = new Thread(r); + t.start();*/ + } + + /** + * Delete the room state file. + * + * @param roomId the room id. + */ + private void deleteRoomStateFile(String roomId) { + // states list + File statesFile = new File(mGzStoreRoomsStateFolderFile, roomId); + + if (statesFile.exists()) { + try { + statesFile.delete(); + } catch (Exception e) { + Log.e(LOG_TAG, "deleteRoomStateFile failed with error " + e.getMessage(), e); + } + } + + File statesEventsFile = new File(mGzStoreRoomsStateEventsFolderFile, roomId); + + if (statesEventsFile.exists()) { + try { + statesEventsFile.delete(); + } catch (Exception e) { + Log.e(LOG_TAG, "deleteRoomStateFile failed with error " + e.getMessage(), e); + } + } + } + + /** + * Save the room state. + * + * @param roomId the room id. + */ + private void saveRoomState(final String roomId) { + Log.d(LOG_TAG, "++ saveRoomsState " + roomId); + + File roomStateFile = new File(mGzStoreRoomsStateFolderFile, roomId); + Room room = mRooms.get(roomId); + + if (null != room) { + long start1 = System.currentTimeMillis(); + writeObject("saveRoomsState " + roomId, roomStateFile, room.getState()); + Log.d(LOG_TAG, "saveRoomsState " + room.getNumberOfMembers() + " members : " + (System.currentTimeMillis() - start1) + " ms"); + } else { + Log.d(LOG_TAG, "saveRoomsState : delete the room state"); + deleteRoomStateFile(roomId); + } + + Log.d(LOG_TAG, "-- saveRoomsState " + roomId); + } + + /** + * Flush the room state files. + */ + private void saveRoomStates() { + if ((mRoomsToCommitForStates.size() > 0) && (null != mFileStoreHandler)) { + // get the list + final Set fRoomsToCommitForStates = mRoomsToCommitForStates; + mRoomsToCommitForStates = new HashSet<>(); + + Runnable r = new Runnable() { + @Override + public void run() { + mFileStoreHandler.post(new Runnable() { + public void run() { + if (!isKilled()) { + long start = System.currentTimeMillis(); + + for (String roomId : fRoomsToCommitForStates) { + saveRoomState(roomId); + } + + Log.d(LOG_TAG, "saveRoomsState : " + fRoomsToCommitForStates.size() + " rooms in " + + (System.currentTimeMillis() - start) + " ms"); + } + } + }); + } + }; + + Thread t = new Thread(r); + t.start(); + } + } + + /** + * Load a room state from the file system. + * + * @param roomId the room id. + * @return true if the operation succeeds. + */ + private boolean loadRoomState(final String roomId) { + boolean succeed = true; + + Room room = getRoom(roomId); + + // should always be true + if (null != room) { + RoomState liveState = null; + + try { + // the room state is not zipped + File roomStateFile = new File(mGzStoreRoomsStateFolderFile, roomId); + + // new format + if (roomStateFile.exists()) { + Object roomStateAsObject = readObject("loadRoomState " + roomId, roomStateFile); + + if (null == roomStateAsObject) { + succeed = false; + } else { + liveState = (RoomState) roomStateAsObject; + } + } + } catch (Exception e) { + succeed = false; + Log.e(LOG_TAG, "loadRoomState failed : " + e.getMessage(), e); + } + + if (null != liveState) { + room.getTimeline().setState(liveState); + } else { + deleteRoom(roomId); + } + } else { + try { + File messagesListFile = new File(mGzStoreRoomsStateFolderFile, roomId); + messagesListFile.delete(); + + } catch (Exception e) { + Log.e(LOG_TAG, "loadRoomState failed to delete a file : " + e.getMessage(), e); + } + } + + return succeed; + } + + /** + * Load room state from the file system. + * + * @return true if the operation succeeds. + */ + private boolean loadRoomsState() { + boolean succeed = true; + + try { + long start = System.currentTimeMillis(); + + List filenames = listFiles(mGzStoreRoomsStateFolderFile.list()); + + for (String filename : filenames) { + if (succeed) { + succeed &= loadRoomState(filename); + } + } + + long delta = (System.currentTimeMillis() - start); + Log.d(LOG_TAG, "loadRoomsState " + filenames.size() + " rooms in " + delta + " ms"); + mStoreStats.put("loadRoomsState", delta); + + } catch (Exception e) { + succeed = false; + Log.e(LOG_TAG, "loadRoomsState failed : " + e.getMessage(), e); + } + + return succeed; + } + + //================================================================================ + // AccountData management + //================================================================================ + + /** + * Delete the room account data file. + * + * @param roomId the room id. + */ + private void deleteRoomAccountDataFile(String roomId) { + File file = new File(mStoreRoomsAccountDataFolderFile, roomId); + + // remove the files + if (file.exists()) { + try { + file.delete(); + } catch (Exception e) { + Log.e(LOG_TAG, "deleteRoomAccountDataFile failed : " + e.getMessage(), e); + } + } + } + + /** + * Flush the pending account data. + */ + private void saveRoomsAccountData() { + if ((mRoomsToCommitForAccountData.size() > 0) && (null != mFileStoreHandler)) { + // get the list + final Set fRoomsToCommitForAccountData = mRoomsToCommitForAccountData; + mRoomsToCommitForAccountData = new HashSet<>(); + + Runnable r = new Runnable() { + @Override + public void run() { + mFileStoreHandler.post(new Runnable() { + public void run() { + if (!isKilled()) { + long start = System.currentTimeMillis(); + + for (String roomId : fRoomsToCommitForAccountData) { + RoomAccountData accountData = mRoomAccountData.get(roomId); + + if (null != accountData) { + writeObject("saveRoomsAccountData " + roomId, new File(mStoreRoomsAccountDataFolderFile, roomId), accountData); + } else { + deleteRoomAccountDataFile(roomId); + } + } + + Log.d(LOG_TAG, "saveSummaries : " + fRoomsToCommitForAccountData.size() + " account data in " + + (System.currentTimeMillis() - start) + " ms"); + } + } + }); + } + }; + + Thread t = new Thread(r); + t.start(); + } + } + + /*** + * Load the account Data of a dedicated room. + * @param roomId the room Id + * @return true if the operation succeeds. + */ + private boolean loadRoomAccountData(final String roomId) { + boolean succeeded = true; + RoomAccountData roomAccountData = null; + + try { + File accountDataFile = new File(mStoreRoomsAccountDataFolderFile, roomId); + + if (accountDataFile.exists()) { + Object accountAsVoid = readObject("loadRoomAccountData " + roomId, accountDataFile); + + if (null == accountAsVoid) { + Log.e(LOG_TAG, "loadRoomAccountData failed"); + return false; + } + + roomAccountData = (RoomAccountData) accountAsVoid; + } + } catch (Exception e) { + succeeded = false; + Log.e(LOG_TAG, "loadRoomAccountData failed : " + e.toString(), e); + } + + // succeeds to extract the message list + if (null != roomAccountData) { + Room room = getRoom(roomId); + + if (null != room) { + room.setAccountData(roomAccountData); + } + } + + return succeeded; + } + + /** + * Load room accountData from the filesystem. + * + * @return true if the operation succeeds. + */ + private boolean loadRoomsAccountData() { + boolean succeed = true; + + try { + // extract the messages list + List filenames = listFiles(mStoreRoomsAccountDataFolderFile.list()); + + long start = System.currentTimeMillis(); + + for (String filename : filenames) { + succeed &= loadRoomAccountData(filename); + } + + if (succeed) { + Log.d(LOG_TAG, "loadRoomsAccountData : " + filenames.size() + " rooms in " + (System.currentTimeMillis() - start) + " ms"); + } + } catch (Exception e) { + succeed = false; + Log.e(LOG_TAG, "loadRoomsAccountData failed : " + e.getMessage(), e); + } + + return succeed; + } + + @Override + public void storeAccountData(String roomId, RoomAccountData accountData) { + super.storeAccountData(roomId, accountData); + + if (null != roomId) { + Room room = mRooms.get(roomId); + + // sanity checks + if ((room != null) && (null != accountData)) { + mRoomsToCommitForAccountData.add(roomId); + } + } + } + + //================================================================================ + // Summary management + //================================================================================ + + /** + * Delete the room summary file. + * + * @param roomId the room id. + */ + private void deleteRoomSummaryFile(String roomId) { + // states list + File statesFile = new File(mStoreRoomsSummaryFolderFile, roomId); + + // remove the files + if (statesFile.exists()) { + try { + statesFile.delete(); + } catch (Exception e) { + Log.e(LOG_TAG, "deleteRoomSummaryFile failed : " + e.getMessage(), e); + } + } + } + + /** + * Flush the pending summaries. + */ + private void saveSummaries() { + if ((mRoomsToCommitForSummaries.size() > 0) && (null != mFileStoreHandler)) { + // get the list + final Set fRoomsToCommitForSummaries = mRoomsToCommitForSummaries; + mRoomsToCommitForSummaries = new HashSet<>(); + + Runnable r = new Runnable() { + @Override + public void run() { + mFileStoreHandler.post(new Runnable() { + public void run() { + if (!isKilled()) { + long start = System.currentTimeMillis(); + + for (String roomId : fRoomsToCommitForSummaries) { + try { + File roomSummaryFile = new File(mStoreRoomsSummaryFolderFile, roomId); + RoomSummary roomSummary = mRoomSummaries.get(roomId); + + if (null != roomSummary) { + writeObject("saveSummaries " + roomId, roomSummaryFile, roomSummary); + } else { + deleteRoomSummaryFile(roomId); + } + } catch (OutOfMemoryError oom) { + dispatchOOM(oom); + } catch (Exception e) { + Log.e(LOG_TAG, "saveSummaries failed : " + e.getMessage(), e); + // Toast.makeText(mContext, "saveSummaries failed " + e.getLocalizedMessage(), Toast.LENGTH_LONG).show(); + } + } + + Log.d(LOG_TAG, "saveSummaries : " + fRoomsToCommitForSummaries.size() + " summaries in " + + (System.currentTimeMillis() - start) + " ms"); + } + } + }); + } + }; + + Thread t = new Thread(r); + t.start(); + } + } + + /** + * Load the room summary from the files system. + * + * @param roomId the room id. + * @return true if the operation succeeds; + */ + private boolean loadSummary(final String roomId) { + boolean succeed = true; + + // do not check if the room exists here. + // if the user is invited to a room, the room object is not created until it is joined. + RoomSummary summary = null; + + try { + File messagesListFile = new File(mStoreRoomsSummaryFolderFile, roomId); + Object summaryAsVoid = readObject("loadSummary " + roomId, messagesListFile); + + if (null == summaryAsVoid) { + Log.e(LOG_TAG, "loadSummary failed"); + return false; + } + + summary = (RoomSummary) summaryAsVoid; + } catch (Exception e) { + succeed = false; + Log.e(LOG_TAG, "loadSummary failed : " + e.getMessage(), e); + } + + if (null != summary) { + //summary.getLatestReceivedEvent().finalizeDeserialization(); + + Room room = getRoom(summary.getRoomId()); + + // the room state is not saved in the summary. + // it is restored from the room + if (null != room) { + summary.setLatestRoomState(room.getState()); + } + + mRoomSummaries.put(roomId, summary); + } + + return succeed; + } + + /** + * Load room summaries from the file system. + * + * @return true if the operation succeeds. + */ + private boolean loadSummaries() { + boolean succeed = true; + try { + // extract the room states + List filenames = listFiles(mStoreRoomsSummaryFolderFile.list()); + + long start = System.currentTimeMillis(); + + for (String filename : filenames) { + succeed &= loadSummary(filename); + } + + long delta = (System.currentTimeMillis() - start); + Log.d(LOG_TAG, "loadSummaries " + filenames.size() + " rooms in " + delta + " ms"); + mStoreStats.put("loadSummaries", delta); + } catch (Exception e) { + succeed = false; + Log.e(LOG_TAG, "loadSummaries failed : " + e.getMessage(), e); + } + + return succeed; + } + + //================================================================================ + // Metadata management + //================================================================================ + + /** + * Load the metadata info from the file system. + */ + private void loadMetaData() { + long start = System.currentTimeMillis(); + + // init members + mEventStreamToken = null; + mMetadata = null; + + File metaDataFile = new File(mStoreFolderFile, MXFILE_STORE_METADATA_FILE_NAME); + + if (metaDataFile.exists()) { + Object metadataAsVoid = readObject("loadMetaData", metaDataFile); + + if (null != metadataAsVoid) { + try { + mMetadata = (MXFileStoreMetaData) metadataAsVoid; + + // remove pending \n + if (null != mMetadata.mUserDisplayName) { + mMetadata.mUserDisplayName.trim(); + } + + // extract the latest event stream token + mEventStreamToken = mMetadata.mEventStreamToken; + } catch (Exception e) { + Log.e(LOG_TAG, "## loadMetaData() : is corrupted", e); + return; + } + } + } + + Log.d(LOG_TAG, "loadMetaData : " + (System.currentTimeMillis() - start) + " ms"); + } + + /** + * flush the metadata info from the file system. + */ + private void saveMetaData() { + if ((mMetaDataHasChanged) && (null != mFileStoreHandler) && (null != mMetadata)) { + mMetaDataHasChanged = false; + + final MXFileStoreMetaData fMetadata = mMetadata.deepCopy(); + + Runnable r = new Runnable() { + @Override + public void run() { + mFileStoreHandler.post(new Runnable() { + public void run() { + if (!mIsKilled) { + // save the metadata only when there is a current valid stream token + // avoid saving the metadata if the store has been cleared + if (null != mMetadata.mEventStreamToken) { + long start = System.currentTimeMillis(); + writeObject("saveMetaData", new File(mStoreFolderFile, MXFILE_STORE_METADATA_FILE_NAME), fMetadata); + Log.d(LOG_TAG, "saveMetaData : " + (System.currentTimeMillis() - start) + " ms"); + } else { + Log.e(LOG_TAG, "## saveMetaData() : cancelled because mEventStreamToken is null"); + } + } + } + }); + } + }; + + Thread t = new Thread(r); + t.start(); + } + } + + //================================================================================ + // Event receipts management + //================================================================================ + + @Override + public List getEventReceipts(String roomId, String eventId, boolean excludeSelf, boolean sort) { + synchronized (mRoomReceiptsToLoad) { + int pos = mRoomReceiptsToLoad.indexOf(roomId); + + // the user requires the receipts asap + if (pos >= 2) { + mRoomReceiptsToLoad.remove(roomId); + // index 0 is the current managed one + mRoomReceiptsToLoad.add(1, roomId); + } + } + + return super.getEventReceipts(roomId, eventId, excludeSelf, sort); + } + + /** + * Store the receipt for an user in a room + * + * @param receipt The event + * @param roomId The roomId + * @return true if the receipt has been stored + */ + @Override + public boolean storeReceipt(ReceiptData receipt, String roomId) { + boolean res = super.storeReceipt(receipt, roomId); + + if (res) { + synchronized (this) { + mRoomsToCommitForReceipts.add(roomId); + } + } + + return res; + } + + /*** + * Load the events receipts. + * @param roomId the room Id + * @return true if the operation succeeds. + */ + private boolean loadReceipts(String roomId) { + Map receiptsMap = null; + File file = new File(mStoreRoomsMessagesReceiptsFolderFile, roomId); + + if (file.exists()) { + Object receiptsAsVoid = readObject("loadReceipts " + roomId, file); + + if (null != receiptsAsVoid) { + try { + List receipts = (List) receiptsAsVoid; + + receiptsMap = new HashMap<>(); + + for (ReceiptData r : receipts) { + receiptsMap.put(r.userId, r); + } + } catch (Exception e) { + Log.e(LOG_TAG, "loadReceipts failed : " + e.getMessage(), e); + return false; + } + } else { + return false; + } + } + + if (null != receiptsMap) { + Map currentReceiptMap; + + synchronized (mReceiptsByRoomIdLock) { + currentReceiptMap = mReceiptsByRoomId.get(roomId); + mReceiptsByRoomId.put(roomId, receiptsMap); + } + + // merge the current read receipts + if (null != currentReceiptMap) { + Collection receipts = currentReceiptMap.values(); + + for (ReceiptData receipt : receipts) { + storeReceipt(receipt, roomId); + } + } + + dispatchOnReadReceiptsLoaded(roomId); + } + + return true; + } + + /** + * Load event receipts from the file system. + * + * @return true if the operation succeeds. + */ + private boolean loadReceipts() { + boolean succeed = true; + try { + int count = mRoomReceiptsToLoad.size(); + long start = System.currentTimeMillis(); + + while (mRoomReceiptsToLoad.size() > 0) { + String roomId; + synchronized (mRoomReceiptsToLoad) { + roomId = mRoomReceiptsToLoad.get(0); + } + + loadReceipts(roomId); + + synchronized (mRoomReceiptsToLoad) { + mRoomReceiptsToLoad.remove(0); + } + } + + saveReceipts(); + + long delta = (System.currentTimeMillis() - start); + Log.d(LOG_TAG, "loadReceipts " + count + " rooms in " + delta + " ms"); + mStoreStats.put("loadReceipts", delta); + } catch (Exception e) { + succeed = false; + //Toast.makeText(mContext, "loadReceipts failed" + e, Toast.LENGTH_LONG).show(); + Log.e(LOG_TAG, "loadReceipts failed : " + e.getMessage(), e); + } + + synchronized (this) { + mAreReceiptsReady = true; + } + + return succeed; + } + + /** + * Flush the events receipts + * + * @param roomId the roomId. + */ + private void saveReceipts(final String roomId) { + synchronized (mRoomReceiptsToLoad) { + // please wait + if (mRoomReceiptsToLoad.contains(roomId)) { + return; + } + } + + final List receipts; + + synchronized (mReceiptsByRoomIdLock) { + if (mReceiptsByRoomId.containsKey(roomId)) { + receipts = new ArrayList<>(mReceiptsByRoomId.get(roomId).values()); + } else { + receipts = null; + } + } + + // sanity check + if (null == receipts) { + return; + } + + Runnable r = new Runnable() { + @Override + public void run() { + mFileStoreHandler.post(new Runnable() { + public void run() { + if (!mIsKilled) { + long start = System.currentTimeMillis(); + writeObject("saveReceipts " + roomId, new File(mStoreRoomsMessagesReceiptsFolderFile, roomId), receipts); + Log.d(LOG_TAG, "saveReceipts : roomId " + roomId + " eventId : " + (System.currentTimeMillis() - start) + " ms"); + } + } + }); + } + }; + + Thread t = new Thread(r); + t.start(); + } + + /** + * Save the events receipts. + */ + private void saveReceipts() { + synchronized (this) { + Set roomsToCommit = mRoomsToCommitForReceipts; + + for (String roomId : roomsToCommit) { + saveReceipts(roomId); + } + + mRoomsToCommitForReceipts.clear(); + } + } + + /** + * Delete the room receipts + * + * @param roomId the room id. + */ + private void deleteRoomReceiptsFile(String roomId) { + File receiptsFile = new File(mStoreRoomsMessagesReceiptsFolderFile, roomId); + + // remove the files + if (receiptsFile.exists()) { + try { + receiptsFile.delete(); + } catch (Exception e) { + Log.e(LOG_TAG, "deleteReceiptsFile - failed " + e.getMessage(), e); + } + } + } + + //================================================================================ + // read/write methods + //================================================================================ + + /** + * Write an object in a dedicated file. + * + * @param description the operation description + * @param file the file + * @param object the object to save + * @return true if the operation succeeds + */ + private boolean writeObject(String description, File file, Object object) { + String parent = file.getParent(); + String name = file.getName(); + + File tmpFile = new File(parent, name + ".tmp"); + + if (tmpFile.exists()) { + tmpFile.delete(); + } + + if (file.exists()) { + file.renameTo(tmpFile); + } + + boolean succeed = false; + try { + FileOutputStream fos = new FileOutputStream(file); + OutputStream cos; + if (mEnableFileEncryption) { + cos = CompatUtil.createCipherOutputStream(fos, mContext); + } else { + cos = fos; + } + GZIPOutputStream gz = CompatUtil.createGzipOutputStream(cos); + ObjectOutputStream out = new ObjectOutputStream(gz); + + out.writeObject(object); + out.flush(); + out.close(); + + succeed = true; + } catch (OutOfMemoryError oom) { + dispatchOOM(oom); + } catch (Exception e) { + Log.e(LOG_TAG, "## writeObject() " + description + " : failed " + e.getMessage(), e); + } + + if (succeed) { + tmpFile.delete(); + } else { + tmpFile.renameTo(file); + } + + return succeed; + } + + /** + * Read an object from a dedicated file + * + * @param description the operation description + * @param file the file + * @return the read object if it can be retrieved + */ + private Object readObject(String description, File file) { + String parent = file.getParent(); + String name = file.getName(); + + File tmpFile = new File(parent, name + ".tmp"); + + if (tmpFile.exists()) { + Log.e(LOG_TAG, "## readObject : rescue from a tmp file " + tmpFile.getName()); + file = tmpFile; + } + + Object object = null; + try { + FileInputStream fis = new FileInputStream(file); + InputStream cis; + if (mEnableFileEncryption) { + cis = CompatUtil.createCipherInputStream(fis, mContext); + + if (cis == null) { + // fallback to unencrypted stream for backward compatibility + Log.i(LOG_TAG, "## readObject() : failed to read encrypted, fallback to unencrypted read"); + fis.close(); + cis = new FileInputStream(file); + } + } else { + cis = fis; + } + + GZIPInputStream gz = new GZIPInputStream(cis); + ObjectInputStream ois = new ObjectInputStream(gz); + object = ois.readObject(); + ois.close(); + } catch (OutOfMemoryError oom) { + dispatchOOM(oom); + } catch (Exception e) { + Log.e(LOG_TAG, "## readObject() " + description + " : failed " + e.getMessage(), e); + } + return object; + } + + + /** + * Remove the tmp files from a filename list + * + * @param names the names list + * @return the filtered list + */ + private static List listFiles(String[] names) { + List filteredFilenames = new ArrayList<>(); + List tmpFilenames = new ArrayList<>(); + + // sanity checks + // it has been reported by GA + if (null != names) { + for (int i = 0; i < names.length; i++) { + String name = names[i]; + + if (!name.endsWith(".tmp")) { + filteredFilenames.add(name); + } else { + tmpFilenames.add(name.substring(0, name.length() - ".tmp".length())); + } + } + + // check if the tmp file is not alone i.e the matched file was not saved (app crash...) + for (String tmpFileName : tmpFilenames) { + if (!filteredFilenames.contains(tmpFileName)) { + Log.e(LOG_TAG, "## listFiles() : " + tmpFileName + " does not exist but a tmp file has been retrieved"); + filteredFilenames.add(tmpFileName); + } + } + } + + return filteredFilenames; + } + + /** + * Start a runnable from the store thread + * + * @param runnable the runnable to call + */ + public void post(Runnable runnable) { + if (null != mFileStoreHandler) { + mFileStoreHandler.post(runnable); + } else { + super.post(runnable); + } + } + + //================================================================================ + // groups management + //================================================================================ + + /** + * Store a group + * + * @param group the group to store + */ + @Override + public void storeGroup(Group group) { + super.storeGroup(group); + if ((null != group) && !TextUtils.isEmpty(group.getGroupId())) { + mGroupsToCommit.add(group.getGroupId()); + } + } + + /** + * Flush a group + * + * @param group the group to store + */ + @Override + public void flushGroup(Group group) { + super.flushGroup(group); + if ((null != group) && !TextUtils.isEmpty(group.getGroupId())) { + mGroupsToCommit.add(group.getGroupId()); + saveGroups(); + } + } + + /** + * Delete a group + * + * @param groupId the groupId to delete + */ + @Override + public void deleteGroup(String groupId) { + super.deleteGroup(groupId); + if (!TextUtils.isEmpty(groupId)) { + mGroupsToCommit.add(groupId); + } + } + + /** + * Flush groups list + */ + private void saveGroups() { + // some updated rooms ? + if ((mGroupsToCommit.size() > 0) && (null != mFileStoreHandler)) { + // get the list + final Set fGroupIds = mGroupsToCommit; + mGroupsToCommit = new HashSet<>(); + + try { + Runnable r = new Runnable() { + @Override + public void run() { + mFileStoreHandler.post(new Runnable() { + public void run() { + if (!isKilled()) { + Log.d(LOG_TAG, "saveGroups " + fGroupIds.size() + " groups"); + + long start = System.currentTimeMillis(); + + for (String groupId : fGroupIds) { + Group group; + + synchronized (mGroups) { + group = mGroups.get(groupId); + } + + if (null != group) { + writeObject("saveGroup " + groupId, new File(mStoreGroupsFolderFile, groupId), group); + } else { + File tokenFile = new File(mStoreGroupsFolderFile, groupId); + + if (tokenFile.exists()) { + tokenFile.delete(); + } + } + } + + Log.d(LOG_TAG, "saveGroups done in " + (System.currentTimeMillis() - start) + " ms"); + } + } + }); + } + }; + + Thread t = new Thread(r); + t.start(); + } catch (OutOfMemoryError oom) { + Log.e(LOG_TAG, "saveGroups : failed" + oom.getMessage(), oom); + } + } + } + + /** + * Load groups from the filesystem. + * + * @return true if the operation succeeds. + */ + private boolean loadGroups() { + boolean succeed = true; + + try { + // extract the messages list + List filenames = listFiles(mStoreGroupsFolderFile.list()); + + long start = System.currentTimeMillis(); + + for (String filename : filenames) { + File groupFile = new File(mStoreGroupsFolderFile, filename); + + if (groupFile.exists()) { + Object groupAsVoid = readObject("loadGroups " + filename, groupFile); + + if ((null != groupAsVoid) && (groupAsVoid instanceof Group)) { + Group group = (Group) groupAsVoid; + mGroups.put(group.getGroupId(), group); + } else { + succeed = false; + break; + } + } + } + + if (succeed) { + long delta = (System.currentTimeMillis() - start); + Log.d(LOG_TAG, "loadGroups : " + filenames.size() + " groups in " + delta + " ms"); + mStoreStats.put("loadGroups", delta); + } + + } catch (Exception e) { + succeed = false; + Log.e(LOG_TAG, "loadGroups failed : " + e.getMessage(), e); + } + + return succeed; + } + + @Override + public void setURLPreviewEnabled(boolean value) { + super.setURLPreviewEnabled(value); + mMetaDataHasChanged = true; + } + + @Override + public void setRoomsWithoutURLPreview(Set roomIds) { + super.setRoomsWithoutURLPreview(roomIds); + mMetaDataHasChanged = true; + } + + @Override + public void setUserWidgets(Map contentDict) { + super.setUserWidgets(contentDict); + mMetaDataHasChanged = true; + } + + @Override + public void addFilter(String jsonFilter, String filterId) { + super.addFilter(jsonFilter, filterId); + mMetaDataHasChanged = true; + } + + @Override + public void setAntivirusServerPublicKey(@Nullable String key) { + super.setAntivirusServerPublicKey(key); + mMetaDataHasChanged = true; + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/MXFileStoreMetaData.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/MXFileStoreMetaData.java new file mode 100644 index 0000000000..1a3f6faaf6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/MXFileStoreMetaData.java @@ -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 mThirdPartyIdentifiers = null; + public List mIgnoredUsers = new ArrayList<>(); + public Map> mDirectChatRoomsMap = null; + public boolean mIsUrlPreviewEnabled = false; + public Map mUserWidgets = new HashMap<>(); + public Set 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 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; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/MXMemoryStore.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/MXMemoryStore.java new file mode 100644 index 0000000000..42aaf1b073 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/MXMemoryStore.java @@ -0,0 +1,1666 @@ +/* + * 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.os.Handler; +import android.os.Looper; +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.RoomAccountData; +import im.vector.matrix.android.internal.legacy.data.RoomSummary; +import im.vector.matrix.android.internal.legacy.data.comparator.Comparators; +import im.vector.matrix.android.internal.legacy.data.metrics.MetricsListener; +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.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.login.Credentials; +import im.vector.matrix.android.internal.legacy.rest.model.pid.ThirdPartyIdentifier; +import im.vector.matrix.android.internal.legacy.util.Log; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * An in-memory IMXStore. + */ +public class MXMemoryStore implements IMXStore { + + private static final String LOG_TAG = MXMemoryStore.class.getSimpleName(); + + protected Map mRooms; + protected Map mUsers; + + protected static final Object mRoomEventsLock = new Object(); + + // room id -> map of (event_id -> event) events for this room (linked so insertion order is preserved) + protected Map> mRoomEvents; + + protected Map mRoomTokens; + + protected Map mRoomSummaries; + protected Map mRoomAccountData; + + // dict of dict of MXReceiptData indexed by userId + protected final Object mReceiptsByRoomIdLock = new Object(); + protected Map> mReceiptsByRoomId; + + protected Map mGroups; + + // room state events + //protected final Map> mRoomStateEventsByRoomId = new HashMap<>(); + + // common context + private static Context mSharedContext = null; + + // the context + protected Context mContext; + + // + private final Map mTemporaryEventsList = new HashMap<>(); + protected MetricsListener mMetricsListener; + + protected Credentials mCredentials; + + protected String mEventStreamToken = null; + + protected final List mListeners = new ArrayList<>(); + + // Meta data about the store. It is defined only if the passed MXCredentials contains all information. + // When nil, nothing is stored on the file system. + protected MXFileStoreMetaData mMetadata = null; + + // last time the avatar / displayname was updated + protected long mUserDisplayNameTs; + protected long mUserAvatarUrlTs; + + // DataHandler -- added waiting to be refactored + private MXDataHandler mDataHandler; + + /** + * Initialization method. + */ + protected void initCommon() { + mRooms = new ConcurrentHashMap<>(); + mUsers = new ConcurrentHashMap<>(); + mRoomEvents = new ConcurrentHashMap<>(); + mRoomTokens = new ConcurrentHashMap<>(); + mRoomSummaries = new ConcurrentHashMap<>(); + mReceiptsByRoomId = new ConcurrentHashMap<>(); + mRoomAccountData = new ConcurrentHashMap<>(); + mGroups = new ConcurrentHashMap<>(); + mEventStreamToken = null; + } + + public MXMemoryStore() { + initCommon(); + } + + /** + * Set the application context + * + * @param context the context + */ + protected void setContext(Context context) { + if (null == mSharedContext) { + if (null != context) { + mSharedContext = context.getApplicationContext(); + } else { + throw new RuntimeException("MXMemoryStore : context cannot be null"); + } + } + + mContext = mSharedContext; + } + + /** + * Default constructor + * + * @param credentials the expected credentials + * @param context the context + */ + public MXMemoryStore(Credentials credentials, Context context) { + initCommon(); + + setContext(context); + mCredentials = credentials; + + mMetadata = new MXFileStoreMetaData(); + } + + @Override + public Context getContext() { + return mContext; + } + + /** + * 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. + */ + @Override + public void commit() { + } + + /** + * Open the store. + */ + public void open() { + } + + /** + * Close the store. + * Any pending operation must be complete in this call. + */ + @Override + public void close() { + } + + /** + * Clear the store. + * Any pending operation must be complete in this call. + */ + @Override + public void clear() { + initCommon(); + } + + /** + * Indicate if the MXStore implementation stores data permanently. + * Permanent storage allows the SDK to make less requests at the startup. + * + * @return true if permanent. + */ + @Override + public boolean isPermanent() { + return false; + } + + /** + * Check if the initial load is performed. + * + * @return true if it is ready. + */ + @Override + public boolean isReady() { + return true; + } + + /** + * Check if the read receipts are ready to be used. + * + * @return true if they are ready. + */ + @Override + public boolean areReceiptsReady() { + return true; + } + + /** + * @return true if the store is corrupted. + */ + @Override + public boolean isCorrupted() { + return false; + } + + /** + * Warn that the store data are corrupted. + * It might append if an update request failed. + * + * @param reason the corruption reason + */ + @Override + public void setCorrupted(String reason) { + dispatchOnStoreCorrupted(mCredentials.userId, reason); + } + + /** + * Returns to disk usage size in bytes. + * + * @return disk usage size + */ + @Override + public long diskUsage() { + return 0; + } + + /** + * Returns the latest known event stream token + * + * @return the event stream token + */ + @Override + public String getEventStreamToken() { + return mEventStreamToken; + } + + /** + * Set the event stream token. + * + * @param token the event stream token + */ + @Override + public void setEventStreamToken(String token) { + if (null != mMetadata) { + mMetadata.mEventStreamToken = token; + } + mEventStreamToken = token; + } + + @Override + public void addMXStoreListener(IMXStoreListener listener) { + synchronized (this) { + if ((null != listener) && (mListeners.indexOf(listener) < 0)) { + mListeners.add(listener); + } + } + } + + @Override + public void removeMXStoreListener(IMXStoreListener listener) { + synchronized (this) { + if (null != listener) { + mListeners.remove(listener); + } + } + } + + /** + * profile information + */ + @Override + public String displayName() { + if (null != mMetadata) { + return mMetadata.mUserDisplayName; + } else { + return null; + } + } + + @Override + public boolean setDisplayName(String displayName, long ts) { + boolean isUpdated; + + synchronized (LOG_TAG) { + if (null != mMetadata) { + Log.d(LOG_TAG, "## setDisplayName() : from " + mMetadata.mUserDisplayName + " to " + displayName + " ts " + ts); + } + + isUpdated = (null != mMetadata) + && !TextUtils.equals(mMetadata.mUserDisplayName, displayName) + && (mUserDisplayNameTs < ts) + && (ts != 0) + && (ts <= System.currentTimeMillis()); + + if (isUpdated) { + mMetadata.mUserDisplayName = (null != displayName) ? displayName.trim() : null; + mUserDisplayNameTs = ts; + + // update the cached oneself User + User myUser = getUser(mMetadata.mUserId); + + if (null != myUser) { + myUser.displayname = mMetadata.mUserDisplayName; + } + + Log.d(LOG_TAG, "## setDisplayName() : updated"); + commit(); + } + } + + return isUpdated; + } + + @Override + public String avatarURL() { + if (null != mMetadata) { + return mMetadata.mUserAvatarUrl; + } else { + return null; + } + } + + @Override + public boolean setAvatarURL(String avatarURL, long ts) { + boolean isUpdated = false; + + synchronized (LOG_TAG) { + if (null != mMetadata) { + Log.d(LOG_TAG, "## setAvatarURL() : from " + mMetadata.mUserAvatarUrl + " to " + avatarURL + " ts " + ts); + } + + isUpdated = (null != mMetadata) && !TextUtils.equals(mMetadata.mUserAvatarUrl, avatarURL) + && (mUserAvatarUrlTs < ts) && (ts != 0) && (ts <= System.currentTimeMillis()); + + if (isUpdated) { + mMetadata.mUserAvatarUrl = avatarURL; + mUserAvatarUrlTs = ts; + + // update the cached oneself User + User myUser = getUser(mMetadata.mUserId); + + if (null != myUser) { + myUser.setAvatarUrl(avatarURL); + } + + Log.d(LOG_TAG, "## setAvatarURL() : updated"); + commit(); + } + } + + return isUpdated; + } + + @Override + public List thirdPartyIdentifiers() { + if (null != mMetadata) { + return mMetadata.mThirdPartyIdentifiers; + } else { + return new ArrayList<>(); + } + } + + @Override + public void setThirdPartyIdentifiers(List identifiers) { + if (null != mMetadata) { + mMetadata.mThirdPartyIdentifiers = identifiers; + + Log.d(LOG_TAG, "setThirdPartyIdentifiers : commit"); + commit(); + } + } + + @Override + public List getIgnoredUserIdsList() { + if (null != mMetadata) { + return mMetadata.mIgnoredUsers; + } else { + return new ArrayList<>(); + } + } + + @Override + public void setIgnoredUserIdsList(List users) { + if (null != mMetadata) { + mMetadata.mIgnoredUsers = users; + Log.d(LOG_TAG, "setIgnoredUserIdsList : commit"); + commit(); + } + } + + @Override + public Map> getDirectChatRoomsDict() { + return mMetadata.mDirectChatRoomsMap; + } + + @Override + public void setDirectChatRoomsDict(Map> directChatRoomsDict) { + if (null != mMetadata) { + mMetadata.mDirectChatRoomsMap = directChatRoomsDict; + Log.d(LOG_TAG, "setDirectChatRoomsDict : commit"); + commit(); + } + } + + @Override + public Collection getRooms() { + return new ArrayList<>(mRooms.values()); + } + + @Override + public Collection getUsers() { + Collection users; + + synchronized (mUsers) { + users = new ArrayList<>(mUsers.values()); + } + + return users; + } + + @Override + public Room getRoom(String roomId) { + if (null != roomId) { + return mRooms.get(roomId); + } else { + return null; + } + } + + @Override + public User getUser(String userId) { + if (null != userId) { + User user; + + synchronized (mUsers) { + user = mUsers.get(userId); + } + + return user; + } else { + return null; + } + } + + @Override + public void storeUser(User user) { + if ((null != user) && (null != user.user_id)) { + try { + synchronized (mUsers) { + mUsers.put(user.user_id, user); + } + } catch (OutOfMemoryError e) { + dispatchOOM(e); + } + } + } + + /** + * Update the user information from a room member. + * + * @param roomMember the room member. + */ + @Override + public void updateUserWithRoomMemberEvent(RoomMember roomMember) { + try { + if (null != roomMember) { + User user = getUser(roomMember.getUserId()); + + // if the user does not exist, create it + if (null == user) { + user = new User(); + user.user_id = roomMember.getUserId(); + user.setRetrievedFromRoomMember(); + storeUser(user); + } + + // update the display name and the avatar url. + // the leave and ban events have no displayname and no avatar url. + if (TextUtils.equals(roomMember.membership, RoomMember.MEMBERSHIP_JOIN)) { + boolean hasUpdates = !TextUtils.equals(user.displayname, roomMember.displayname) + || !TextUtils.equals(user.getAvatarUrl(), roomMember.getAvatarUrl()); + + if (hasUpdates) { + // invite event does not imply that the user uses the application. + // but if the presence is set to 0, it means that the user information is not initialized + if (user.getLatestPresenceTs() < roomMember.getOriginServerTs()) { + // if the user joined the room, it implies that he used the application + user.displayname = roomMember.displayname; + user.setAvatarUrl(roomMember.getAvatarUrl()); + user.setLatestPresenceTs(roomMember.getOriginServerTs()); + user.setRetrievedFromRoomMember(); + } + } + } + } + } catch (OutOfMemoryError oom) { + dispatchOOM(oom); + Log.e(LOG_TAG, "## updateUserWithRoomMemberEvent() failed " + oom.getMessage(), oom); + } catch (Exception e) { + Log.e(LOG_TAG, "## updateUserWithRoomMemberEvent() failed " + e.getMessage(), e); + } + } + + @Override + public void storeRoom(Room room) { + if ((null != room) && (null != room.getRoomId())) { + mRooms.put(room.getRoomId(), room); + + // defines a default back token + if (!mRoomTokens.containsKey(room.getRoomId())) { + storeBackToken(room.getRoomId(), ""); + } + } + } + + @Override + public Event getOldestEvent(String roomId) { + Event event = null; + + if (null != roomId) { + synchronized (mRoomEventsLock) { + LinkedHashMap events = mRoomEvents.get(roomId); + + if (events != null) { + Iterator it = events.values().iterator(); + if (it.hasNext()) { + event = it.next(); + } + } + } + } + + return event; + } + + /** + * Get the latest event from the given room (to update summary for example) + * + * @param roomId the room id + * @return the event + */ + @Override + public Event getLatestEvent(String roomId) { + Event event = null; + + if (null != roomId) { + synchronized (mRoomEventsLock) { + LinkedHashMap events = mRoomEvents.get(roomId); + + if (events != null) { + Iterator it = events.values().iterator(); + if (it.hasNext()) { + Event lastEvent = null; + + while (it.hasNext()) { + lastEvent = it.next(); + } + + event = lastEvent; + } + } + } + } + return event; + } + + /** + * 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 + */ + @Override + public int eventsCountAfter(String roomId, String eventId) { + return eventsAfter(roomId, eventId, mCredentials.userId, null).size(); + } + + @Override + public void storeLiveRoomEvent(Event event) { + try { + if ((null != event) && (null != event.roomId) && (null != event.eventId)) { + synchronized (mRoomEventsLock) { + LinkedHashMap events = mRoomEvents.get(event.roomId); + + // create the list it does not exist + if (null == events) { + events = new LinkedHashMap<>(); + mRoomEvents.put(event.roomId, events); + } else if (events.containsKey(event.eventId)) { + // the event is already define + return; + } else if (!event.isDummyEvent() && (mTemporaryEventsList.size() > 0)) { + // remove any waiting echo event + String dummyKey = null; + + for (String key : mTemporaryEventsList.keySet()) { + Event eventToCheck = mTemporaryEventsList.get(key); + if (TextUtils.equals(eventToCheck.eventId, event.eventId)) { + dummyKey = key; + break; + } + } + + if (null != dummyKey) { + events.remove(dummyKey); + mTemporaryEventsList.remove(dummyKey); + } + } + + // If we don't have any information on this room - a pagination token, namely - we don't store the event but instead + // wait for the first pagination request to set things right + events.put(event.eventId, event); + + if (event.isDummyEvent()) { + mTemporaryEventsList.put(event.eventId, event); + } + } + } + } catch (OutOfMemoryError e) { + dispatchOOM(e); + } + } + + @Override + public boolean doesEventExist(String eventId, String roomId) { + boolean res = false; + + if (!TextUtils.isEmpty(eventId) && !TextUtils.isEmpty(roomId)) { + synchronized (mRoomEventsLock) { + res = mRoomEvents.containsKey(roomId) && mRoomEvents.get(roomId).containsKey(eventId); + } + } + + return res; + } + + @Override + public Event getEvent(String eventId, String roomId) { + Event event = null; + + if (!TextUtils.isEmpty(eventId) && !TextUtils.isEmpty(roomId)) { + synchronized (mRoomEventsLock) { + LinkedHashMap events = mRoomEvents.get(roomId); + + if (events != null) { + event = events.get(eventId); + } + } + } + + return event; + } + + @Override + public void deleteEvent(Event event) { + if ((null != event) && (null != event.roomId) && (event.eventId != null)) { + synchronized (mRoomEventsLock) { + LinkedHashMap events = mRoomEvents.get(event.roomId); + if (events != null) { + events.remove(event.eventId); + } + } + } + } + + @Override + public void deleteRoom(String roomId) { + // sanity check + if (null != roomId) { + deleteRoomData(roomId); + synchronized (mRoomEventsLock) { + mRooms.remove(roomId); + } + } + } + + @Override + public void deleteRoomData(String roomId) { + // sanity check + if (null != roomId) { + synchronized (mRoomEventsLock) { + mRoomEvents.remove(roomId); + mRoomTokens.remove(roomId); + mRoomSummaries.remove(roomId); + mRoomAccountData.remove(roomId); + mReceiptsByRoomId.remove(roomId); + } + } + } + + /** + * 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 + */ + @Override + public void deleteAllRoomMessages(String roomId, boolean keepUnsent) { + // sanity check + if (null != roomId) { + synchronized (mRoomEventsLock) { + + if (keepUnsent) { + LinkedHashMap eventMap = mRoomEvents.get(roomId); + + if (null != eventMap) { + List events = new ArrayList<>(eventMap.values()); + + for (Event event : events) { + if (event.mSentState == Event.SentState.SENT) { + if (null != event.eventId) { + eventMap.remove(event.eventId); + } + } + } + } + } else { + mRoomEvents.remove(roomId); + } + + mRoomSummaries.remove(roomId); + } + } + } + + @Override + public void flushRoomEvents(String roomId) { + // NOP + } + + @Override + public void storeRoomEvents(String roomId, TokensChunkEvents tokensChunkEvents, EventTimeline.Direction direction) { + try { + if (null != roomId) { + synchronized (mRoomEventsLock) { + LinkedHashMap events = mRoomEvents.get(roomId); + if (events == null) { + events = new LinkedHashMap<>(); + mRoomEvents.put(roomId, events); + } + + if (direction == EventTimeline.Direction.FORWARDS) { + mRoomTokens.put(roomId, tokensChunkEvents.start); + + for (Event event : tokensChunkEvents.chunk) { + events.put(event.eventId, event); + } + } else { // BACKWARD + Collection eventsList = events.values(); + + // no stored events + if (events.size() == 0) { + // insert the catchup events in reverse order + for (int index = tokensChunkEvents.chunk.size() - 1; index >= 0; index--) { + Event backEvent = tokensChunkEvents.chunk.get(index); + events.put(backEvent.eventId, backEvent); + } + + // define a token + mRoomTokens.put(roomId, tokensChunkEvents.start); + } else { + LinkedHashMap events2 = new LinkedHashMap<>(); + + // insert the catchup events in reverse order + for (int index = tokensChunkEvents.chunk.size() - 1; index >= 0; index--) { + Event backEvent = tokensChunkEvents.chunk.get(index); + events2.put(backEvent.eventId, backEvent); + } + + // add the previous added Events + for (Event event : eventsList) { + events2.put(event.eventId, event); + } + + // store the new list + mRoomEvents.put(roomId, events2); + } + } + } + } + } catch (OutOfMemoryError e) { + dispatchOOM(e); + } + } + + /** + * Store the back token of a room. + * + * @param roomId the room id. + * @param backToken the back token + */ + @Override + public void storeBackToken(String roomId, String backToken) { + if ((null != roomId) && (null != backToken)) { + mRoomTokens.put(roomId, backToken); + } + } + + @Override + public void flushSummary(RoomSummary summary) { + } + + @Override + public void flushSummaries() { + } + + @Override + public void storeSummary(RoomSummary summary) { + try { + if ((null != summary) && (null != summary.getRoomId())) { + mRoomSummaries.put(summary.getRoomId(), summary); + } + } catch (OutOfMemoryError e) { + dispatchOOM(e); + } + } + + @Override + public void storeAccountData(String roomId, RoomAccountData accountData) { + try { + if (null != roomId) { + Room room = mRooms.get(roomId); + + // sanity checks + if ((room != null) && (null != accountData)) { + mRoomAccountData.put(roomId, accountData); + } + } + } catch (OutOfMemoryError e) { + dispatchOOM(e); + } + } + + @Override + public void storeLiveStateForRoom(String roomId) { + } + + @Override + public void storeRoomStateEvent(String roomId, Event event) { + /*synchronized (mRoomStateEventsByRoomId) { + Map events = mRoomStateEventsByRoomId.get(roomId); + + if (null == events) { + events = new HashMap<>(); + mRoomStateEventsByRoomId.put(roomId, events); + } + + // keeps the latest state events + if (null != event.stateKey) { + events.put(event.stateKey, event); + } + }*/ + } + + @Override + public void getRoomStateEvents(final String roomId, final ApiCallback> callback) { + final List events = new ArrayList<>(); + + /*synchronized (mRoomStateEventsByRoomId) { + if (mRoomStateEventsByRoomId.containsKey(roomId)) { + events.addAll(mRoomStateEventsByRoomId.get(roomId).values()); + } + }*/ + + (new Handler(Looper.getMainLooper())).post(new Runnable() { + @Override + public void run() { + callback.onSuccess(events); + } + }); + } + + /** + * 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. + */ + @Override + public Collection getRoomMessages(final String roomId) { + // sanity check + if (null == roomId) { + return null; + } + + Collection collection = null; + + synchronized (mRoomEventsLock) { + LinkedHashMap events = mRoomEvents.get(roomId); + + if (null != events) { + collection = new ArrayList<>(events.values()); + } + } + + return collection; + } + + @Override + public TokensChunkEvents getEarlierMessages(final String roomId, final String fromToken, final int limit) { + // For now, we return everything we have for the original null token request + // For older requests (providing a token), returning null for now + if (null != roomId) { + List eventsList; + + synchronized (mRoomEventsLock) { + LinkedHashMap events = mRoomEvents.get(roomId); + if ((events == null) || (events.size() == 0)) { + return null; + } + + // reach the end of the stored items + if (TextUtils.equals(mRoomTokens.get(roomId), fromToken)) { + return null; + } + + // check if the token is known in the sublist + eventsList = new ArrayList<>(events.values()); + } + + + List subEventsList = new ArrayList<>(); + + // search from the latest to the oldest events + Collections.reverse(eventsList); + + TokensChunkEvents response = new TokensChunkEvents(); + + // start the latest event and there is enough events to provide to the caller ? + if ((null == fromToken) && (eventsList.size() <= limit)) { + subEventsList = eventsList; + } else { + int index = 0; + + if (null != fromToken) { + // search if token is one of the stored events + for (; (index < eventsList.size()) && (!TextUtils.equals(fromToken, eventsList.get(index).mToken)); index++) + ; + + index++; + } + + // found it ? + if (index < eventsList.size()) { + for (; index < eventsList.size(); index++) { + Event event = eventsList.get(index); + subEventsList.add(event); + + // loop until to find an event with a token + if ((subEventsList.size() >= limit) && (event.mToken != null)) { + break; + } + } + } + } + + // unknown token + if (subEventsList.size() == 0) { + return null; + } + + response.chunk = subEventsList; + + Event firstEvent = subEventsList.get(0); + Event lastEvent = subEventsList.get(subEventsList.size() - 1); + + response.start = firstEvent.mToken; + + // unknown last event token, use the latest known one + if ((null == lastEvent.mToken) && !TextUtils.isEmpty(mRoomTokens.get(roomId))) { + lastEvent.mToken = mRoomTokens.get(roomId); + } + + response.end = lastEvent.mToken; + + return response; + } + return null; + } + + @Override + public Collection getSummaries() { + List summaries = new ArrayList<>(); + + for (String roomId : mRoomSummaries.keySet()) { + Room room = mRooms.get(roomId); + if (null != room) { + if (!room.isJoined() && !room.isInvited()) { + Log.e(LOG_TAG, "## getSummaries() : a summary exists for the roomId " + roomId + " but the user is not anymore a member"); + } else { + summaries.add(mRoomSummaries.get(roomId)); + } + } else { + Log.e(LOG_TAG, "## getSummaries() : a summary exists for the roomId " + roomId + " but it does not exist in the room list"); + } + } + + return summaries; + } + + @Nullable + @Override + public RoomSummary getSummary(String roomId) { + // sanity check + if (null == roomId) { + return null; + } + + Room room = mRooms.get(roomId); + if (null != room) { + return mRoomSummaries.get(roomId); + } else { + Log.e(LOG_TAG, "## getSummary() : a summary exists for the roomId " + roomId + " but it does not exist in the room list"); + } + + return null; + } + + @Override + public List getLatestUnsentEvents(String roomId) { + if (null == roomId) { + return null; + } + + List unsentRoomEvents = new ArrayList<>(); + + synchronized (mRoomEventsLock) { + LinkedHashMap events = mRoomEvents.get(roomId); + + // contain some events + if ((null != events) && (events.size() > 0)) { + List eventsList = new ArrayList<>(events.values()); + + for (int index = events.size() - 1; index >= 0; index--) { + Event event = eventsList.get(index); + + if (event.mSentState == Event.SentState.WAITING_RETRY) { + unsentRoomEvents.add(event); + } else { + //break; + } + } + + Collections.reverse(unsentRoomEvents); + } + } + + return unsentRoomEvents; + } + + @Override + public List getUndeliveredEvents(String roomId) { + if (null == roomId) { + return null; + } + + List undeliveredEvents = new ArrayList<>(); + + synchronized (mRoomEventsLock) { + LinkedHashMap events = mRoomEvents.get(roomId); + + // contain some events + if ((null != events) && (events.size() > 0)) { + List eventsList = new ArrayList<>(events.values()); + + for (int index = 0; index < events.size(); index++) { + Event event = eventsList.get(index); + + if (event.isUndelivered()) { + undeliveredEvents.add(event); + } + } + } + } + + return undeliveredEvents; + } + + @Override + public List getUnknownDeviceEvents(String roomId) { + if (null == roomId) { + return null; + } + + List unknownDeviceEvents = new ArrayList<>(); + + synchronized (mRoomEventsLock) { + LinkedHashMap events = mRoomEvents.get(roomId); + + // contain some events + if ((null != events) && (events.size() > 0)) { + List eventsList = new ArrayList<>(events.values()); + + for (int index = 0; index < events.size(); index++) { + Event event = eventsList.get(index); + + if (event.isUnknownDevice()) { + unknownDeviceEvents.add(event); + } + } + } + } + + return unknownDeviceEvents; + } + + /** + * 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. + */ + @Override + public List getEventReceipts(String roomId, String eventId, boolean excludeSelf, boolean sort) { + List receipts = new ArrayList<>(); + + synchronized (mReceiptsByRoomIdLock) { + if (mReceiptsByRoomId.containsKey(roomId)) { + String myUserID = mCredentials.userId; + + Map receiptsByUserId = mReceiptsByRoomId.get(roomId); + // copy the user id list to avoid having update while looping + List userIds = new ArrayList<>(receiptsByUserId.keySet()); + + if (null == eventId) { + receipts.addAll(receiptsByUserId.values()); + } else { + for (String userId : userIds) { + if (receiptsByUserId.containsKey(userId) && (!excludeSelf || !TextUtils.equals(myUserID, userId))) { + ReceiptData receipt = receiptsByUserId.get(userId); + + if (TextUtils.equals(receipt.eventId, eventId)) { + receipts.add(receipt); + } + } + } + } + } + } + + if (sort && (receipts.size() > 0)) { + Collections.sort(receipts, Comparators.descComparator); + } + + return receipts; + } + + /** + * 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 + */ + @Override + public boolean storeReceipt(ReceiptData receipt, String roomId) { + try { + // sanity check + if (TextUtils.isEmpty(roomId) || (null == receipt)) { + return false; + } + + Map receiptsByUserId; + + //Log.d(LOG_TAG, "## storeReceipt() : roomId " + roomId + " userId " + receipt.userId + " eventId " + receipt.eventId + // + " originServerTs " + receipt.originServerTs); + + synchronized (mReceiptsByRoomIdLock) { + if (!mReceiptsByRoomId.containsKey(roomId)) { + receiptsByUserId = new HashMap<>(); + mReceiptsByRoomId.put(roomId, receiptsByUserId); + } else { + receiptsByUserId = mReceiptsByRoomId.get(roomId); + } + } + + ReceiptData curReceipt = null; + + if (receiptsByUserId.containsKey(receipt.userId)) { + curReceipt = receiptsByUserId.get(receipt.userId); + } + + if (null == curReceipt) { + //Log.d(LOG_TAG, "## storeReceipt() : there was no receipt from this user"); + receiptsByUserId.put(receipt.userId, receipt); + return true; + } + + if (TextUtils.equals(receipt.eventId, curReceipt.eventId)) { + //Log.d(LOG_TAG, "## storeReceipt() : receipt for the same event"); + return false; + } + + if (receipt.originServerTs < curReceipt.originServerTs) { + //Log.d(LOG_TAG, "## storeReceipt() : the receipt is older that the current one"); + return false; + } + + // check if the read receipt is not for an already read message + if (TextUtils.equals(receipt.userId, mCredentials.userId)) { + synchronized (mReceiptsByRoomIdLock) { + LinkedHashMap eventsMap = mRoomEvents.get(roomId); + + // test if the event is know + if ((null != eventsMap) && eventsMap.containsKey(receipt.eventId)) { + List eventIds = new ArrayList<>(eventsMap.keySet()); + + int curEventPos = eventIds.indexOf(curReceipt.eventId); + int newEventPos = eventIds.indexOf(receipt.eventId); + + if (curEventPos >= newEventPos) { + Log.d(LOG_TAG, "## storeReceipt() : the read message is already read (cur pos " + curEventPos + + " receipt event pos " + newEventPos + ")"); + return false; + } + } + } + } + + //Log.d(LOG_TAG, "## storeReceipt() : updated"); + receiptsByUserId.put(receipt.userId, receipt); + } catch (OutOfMemoryError e) { + dispatchOOM(e); + } + + return true; + } + + /** + * Get the receipt for an user in a dedicated room. + * + * @param roomId the room id. + * @param userId the user id. + * @return the dedicated receipt + */ + @Override + public ReceiptData getReceipt(String roomId, String userId) { + ReceiptData res = null; + + // sanity checks + if (!TextUtils.isEmpty(roomId) && !TextUtils.isEmpty(userId)) { + synchronized (mReceiptsByRoomIdLock) { + if (mReceiptsByRoomId.containsKey(roomId)) { + Map receipts = mReceiptsByRoomId.get(roomId); + res = receipts.get(userId); + } + } + } + + return res; + } + + /** + * Return a list of stored events after the parameter one. + * It could the ones sent by the user excludedUserId. + * A filter can be applied to ignore some event (Event.EVENT_TYPE_...). + * + * @param roomId the roomId + * @param eventId the start event Id. + * @param excludedUserId the excluded user id + * @param allowedTypes the filtered event type (null to allow anyone) + * @return the evnts list + */ + private List eventsAfter(String roomId, String eventId, String excludedUserId, List allowedTypes) { + // events list + List events = new ArrayList<>(); + + // sanity check + if (null != roomId) { + synchronized (mRoomEventsLock) { + LinkedHashMap roomEvents = mRoomEvents.get(roomId); + + if (roomEvents != null) { + List linkedEvents = new ArrayList<>(roomEvents.values()); + + // Check messages from the most recent + for (int i = linkedEvents.size() - 1; i >= 0; i--) { + Event event = linkedEvents.get(i); + + if ((null == eventId) || !TextUtils.equals(event.eventId, eventId)) { + // Keep events matching filters + if ((null == allowedTypes || (allowedTypes.indexOf(event.getType()) >= 0)) + && !TextUtils.equals(event.getSender(), excludedUserId)) { + events.add(event); + } + } else { + // We are done + break; + } + } + + // filter the unread messages + // some messages are not defined as unreadable + for (int index = 0; index < events.size(); index++) { + Event event = events.get(index); + + if (TextUtils.equals(event.getSender(), mCredentials.userId) || TextUtils.equals(event.getType(), Event.EVENT_TYPE_STATE_ROOM_MEMBER)) { + events.remove(index); + index--; + } + } + + Collections.reverse(events); + } + } + } + + return events; + } + + /** + * Check if an event has been read by an user. + * + * @param roomId the room Id + * @param userId the user id + * @param eventIdTotest the event id + * @return true if the user has read the message. + */ + @Override + public boolean isEventRead(String roomId, String userId, String eventIdTotest) { + boolean res = false; + + // sanity check + if ((null != roomId) && (null != userId)) { + synchronized (mReceiptsByRoomIdLock) { + synchronized (mRoomEventsLock) { + if (mReceiptsByRoomId.containsKey(roomId) && mRoomEvents.containsKey(roomId)) { + Map receiptsByUserId = mReceiptsByRoomId.get(roomId); + LinkedHashMap eventsMap = mRoomEvents.get(roomId); + + // check if the event is known + if (eventsMap.containsKey(eventIdTotest) && receiptsByUserId.containsKey(userId)) { + ReceiptData data = receiptsByUserId.get(userId); + List eventIds = new ArrayList<>(eventsMap.keySet()); + + // the message has been read if it was sent before the latest read one + res = eventIds.indexOf(eventIdTotest) <= eventIds.indexOf(data.eventId); + } else if (receiptsByUserId.containsKey(userId)) { + // the event is not known so assume it is has been flushed + res = true; + } + } + } + } + } + + return res; + } + + /** + * 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. + */ + @Override + public List unreadEvents(String roomId, List types) { + List res = null; + + synchronized (mReceiptsByRoomIdLock) { + if (mReceiptsByRoomId.containsKey(roomId)) { + Map receiptsByUserId = mReceiptsByRoomId.get(roomId); + + if (receiptsByUserId.containsKey(mCredentials.userId)) { + ReceiptData data = receiptsByUserId.get(mCredentials.userId); + + res = eventsAfter(roomId, data.eventId, mCredentials.userId, types); + } + } + } + + if (null == res) { + res = new ArrayList<>(); + } + + return res; + } + + /** + * @return the current listeners + */ + private List getListeners() { + List listeners; + + synchronized (this) { + listeners = new ArrayList<>(mListeners); + } + + return listeners; + } + + /** + * Dispatch postProcess + * + * @param accountId the account id + */ + protected void dispatchPostProcess(String accountId) { + List listeners = getListeners(); + + for (IMXStoreListener listener : listeners) { + listener.postProcess(accountId); + } + } + + /** + * Dispatch store ready + * + * @param accountId the account id + */ + protected void dispatchOnStoreReady(String accountId) { + List listeners = getListeners(); + + for (IMXStoreListener listener : listeners) { + listener.onStoreReady(accountId); + } + } + + /** + * Dispatch that the store is corrupted + * + * @param accountId the account id + * @param description the error description + */ + protected void dispatchOnStoreCorrupted(String accountId, String description) { + List listeners = getListeners(); + + for (IMXStoreListener listener : listeners) { + listener.onStoreCorrupted(accountId, description); + } + } + + /** + * Dispatch an out of memory error. + * + * @param e the out of memory error + */ + protected void dispatchOOM(OutOfMemoryError e) { + List listeners = getListeners(); + + for (IMXStoreListener listener : listeners) { + listener.onStoreOOM(mCredentials.userId, e.getMessage()); + } + } + + /** + * Dispatch the read receipts loading. + * + * @param roomId the room id. + */ + protected void dispatchOnReadReceiptsLoaded(String roomId) { + List listeners = getListeners(); + + for (IMXStoreListener listener : listeners) { + listener.onReadReceiptsLoaded(roomId); + } + } + + /** + * Provides the store preload time in milliseconds. + * + * @return the store preload time in milliseconds. + */ + @Override + public long getPreloadTime() { + return 0; + } + + /** + * Provides some store stats + * + * @return the store stats + */ + @Override + public Map getStats() { + return new HashMap<>(); + } + + /** + * Start a runnable from the store thread + * + * @param runnable the runnable to call + */ + @Override + public void post(Runnable runnable) { + new Handler(Looper.getMainLooper()).post(runnable); + } + + /** + * Store a group + * + * @param group the group to store + */ + @Override + public void storeGroup(Group group) { + if ((null != group) && !TextUtils.isEmpty(group.getGroupId())) { + synchronized (mGroups) { + mGroups.put(group.getGroupId(), group); + } + } + } + + /** + * Flush a group + * + * @param group the group to store + */ + @Override + public void flushGroup(Group group) { + } + + /** + * Delete a group + * + * @param groupId the groupId to delete + */ + @Override + public void deleteGroup(String groupId) { + if (!TextUtils.isEmpty(groupId)) { + synchronized (mGroups) { + mGroups.remove(groupId); + } + } + } + + /** + * Retrieve a group from its id. + * + * @param groupId the group id + * @return the group if it exists + */ + @Override + public Group getGroup(String groupId) { + synchronized (mGroups) { + return (null != groupId) ? mGroups.get(groupId) : null; + } + } + + /** + * @return the stored groups + */ + @Override + public Collection getGroups() { + synchronized (mGroups) { + return mGroups.values(); + } + } + + @Override + public void setURLPreviewEnabled(boolean value) { + mMetadata.mIsUrlPreviewEnabled = value; + } + + @Override + public boolean isURLPreviewEnabled() { + return mMetadata.mIsUrlPreviewEnabled; + } + + @Override + public void setRoomsWithoutURLPreview(Set roomIds) { + mMetadata.mRoomsListWithoutURLPrevew = roomIds; + } + + @Override + public void setUserWidgets(Map contentDict) { + mMetadata.mUserWidgets = contentDict; + } + + @Override + public Map getUserWidgets() { + return mMetadata.mUserWidgets; + } + + @Override + public Set getRoomsWithoutURLPreviews() { + return (null != mMetadata.mRoomsListWithoutURLPrevew) ? mMetadata.mRoomsListWithoutURLPrevew : new HashSet(); + } + + @Override + public void addFilter(String jsonFilter, String filterId) { + mMetadata.mKnownFilters.put(jsonFilter, filterId); + } + + @Override + public Map getFilters() { + return new HashMap<>(mMetadata.mKnownFilters); + } + + @Override + public void setAntivirusServerPublicKey(@Nullable String key) { + mMetadata.mAntivirusServerPublicKey = key; + } + + @Override + @Nullable + public String getAntivirusServerPublicKey() { + return mMetadata.mAntivirusServerPublicKey; + } + + /** + * Update the metrics listener + * + * @param metricsListener the metrics listener + */ + public void setMetricsListener(MetricsListener metricsListener) { + mMetricsListener = metricsListener; + } + + /** + * Get the associated dataHandler + * + * @return the associated dataHandler + */ + protected MXDataHandler getDataHandler() { + return mDataHandler; + } + + /** + * Update the associated dataHandler + * + * @param dataHandler the dataHandler + */ + public void setDataHandler(final MXDataHandler dataHandler) { + mDataHandler = dataHandler; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/MXStoreListener.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/MXStoreListener.java new file mode 100644 index 0000000000..19de74a980 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/MXStoreListener.java @@ -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) { + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/EventTimeline.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/EventTimeline.java new file mode 100644 index 0000000000..9737266961 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/EventTimeline.java @@ -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. + *

+ * There are two kinds of timeline: + *

+ * - 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. + *

+ * - 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 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 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 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 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 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 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); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/EventTimelineFactory.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/EventTimelineFactory.java new file mode 100644 index 0000000000..6cbdefab94 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/EventTimelineFactory.java @@ -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; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/MXEventTimeline.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/MXEventTimeline.java new file mode 100644 index 0000000000..ea4ead5f5d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/MXEventTimeline.java @@ -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 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 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 events, + @Nullable List 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 events, + @Nullable final List stateEvents, + final Direction direction, + final ApiCallback callback) { + AsyncTask task = new AsyncTask() { + @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 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 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 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(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() : 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 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(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 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 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(callback) { + @Override + public void onSuccess(final EventContext eventContext) { + + AsyncTask task = new AsyncTask() { + @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 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 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() { + @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); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/StateEventRedactionChecker.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/StateEventRedactionChecker.java new file mode 100644 index 0000000000..f0ac3bf5ba --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/StateEventRedactionChecker.java @@ -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>() { + @Override + public void onSuccess(List 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() { + @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); + } + }); + } + } + + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/TimelineEventListeners.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/TimelineEventListeners.java new file mode 100644 index 0000000000..ed5c843bc7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/TimelineEventListeners.java @@ -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 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 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); + } + }); + } + } + + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/TimelineEventSaver.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/TimelineEventSaver.java new file mode 100644 index 0000000000..a7b9b507cf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/TimelineEventSaver.java @@ -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); + } + } + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/TimelineInvitedRoomSyncHandler.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/TimelineInvitedRoomSyncHandler.java new file mode 100644 index 0000000000..e70c9a8dd6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/TimelineInvitedRoomSyncHandler.java @@ -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); + } + } + + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/TimelineJoinRoomSyncHandler.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/TimelineJoinRoomSyncHandler.java new file mode 100644 index 0000000000..4a2d3b7bbc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/TimelineJoinRoomSyncHandler.java @@ -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 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 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); + } + } + } + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/TimelineLiveEventHandler.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/TimelineLiveEventHandler.java new file mode 100644 index 0000000000..9d75e94d2d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/TimelineLiveEventHandler.java @@ -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 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); + } + } + } + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/TimelinePushWorker.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/TimelinePushWorker.java new file mode 100644 index 0000000000..c97fe0345e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/TimelinePushWorker.java @@ -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); + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/TimelineStateHolder.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/TimelineStateHolder.java new file mode 100644 index 0000000000..1e40769f1f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/timeline/TimelineStateHolder.java @@ -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; + } + + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/db/MXLatestChatMessageCache.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/db/MXLatestChatMessageCache.java new file mode 100644 index 0000000000..c1e16412e1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/db/MXLatestChatMessageCache.java @@ -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 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); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/db/MXMediaDownloadWorkerTask.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/db/MXMediaDownloadWorkerTask.java new file mode 100644 index 0000000000..7ae2f8da5e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/db/MXMediaDownloadWorkerTask.java @@ -0,0 +1,1157 @@ +/* + * 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.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.AsyncTask; +import android.support.annotation.Nullable; +import android.support.v4.util.LruCache; +import android.text.TextUtils; +import android.util.Pair; +import android.webkit.MimeTypeMap; +import android.widget.ImageView; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; + +import im.vector.matrix.android.internal.legacy.HomeServerConnectionConfig; +import im.vector.matrix.android.internal.legacy.RestClient; +import im.vector.matrix.android.internal.legacy.crypto.MXEncryptedAttachments; +import im.vector.matrix.android.internal.legacy.listeners.IMXMediaDownloadListener; +import im.vector.matrix.android.internal.legacy.network.NetworkConnectivityReceiver; +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.MediaScanRestClient; +import im.vector.matrix.android.internal.legacy.rest.model.EncryptedMediaScanBody; +import im.vector.matrix.android.internal.legacy.rest.model.EncryptedMediaScanEncryptedBody; +import im.vector.matrix.android.internal.legacy.rest.model.MatrixError; +import im.vector.matrix.android.internal.legacy.rest.model.crypto.EncryptedBodyFileInfo; +import im.vector.matrix.android.internal.legacy.rest.model.crypto.EncryptedFileInfo; +import im.vector.matrix.android.internal.legacy.ssl.CertUtil; +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 org.matrix.olm.OlmPkEncryption; +import org.matrix.olm.OlmPkMessage; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.lang.ref.WeakReference; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.MessageDigest; +import java.util.ArrayList; +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 java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509TrustManager; + +/** + * This class manages the media downloading in background. + *

+ * JsonElement: Error message if not null. + */ +class MXMediaDownloadWorkerTask extends AsyncTask { + + private static final String LOG_TAG = MXMediaDownloadWorkerTask.class.getSimpleName(); + + /** + * Pending media URLs + */ + private static final Map sPendingDownloadById = new HashMap<>(); + + /** + * List of unreachable media urls. + */ + private static final List sUnreachableUrls = new ArrayList<>(); + + // avoid sync on "this" because it might differ if there is a timer. + private static final Object sSyncObject = new Object(); + + /** + * The medias cache + */ + private static LruCache sBitmapByDownloadIdCache = null; + + /** + * The downloaded media callbacks. + */ + private final List mDownloadListeners = new ArrayList<>(); + + /** + * The ImageView list to refresh when the media is downloaded. + */ + private final List> mImageViewReferences; + + /** + * The media URL. + */ + private String mUrl; + + /** + * The download identifier based on the original matrix content url for this media. + */ + private String mDownloadId; + + /** + * Tells if the anti-virus scanner is enabled. + */ + private boolean mIsAvScannerEnabled; + + /** + * The media mime type + */ + private String mMimeType; + + /** + * The application context + */ + private Context mApplicationContext; + + /** + * The directory in which the media must be stored. + */ + private File mDirectoryFile; + + /** + * The rotation to apply. + */ + private int mRotation; + + /** + * The download stats. + */ + private IMXMediaDownloadListener.DownloadStats mDownloadStats; + + /** + * Tells the download has been cancelled. + */ + private boolean mIsDownloadCancelled; + + /** + * Tells if the download has been completed + */ + private boolean mIsDone; + + /** + * The home server config. + */ + private final HomeServerConnectionConfig mHsConfig; + + /** + * The bitmap to use when the URL is unreachable. + */ + private Bitmap mDefaultBitmap; + + /** + * the encrypted file information + */ + private final EncryptedFileInfo mEncryptedFileInfo; + + /** + * Network updates tracker + */ + private final NetworkConnectivityReceiver mNetworkConnectivityReceiver; + + /** + * Rest client to retrieve public antivirus server key + */ + @Nullable + private MediaScanRestClient mMediaScanRestClient; + + /** + * Download constants + */ + private static final int DOWNLOAD_TIME_OUT = 10 * 1000; + private static final int DOWNLOAD_BUFFER_READ_SIZE = 1024 * 32; + + //============================================================================================================== + // static methods + //============================================================================================================== + + /** + * Clear the internal cache. + */ + public static void clearBitmapsCache() { + if (null != sBitmapByDownloadIdCache) { + sBitmapByDownloadIdCache.evictAll(); + } + + // Clear the list of unreachable Urls, to retry to download it on next access + synchronized (sUnreachableUrls) { + sUnreachableUrls.clear(); + } + } + + /** + * Check if there is a pending download with the provided id. + * + * @param downloadId The identifier to check + * @return the dedicated MXMediaDownloadWorkerTask if it exists. + */ + public static MXMediaDownloadWorkerTask getMediaDownloadWorkerTask(String downloadId) { + if (sPendingDownloadById.containsKey(downloadId)) { + MXMediaDownloadWorkerTask task; + synchronized (sPendingDownloadById) { + task = sPendingDownloadById.get(downloadId); + } + return task; + } else { + return null; + } + } + + /** + * Generate an unique ID for a string + * + * @param input the string + * @return the unique ID + */ + private static String uniqueId(String input) { + String uniqueId = null; + + try { + MessageDigest mDigest = MessageDigest.getInstance("SHA1"); + byte[] result = mDigest.digest(input.getBytes()); + StringBuffer sb = new StringBuffer(); + + for (int i = 0; i < result.length; i++) { + sb.append(Integer.toString((result[i] & 0xff) + 0x100, 16).substring(1)); + } + + uniqueId = sb.toString(); + } catch (Exception e) { + Log.e(LOG_TAG, "uniqueId failed " + e.getMessage(), e); + } + + if (null == uniqueId) { + uniqueId = "" + Math.abs(input.hashCode() + (System.currentTimeMillis() + "").hashCode()); + } + + return uniqueId; + } + + /** + * Build a filename from a download Id + * + * @param downloadId the media url + * @param mimeType the mime type; + * @return the cache filename + */ + static String buildFileName(String downloadId, String mimeType) { + String name = "file_" + uniqueId(downloadId); + + if (!TextUtils.isEmpty(mimeType)) { + String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); + + // some devices don't support .jpeg files + if ("jpeg".equals(fileExtension)) { + fileExtension = "jpg"; + } + + if (null != fileExtension) { + name += "." + fileExtension; + } + } + + return name; + } + + /** + * Tell if the media is cached with the provided cache identifier + * + * @param mediaCacheId + * @return true if a media is cached with this identifier + */ + public static boolean isMediaCached(String mediaCacheId) { + boolean res = false; + + if ((null != sBitmapByDownloadIdCache)) { + synchronized (sSyncObject) { + res = (null != sBitmapByDownloadIdCache.get(mediaCacheId)); + } + } + + return res; + } + + /** + * Tells if the media URL is unreachable. + * + * @param url the url to test. + * @return true if the media URL is unreachable. + */ + public static boolean isMediaUrlUnreachable(String url) { + boolean res = true; + + if (!TextUtils.isEmpty(url)) { + synchronized (sUnreachableUrls) { + res = sUnreachableUrls.contains(url); + } + } + + return res; + } + + /** + * Search a cached bitmap from an url. + * rotationAngle is set to Integer.MAX_VALUE when undefined : the EXIF metadata must be checked. + * + * @param baseFile the base file + * @param url the actual media url + * @param downloadId the predefined id of the download task for this content + * @param aRotation the bitmap rotation + * @param mimeType the mime type + * @param encryptionInfo the encryption information + * @return true if the bitmap is cached + */ + static boolean bitmapForURL(final Context context, + final File baseFile, + final String url, + final String downloadId, + final int aRotation, + final String mimeType, + final EncryptedFileInfo encryptionInfo, + final ApiCallback callback) { + if (TextUtils.isEmpty(url)) { + Log.d(LOG_TAG, "bitmapForURL : null url"); + return false; + } + + if (null == sBitmapByDownloadIdCache) { + int lruSize = Math.min(20 * 1024 * 1024, (int) Runtime.getRuntime().maxMemory() / 8); + + Log.d(LOG_TAG, "bitmapForURL lruSize : " + lruSize); + + sBitmapByDownloadIdCache = new LruCache(lruSize) { + @Override + protected int sizeOf(String key, Bitmap bitmap) { + return bitmap.getRowBytes() * bitmap.getHeight(); // size in bytes + } + }; + } + + // the image is downloading in background + if (null != getMediaDownloadWorkerTask(downloadId)) { + return false; + } + + // the url is invalid + if (isMediaUrlUnreachable(url)) { + return false; + } + + final Bitmap cachedBitmap; + + synchronized (sSyncObject) { + cachedBitmap = sBitmapByDownloadIdCache.get(downloadId); + } + + if (null != cachedBitmap) { + MXMediasCache.mUIHandler.post(new Runnable() { + @Override + public void run() { + callback.onSuccess(cachedBitmap); + } + }); + return true; + } + + // invalid basefile + if (null == baseFile) { + return false; + } + + // check if the image has not been saved in file system + String filename = null; + + // the url is a file one + if (url.startsWith("file:")) { + // try to parse it + try { + Uri uri = Uri.parse(url); + filename = uri.getPath(); + } catch (Exception e) { + Log.e(LOG_TAG, "bitmapForURL #1 : " + e.getMessage(), e); + } + + // cannot extract the filename -> sorry + if (null == filename) { + return false; + } + } + + // not a valid file name + if (null == filename) { + filename = buildFileName(downloadId, mimeType); + } + + final String fFilename = filename; + final File file = filename.startsWith(File.separator) ? new File(filename) : new File(baseFile, filename); + + if (!file.exists()) { + return false; + } + + MXMediasCache.mDecryptingHandler.post(new Runnable() { + @Override + public void run() { + Bitmap bitmap = null; + int rotation = aRotation; + + try { + + InputStream fis = new FileInputStream(file); + + if (null != encryptionInfo) { + InputStream decryptedIs = MXEncryptedAttachments.decryptAttachment(fis, encryptionInfo); + fis.close(); + fis = decryptedIs; + } + + // read the metadata + if (Integer.MAX_VALUE == rotation) { + rotation = ImageUtils.getRotationAngleForBitmap(context, Uri.fromFile(file)); + } + + if (null != fis) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + + try { + bitmap = BitmapFactory.decodeStream(fis, null, options); + } catch (OutOfMemoryError error) { + System.gc(); + Log.e(LOG_TAG, "bitmapForURL() : Out of memory 1 " + error, error); + } + + // try again + if (null == bitmap) { + try { + bitmap = BitmapFactory.decodeStream(fis, null, options); + } catch (OutOfMemoryError error) { + Log.e(LOG_TAG, "bitmapForURL() Out of memory 2 " + error, error); + } + } + + if (null != bitmap) { + synchronized (sSyncObject) { + if (0 != rotation) { + try { + android.graphics.Matrix bitmapMatrix = new android.graphics.Matrix(); + bitmapMatrix.postRotate(rotation); + + Bitmap transformedBitmap = Bitmap.createBitmap(bitmap, + 0, 0, bitmap.getWidth(), bitmap.getHeight(), bitmapMatrix, false); + + // Bitmap.createBitmap() can return the same bitmap, so do not recycle it if it is the case + if (transformedBitmap != bitmap) { + bitmap.recycle(); + } + + bitmap = transformedBitmap; + } catch (OutOfMemoryError ex) { + Log.e(LOG_TAG, "bitmapForURL rotation error : " + ex.getMessage(), ex); + } + } + + // cache only small images + // caching large images does not make sense + // it would replace small ones. + // let assume that the application must be faster when showing the chat history. + if ((bitmap.getWidth() < 1000) && (bitmap.getHeight() < 1000)) { + sBitmapByDownloadIdCache.put(downloadId, bitmap); + } + } + } + + fis.close(); + } + + } catch (FileNotFoundException e) { + Log.d(LOG_TAG, "bitmapForURL() : " + fFilename + " does not exist"); + } catch (Exception e) { + Log.e(LOG_TAG, "bitmapForURL() " + e, e); + + } + + final Bitmap fBitmap = bitmap; + MXMediasCache.mUIHandler.post(new Runnable() { + @Override + public void run() { + callback.onSuccess(fBitmap); + } + }); + } + }); + + return true; + } + + //============================================================================================================== + // class methods + //============================================================================================================== + + /** + * MXMediaDownloadWorkerTask creator + * + * @param appContext the context + * @param hsConfig the home server config + * @param networkConnectivityReceiver the network connectivity receiver + * @param directoryFile the directory in which the media must be stored + * @param url the media url + * @param downloadId the predefined id of the download task for this content + * @param rotation the rotation angle (degrees), use 0 by default + * @param mimeType the mime type. + * @param encryptedFileInfo the encryption information + * @param mediaScanRestClient the media scan rest client + * @param isAvScannerEnabled tell whether an anti-virus scanner is enabled + */ + public MXMediaDownloadWorkerTask(Context appContext, + HomeServerConnectionConfig hsConfig, + NetworkConnectivityReceiver networkConnectivityReceiver, + File directoryFile, + String url, + String downloadId, + int rotation, + String mimeType, + EncryptedFileInfo encryptedFileInfo, + @Nullable MediaScanRestClient mediaScanRestClient, + boolean isAvScannerEnabled) { + mApplicationContext = appContext; + mHsConfig = hsConfig; + mNetworkConnectivityReceiver = networkConnectivityReceiver; + mDirectoryFile = directoryFile; + mUrl = url; + mDownloadId = downloadId; + mRotation = rotation; + mMimeType = mimeType; + mEncryptedFileInfo = encryptedFileInfo; + mMediaScanRestClient = mediaScanRestClient; + mIsAvScannerEnabled = isAvScannerEnabled; + + mImageViewReferences = new ArrayList<>(); + + synchronized (sPendingDownloadById) { + sPendingDownloadById.put(downloadId, this); + } + } + + /** + * MXMediaDownloadWorkerTask creator + * + * @param task another bitmap task + */ + public MXMediaDownloadWorkerTask(MXMediaDownloadWorkerTask task) { + mApplicationContext = task.mApplicationContext; + mHsConfig = task.mHsConfig; + mNetworkConnectivityReceiver = task.mNetworkConnectivityReceiver; + mDirectoryFile = task.mDirectoryFile; + mUrl = task.mUrl; + mDownloadId = task.mDownloadId; + mRotation = task.mRotation; + mMimeType = task.mMimeType; + mEncryptedFileInfo = task.mEncryptedFileInfo; + mIsAvScannerEnabled = task.mIsAvScannerEnabled; + mMediaScanRestClient = task.mMediaScanRestClient; + + mImageViewReferences = task.mImageViewReferences; + + synchronized (sPendingDownloadById) { + sPendingDownloadById.put(mDownloadId, this); + } + } + + /** + * Cancels the current download. + */ + public synchronized void cancelDownload() { + mIsDownloadCancelled = true; + } + + /** + * @return tells if the current download has been cancelled. + */ + public synchronized boolean isDownloadCancelled() { + return mIsDownloadCancelled; + } + + /** + * @return the media URL. + */ + public String getUrl() { + return mUrl; + } + + /** + * Add an imageView to the list to refresh when the bitmap is downloaded. + * + * @param imageView an image view instance to refresh. + */ + public void addImageView(ImageView imageView) { + mImageViewReferences.add(new WeakReference<>(imageView)); + } + + /** + * Set the default bitmap to use when the Url is unreachable. + * + * @param aBitmap the bitmap. + */ + public void setDefaultBitmap(Bitmap aBitmap) { + mDefaultBitmap = aBitmap; + } + + /** + * Add a download listener. + * + * @param listener the listener to add. + */ + public void addDownloadListener(IMXMediaDownloadListener listener) { + if (null != listener) { + mDownloadListeners.add(listener); + } + } + + /** + * Returns the download progress. + * + * @return the download progress + */ + public int getProgress() { + if (null != mDownloadStats) { + return mDownloadStats.mProgress; + } + + return -1; + } + + /** + * @return the download stats + */ + public IMXMediaDownloadListener.DownloadStats getDownloadStats() { + return mDownloadStats; + } + + /** + * @return true if the current task is an image one. + */ + private boolean isBitmapDownloadTask() { + return null != mMimeType && mMimeType.startsWith("image/"); + } + + /** + * Push the download progress. + * + * @param startDownloadTime the start download time. + */ + private void updateAndPublishProgress(long startDownloadTime) { + mDownloadStats.mElapsedTime = (int) ((System.currentTimeMillis() - startDownloadTime) / 1000); + + if (mDownloadStats.mFileSize > 0) { + if (mDownloadStats.mDownloadedSize >= mDownloadStats.mFileSize) { + mDownloadStats.mProgress = 99; + } else { + mDownloadStats.mProgress = (int) (mDownloadStats.mDownloadedSize * 100L / mDownloadStats.mFileSize); + } + } else { + mDownloadStats.mProgress = -1; + } + + // avoid zero div + if (System.currentTimeMillis() != startDownloadTime) { + mDownloadStats.mBitRate = (int) (mDownloadStats.mDownloadedSize * 1000L / (System.currentTimeMillis() - startDownloadTime) / 1024); + } else { + mDownloadStats.mBitRate = -1; + } + + if ((0 != mDownloadStats.mBitRate) && (mDownloadStats.mFileSize > 0) && (mDownloadStats.mFileSize > mDownloadStats.mDownloadedSize)) { + mDownloadStats.mEstimatedRemainingTime = (mDownloadStats.mFileSize - mDownloadStats.mDownloadedSize) / 1024 / mDownloadStats.mBitRate; + } else { + mDownloadStats.mEstimatedRemainingTime = -1; + } + + Log.d(LOG_TAG, "updateAndPublishProgress " + this + " : " + mDownloadStats.mProgress); + + publishProgress(); + } + + /** + * Download and decode media in background. + * + * @param params + * @return JsonElement if an error occurs + */ + @Override + protected JsonElement doInBackground(Void... params) { + JsonElement jsonElementResult = null; + + MatrixError defaultError = new MatrixError(); + defaultError.errcode = MatrixError.UNKNOWN; + + // Note: No need for access token here + + try { + URL url = new URL(mUrl); + Log.d(LOG_TAG, "MXMediaDownloadWorkerTask " + this + " starts"); + + mDownloadStats = new IMXMediaDownloadListener.DownloadStats(); + // don't known yet + mDownloadStats.mEstimatedRemainingTime = -1; + + InputStream stream = null; + + int filelen = -1; + HttpURLConnection connection = null; + + try { + connection = (HttpURLConnection) url.openConnection(); + + if (RestClient.getUserAgent() != null) { + connection.setRequestProperty("User-Agent", RestClient.getUserAgent()); + } + + if (mHsConfig != null && connection instanceof HttpsURLConnection) { + // Add SSL Socket factory. + HttpsURLConnection sslConn = (HttpsURLConnection) connection; + try { + Pair pair = CertUtil.newPinnedSSLSocketFactory(mHsConfig); + sslConn.setSSLSocketFactory(pair.first); + sslConn.setHostnameVerifier(CertUtil.newHostnameVerifier(mHsConfig)); + } catch (Exception e) { + Log.e(LOG_TAG, "doInBackground SSL exception " + e.getMessage(), e); + } + } + + // add a timeout to avoid infinite loading display. + float scale = (null != mNetworkConnectivityReceiver) ? mNetworkConnectivityReceiver.getTimeoutScale() : 1.0f; + connection.setReadTimeout((int) (DOWNLOAD_TIME_OUT * scale)); + + if (mIsAvScannerEnabled && null != mEncryptedFileInfo) { + // POST the encryption info to let the av scanner decrypt and scan the content. + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); + connection.setDoOutput(true); + connection.setUseCaches(false); + + EncryptedMediaScanBody encryptedMediaScanBody = new EncryptedMediaScanBody(); + encryptedMediaScanBody.encryptedFileInfo = mEncryptedFileInfo; + + String data = JsonUtils.getCanonicalizedJsonString(encryptedMediaScanBody); + + // Encrypt the data, if antivirus server supports it + String publicServerKey = getAntivirusServerPublicKey(); + + if (publicServerKey == null) { + // Error + throw new Exception("Unable to get public key"); + } else if (!TextUtils.isEmpty(publicServerKey)) { + OlmPkEncryption olmPkEncryption = new OlmPkEncryption(); + + olmPkEncryption.setRecipientKey(publicServerKey); + + OlmPkMessage message = olmPkEncryption.encrypt(data); + + EncryptedMediaScanEncryptedBody encryptedMediaScanEncryptedBody = new EncryptedMediaScanEncryptedBody(); + encryptedMediaScanEncryptedBody.encryptedBodyFileInfo = new EncryptedBodyFileInfo(message); + + data = JsonUtils.getCanonicalizedJsonString(encryptedMediaScanEncryptedBody); + } + // Else: no public key on this server, do not encrypt data + + OutputStream outputStream = connection.getOutputStream(); + try { + outputStream.write(data.getBytes("UTF-8")); + } catch (Exception e) { + Log.e(LOG_TAG, "doInBackground Failed to serialize encryption info " + e.getMessage(), e); + } finally { + outputStream.close(); + } + } + + filelen = connection.getContentLength(); + stream = connection.getInputStream(); + } catch (Exception e) { + Log.e(LOG_TAG, "bitmapForURL : fail to open the connection " + e.getMessage(), e); + defaultError.error = e.getLocalizedMessage(); + + // In case of 403, revert the key + if (connection.getResponseCode() == 403 && mMediaScanRestClient != null) { + mMediaScanRestClient.resetServerPublicKey(); + } + + InputStream errorStream = connection.getErrorStream(); + + if (null != errorStream) { + try { + BufferedReader streamReader = new BufferedReader(new InputStreamReader(errorStream, "UTF-8")); + StringBuilder responseStrBuilder = new StringBuilder(); + + String inputStr; + + while ((inputStr = streamReader.readLine()) != null) { + responseStrBuilder.append(inputStr); + } + + jsonElementResult = new JsonParser().parse(responseStrBuilder.toString()); + } catch (Exception ee) { + Log.e(LOG_TAG, "bitmapForURL : Error parsing error " + ee.getMessage(), ee); + } + } + + // privacy + //Log.d(LOG_TAG, "MediaWorkerTask " + mUrl + " does not exist"); + Log.d(LOG_TAG, "MediaWorkerTask an url does not exist"); + + // If some medias are not found, + // do not try to reload them until the next application launch. + // We mark this url as unreachable. + // We can do this only if the av scanner is disabled or if the media is unencrypted, + // (because the same url is used for all encrypted media when the av scanner is enabled). + if (!mIsAvScannerEnabled || null == mEncryptedFileInfo) { + synchronized (sUnreachableUrls) { + sUnreachableUrls.add(mUrl); + } + } + } + + dispatchDownloadStart(); + + // failed to open the remote stream without having exception + if ((null == stream) && (null == jsonElementResult)) { + jsonElementResult = new JsonParser().parse("Cannot open " + mUrl); + + // if some medias are not found + // do not try to reload them until the next application launch. + // We mark this url as unreachable. + // We can do this only if the av scanner is disabled or if the media is unencrypted, + // (because the same url is used for all encrypted media when the av scanner is enabled). + if (!mIsAvScannerEnabled || null == mEncryptedFileInfo) { + synchronized (sUnreachableUrls) { + sUnreachableUrls.add(mUrl); + } + } + } + + // test if the download has not been cancelled + if (!isDownloadCancelled() && (null == jsonElementResult)) { + final long startDownloadTime = System.currentTimeMillis(); + + String filename = buildFileName(mDownloadId, mMimeType) + ".tmp"; + FileOutputStream fos = new FileOutputStream(new File(mDirectoryFile, filename)); + + mDownloadStats.mDownloadId = mDownloadId; + mDownloadStats.mProgress = 0; + mDownloadStats.mDownloadedSize = 0; + mDownloadStats.mFileSize = filelen; + mDownloadStats.mElapsedTime = 0; + mDownloadStats.mEstimatedRemainingTime = -1; + mDownloadStats.mBitRate = 0; + + // Publish progress every 100ms + final Timer refreshTimer = new Timer(); + + refreshTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + if (!mIsDone) { + updateAndPublishProgress(startDownloadTime); + } + } + }, new Date(), 100); + + try { + byte[] buf = new byte[DOWNLOAD_BUFFER_READ_SIZE]; + int len; + while (!isDownloadCancelled() && (len = stream.read(buf)) != -1) { + fos.write(buf, 0, len); + mDownloadStats.mDownloadedSize += len; + } + + if (!isDownloadCancelled()) { + mDownloadStats.mProgress = 100; + } + } catch (OutOfMemoryError outOfMemoryError) { + Log.e(LOG_TAG, "doInBackground: out of memory", outOfMemoryError); + defaultError.error = outOfMemoryError.getLocalizedMessage(); + } catch (Exception e) { + Log.e(LOG_TAG, "doInBackground fail to read image " + e.getMessage(), e); + defaultError.error = e.getLocalizedMessage(); + } + + mIsDone = true; + + close(stream); + fos.flush(); + fos.close(); + + refreshTimer.cancel(); + + if ((null != connection) && (connection instanceof HttpsURLConnection)) { + connection.disconnect(); + } + + // the file has been successfully downloaded + if (mDownloadStats.mProgress == 100) { + try { + File originalFile = new File(mDirectoryFile, filename); + String newFileName = buildFileName(mDownloadId, mMimeType); + File newFile = new File(mDirectoryFile, newFileName); + if (newFile.exists()) { + // Or you could throw here. + mApplicationContext.deleteFile(newFileName); + } + originalFile.renameTo(newFile); + } catch (Exception e) { + Log.e(LOG_TAG, "doInBackground : renaming error " + e.getMessage(), e); + defaultError.error = e.getLocalizedMessage(); + } + } + } + + if (mDownloadStats.mProgress == 100) { + Log.d(LOG_TAG, "The download " + this + " is done."); + } else { + if (null != jsonElementResult) { + Log.d(LOG_TAG, "The download " + this + " failed : mErrorAsJsonElement " + jsonElementResult.toString()); + } else { + Log.d(LOG_TAG, "The download " + this + " failed."); + } + } + } catch (Exception e) { + Log.e(LOG_TAG, "Unable to download media " + this, e); + defaultError.error = e.getMessage(); + } + + // build a JSON from the error + if (!TextUtils.isEmpty(defaultError.error)) { + jsonElementResult = JsonUtils.getGson(false).toJsonTree(defaultError); + } + + // remove the task from the loading one + synchronized (sPendingDownloadById) { + sPendingDownloadById.remove(mDownloadId); + } + + return jsonElementResult; + } + + /** + * Get the public key of the antivirus server + * + * @return either empty string if server does not provide the public key, null in case of error, or the public server key + */ + @Nullable + private String getAntivirusServerPublicKey() { + if (mMediaScanRestClient == null) { + // Error + Log.e(LOG_TAG, "Mandatory mMediaScanRestClient is null"); + return null; + } + + // Make async request sync with a CountDownLatch + // It is easier than adding a method to get the server public key synchronously with Call.execute() + final CountDownLatch latch = new CountDownLatch(1); + final String[] publicServerKey = new String[1]; + + mMediaScanRestClient.getServerPublicKey(new ApiCallback() { + @Override + public void onSuccess(String serverPublicKey) { + publicServerKey[0] = serverPublicKey; + latch.countDown(); + } + + @Override + public void onNetworkError(Exception e) { + latch.countDown(); + } + + @Override + public void onMatrixError(MatrixError e) { + latch.countDown(); + } + + @Override + public void onUnexpectedError(Exception e) { + latch.countDown(); + } + }); + + try { + latch.await(30, TimeUnit.SECONDS); + } catch (InterruptedException ie) { + + } + + return publicServerKey[0]; + } + + /** + * Close the stream. + * + * @param stream the stream to close. + */ + private void close(InputStream stream) { + try { + stream.close(); + } catch (Exception e) { + Log.e(LOG_TAG, "close error " + e.getMessage(), e); + } + } + + @Override + protected void onProgressUpdate(Void... aVoid) { + super.onProgressUpdate(); + dispatchOnDownloadProgress(mDownloadStats); + } + + // Once complete, see if ImageView is still around and set bitmap. + @Override + protected void onPostExecute(JsonElement jsonElementError) { + if (null != jsonElementError) { + dispatchOnDownloadError(jsonElementError); + } else if (isDownloadCancelled()) { + dispatchDownloadCancel(); + } else { + dispatchOnDownloadComplete(); + + // image download + // update the linked ImageViews. + if (isBitmapDownloadTask()) { + // retrieve the bitmap from the file s + if (!bitmapForURL(mApplicationContext, mDirectoryFile, mUrl, mDownloadId, mRotation, mMimeType, mEncryptedFileInfo, + new SimpleApiCallback() { + @Override + public void onSuccess(Bitmap bitmap) { + setBitmap((null == bitmap) ? mDefaultBitmap : bitmap); + } + })) { + setBitmap(mDefaultBitmap); + } + } + } + } + + /** + * Set the bitmap in a referenced imageview + * + * @param bitmap the bitmap + */ + private void setBitmap(Bitmap bitmap) { + // update the imageViews image + if (bitmap != null) { + for (WeakReference weakRef : mImageViewReferences) { + final ImageView imageView = weakRef.get(); + + if (imageView != null && TextUtils.equals(mDownloadId, (String) imageView.getTag())) { + imageView.setImageBitmap(bitmap); + } + } + } + } + + + //============================================================================================================== + // Dispatchers + //============================================================================================================== + + /** + * Dispatch start event to the callbacks. + */ + private void dispatchDownloadStart() { + for (IMXMediaDownloadListener callback : mDownloadListeners) { + try { + callback.onDownloadStart(mDownloadId); + } catch (Exception e) { + Log.e(LOG_TAG, "dispatchDownloadStart error " + e.getMessage(), e); + } + } + } + + /** + * Dispatch stats update to the callbacks. + * + * @param stats the new stats value + */ + private void dispatchOnDownloadProgress(IMXMediaDownloadListener.DownloadStats stats) { + for (IMXMediaDownloadListener callback : mDownloadListeners) { + try { + callback.onDownloadProgress(mDownloadId, stats); + } catch (Exception e) { + Log.e(LOG_TAG, "dispatchOnDownloadProgress error " + e.getMessage(), e); + } + } + } + + /** + * Dispatch error message. + * + * @param jsonElement the Json error + */ + private void dispatchOnDownloadError(JsonElement jsonElement) { + for (IMXMediaDownloadListener callback : mDownloadListeners) { + try { + callback.onDownloadError(mDownloadId, jsonElement); + } catch (Exception e) { + Log.e(LOG_TAG, "dispatchOnDownloadError error " + e.getMessage(), e); + } + } + } + + /** + * Dispatch end of download + */ + private void dispatchOnDownloadComplete() { + for (IMXMediaDownloadListener callback : mDownloadListeners) { + try { + callback.onDownloadComplete(mDownloadId); + } catch (Exception e) { + Log.e(LOG_TAG, "dispatchOnDownloadComplete error " + e.getMessage(), e); + } + } + } + + /** + * Dispatch download cancel + */ + private void dispatchDownloadCancel() { + for (IMXMediaDownloadListener callback : mDownloadListeners) { + try { + callback.onDownloadCancel(mDownloadId); + } catch (Exception e) { + Log.e(LOG_TAG, "dispatchDownloadCancel error " + e.getMessage(), e); + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/db/MXMediaUploadWorkerTask.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/db/MXMediaUploadWorkerTask.java new file mode 100644 index 0000000000..2ec1f01f78 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/db/MXMediaUploadWorkerTask.java @@ -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 { + + private static final String LOG_TAG = MXMediaUploadWorkerTask.class.getSimpleName(); + + // upload ID -> task + private static final Map mPendingUploadByUploadId = new HashMap<>(); + + // progress listener + private final List 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 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 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); + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/db/MXMediasCache.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/db/MXMediasCache.java new file mode 100644 index 0000000000..ab2ecced28 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/db/MXMediasCache.java @@ -0,0 +1,1522 @@ +/* + * 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.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.ExifInterface; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.webkit.MimeTypeMap; +import android.widget.ImageView; + +import im.vector.matrix.android.internal.legacy.HomeServerConnectionConfig; +import im.vector.matrix.android.internal.legacy.crypto.MXEncryptedAttachments; +import im.vector.matrix.android.internal.legacy.listeners.IMXMediaDownloadListener; +import im.vector.matrix.android.internal.legacy.listeners.IMXMediaUploadListener; +import im.vector.matrix.android.internal.legacy.listeners.MXMediaDownloadListener; +import im.vector.matrix.android.internal.legacy.network.NetworkConnectivityReceiver; +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.MediaScanRestClient; +import im.vector.matrix.android.internal.legacy.rest.model.crypto.EncryptedFileInfo; +import im.vector.matrix.android.internal.legacy.util.ContentManager; +import im.vector.matrix.android.internal.legacy.util.ContentUtils; +import im.vector.matrix.android.internal.legacy.util.Log; +import im.vector.matrix.android.internal.legacy.util.MXOsHandler; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.concurrent.RejectedExecutionException; + +public class MXMediasCache { + + private static final String LOG_TAG = MXMediasCache.class.getSimpleName(); + + /** + * The medias folders. + */ + // Put the previous folders used for cache here. Every time the cache management change (change of id format, etc.), + // append the current cache folder to this list, and change value of MXMEDIA_STORE_FOLDER (typically increment the value) + private static final List sPreviousMediaCacheFolders = Arrays.asList( + "MXMediaStore", + "MXMediaStore2" + ); + + private static final String MXMEDIA_STORE_FOLDER = "MXMediaStore3"; + private static final String MXMEDIA_STORE_MEMBER_THUMBNAILS_FOLDER = "MXMemberThumbnailsStore"; + private static final String MXMEDIA_STORE_IMAGES_FOLDER = "Images"; + private static final String MXMEDIA_STORE_OTHERS_FOLDER = "Others"; + private static final String MXMEDIA_STORE_TMP_FOLDER = "tmp"; + private static final String MXMEDIA_STORE_SHARE_FOLDER = "share"; + + /** + * The content manager + */ + private ContentManager mContentManager; + + /** + * The medias folders list. + */ + private File mMediasFolderFile; + private File mImagesFolderFile; + private File mOthersFolderFile; + private File mThumbnailsFolderFile; + + // This folder will contain decrypted media files + private File mTmpFolderFile; + + // This folder will contain decrypted media files, for file sharing + private File mShareFolderFile; + + // track the network updates + private final NetworkConnectivityReceiver mNetworkConnectivityReceiver; + + // the background thread + static HandlerThread mDecryptingHandlerThread = null; + static MXOsHandler mDecryptingHandler = null; + static android.os.Handler mUIHandler = null; + + private MediaScanRestClient mMediaScanRestClient; + + /** + * Constructor + * + * @param contentManager the content manager. + * @param networkConnectivityReceiver the network connectivity receiver + * @param userID the account user Id. + * @param context the context + */ + public MXMediasCache(ContentManager contentManager, NetworkConnectivityReceiver networkConnectivityReceiver, String userID, Context context) { + mContentManager = contentManager; + mNetworkConnectivityReceiver = networkConnectivityReceiver; + + File mediaBaseFolderFile; + + // Clear previous cache + for (String previousMediaCacheFolder : sPreviousMediaCacheFolders) { + mediaBaseFolderFile = new File(context.getApplicationContext().getFilesDir(), previousMediaCacheFolder); + + if (mediaBaseFolderFile.exists()) { + ContentUtils.deleteDirectory(mediaBaseFolderFile); + } + } + + mediaBaseFolderFile = new File(context.getApplicationContext().getFilesDir(), MXMEDIA_STORE_FOLDER); + + if (!mediaBaseFolderFile.exists()) { + mediaBaseFolderFile.mkdirs(); + } + + // create the dir tree + mMediasFolderFile = new File(mediaBaseFolderFile, userID); + mImagesFolderFile = new File(mMediasFolderFile, MXMEDIA_STORE_IMAGES_FOLDER); + mOthersFolderFile = new File(mMediasFolderFile, MXMEDIA_STORE_OTHERS_FOLDER); + mTmpFolderFile = new File(mMediasFolderFile, MXMEDIA_STORE_TMP_FOLDER); + + if (mTmpFolderFile.exists()) { + ContentUtils.deleteDirectory(mTmpFolderFile); + } + mTmpFolderFile.mkdirs(); + + mShareFolderFile = new File(mMediasFolderFile, MXMEDIA_STORE_SHARE_FOLDER); + + if (mShareFolderFile.exists()) { + ContentUtils.deleteDirectory(mShareFolderFile); + } + mShareFolderFile.mkdirs(); + + mThumbnailsFolderFile = new File(mediaBaseFolderFile, MXMEDIA_STORE_MEMBER_THUMBNAILS_FOLDER); + + // use the same thread for all the sessions + if (null == mDecryptingHandlerThread) { + mDecryptingHandlerThread = new HandlerThread("MXMediaDecryptingBackgroundThread", Thread.MIN_PRIORITY); + mDecryptingHandlerThread.start(); + mDecryptingHandler = new MXOsHandler(mDecryptingHandlerThread.getLooper()); + mUIHandler = new Handler(Looper.getMainLooper()); + } + } + + /** + * Returns the mediasFolder files. + * Creates it if it does not exist + * + * @return the medias folder file. + */ + private File getMediasFolderFile() { + if (!mMediasFolderFile.exists()) { + mMediasFolderFile.mkdirs(); + } + + return mMediasFolderFile; + } + + /** + * Returns the folder file for a dedicated mimetype. + * Creates it if it does not exist + * + * @param mimeType the media mimetype. + * @return the folder file. + */ + private File getFolderFile(String mimeType) { + File file; + + // + if ((null == mimeType) || mimeType.startsWith("image/")) { + file = mImagesFolderFile; + } else { + file = mOthersFolderFile; + } + + if (!file.exists()) { + file.mkdirs(); + } + + return file; + } + + /** + * Returns the thumbnails folder. + * Creates it if it does not exist + * + * @return the thumbnails folder file. + */ + private File getThumbnailsFolderFile() { + if (!mThumbnailsFolderFile.exists()) { + mThumbnailsFolderFile.mkdirs(); + } + + return mThumbnailsFolderFile; + } + + /** + * Compute the medias cache size + * + * @param context the context + * @param callback the asynchronous callback + */ + public static void getCachesSize(final Context context, final ApiCallback callback) { + AsyncTask task = new AsyncTask() { + @Override + protected Long doInBackground(Void... params) { + return ContentUtils.getDirectorySize(context, + new File(context.getApplicationContext().getFilesDir(), MXMEDIA_STORE_FOLDER), + 1); + } + + @Override + protected void onPostExecute(Long result) { + Log.d(LOG_TAG, "## getCachesSize() : " + result); + if (null != callback) { + callback.onSuccess(result); + } + } + }; + try { + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } catch (Exception e) { + Log.e(LOG_TAG, "## getCachesSize() : failed " + e.getMessage(), e); + task.cancel(true); + } + } + + /** + * Remove medias older than ts + * + * @param ts the ts + * @param filePathToKeep set of files to keep + * @return length of deleted files + */ + public long removeMediasBefore(long ts, Set filePathToKeep) { + long length = 0; + + length += removeMediasBefore(getMediasFolderFile(), ts, filePathToKeep); + length += removeMediasBefore(getThumbnailsFolderFile(), ts, filePathToKeep); + + return length; + } + + /** + * Recursive method to remove older messages + * + * @param folder the base folder + * @param aTs the ts + * @param filePathToKeep set of files to keep + * @return length of deleted files + */ + private long removeMediasBefore(File folder, long aTs, Set filePathToKeep) { + long length = 0; + File[] files = folder.listFiles(); + + if (null != files) { + for (int i = 0; i < files.length; i++) { + File file = files[i]; + + if (!file.isDirectory()) { + + if (!filePathToKeep.contains(file.getPath())) { + long ts = ContentUtils.getLastAccessTime(file); + if (ts < aTs) { + length += file.length(); + file.delete(); + } + } + } else { + length += removeMediasBefore(file, aTs, filePathToKeep); + } + } + } + + return length; + } + + /** + * Clear the medias caches. + */ + public void clear() { + ContentUtils.deleteDirectory(getMediasFolderFile()); + + ContentUtils.deleteDirectory(mThumbnailsFolderFile); + + // clear the media cache + MXMediaDownloadWorkerTask.clearBitmapsCache(); + + // cancel pending uploads. + MXMediaUploadWorkerTask.cancelPendingUploads(); + } + + /** + * The thumbnails cached is not cleared when logging out a session + * because many sessions share the same thumbnails. + * This method must be called when performing an application logout + * i.e. logging out of all sessions. + * + * @param applicationContext the application context + */ + public static void clearThumbnailsCache(Context applicationContext) { + ContentUtils.deleteDirectory(new File(new File(applicationContext.getApplicationContext().getFilesDir(), MXMediasCache.MXMEDIA_STORE_FOLDER), + MXMEDIA_STORE_MEMBER_THUMBNAILS_FOLDER)); + } + + /** + * Provide the thumbnail file. + * + * @param url the thumbnail url/ + * @param size the thumbnail size. + * @return the File if it exits. + */ + @Nullable + public File thumbnailCacheFile(String url, int size) { + // We use the download task id to define a cache id + String thumbnailCacheId = mContentManager.downloadTaskIdForMatrixMediaContent(url); + + if (null != thumbnailCacheId) { + if (size > 0) { + thumbnailCacheId += "_w_" + size + "_h_" + size; + } + String filename = MXMediaDownloadWorkerTask.buildFileName(thumbnailCacheId, "image/jpeg"); + + try { + File file = new File(getThumbnailsFolderFile(), filename); + + if (file.exists()) { + return file; + } + } catch (Exception e) { + Log.e(LOG_TAG, "thumbnailCacheFile failed " + e.getMessage(), e); + } + } + + return null; + } + + /** + * Return the cache file name for a media defined by its URL and its mime type. + * + * @param url the media URL + * @param width the media width + * @param height the media height + * @param mimeType the media mime type + * @return the media file it is found + */ + @Nullable + private File mediaCacheFile(String url, int width, int height, String mimeType) { + // sanity check + if (null == url) { + return null; + } + + String filename; + if (url.startsWith("file:")) { + filename = url; + } else { + // We use the download task id to define a cache id + String cacheId = mContentManager.downloadTaskIdForMatrixMediaContent(url); + if (null != cacheId) { + if ((width > 0) && (height > 0)) { + cacheId += "_w_" + width + "_h_" + height; + } + filename = MXMediaDownloadWorkerTask.buildFileName(cacheId, mimeType); + } else { + return null; + } + } + + try { + // already a local file + if (filename.startsWith("file:")) { + Uri uri = Uri.parse(filename); + filename = uri.getLastPathSegment(); + } + + File file = new File(getFolderFile(mimeType), filename); + + if (file.exists()) { + return file; + } + + } catch (Exception e) { + Log.e(LOG_TAG, "mediaCacheFile failed " + e.getMessage(), e); + } + + return null; + } + + /** + * Tells if a media is cached + * + * @param url the url + * @param mimeType the mimetype + * @return true if the media is cached + */ + public boolean isMediaCached(String url, String mimeType) { + return isMediaCached(url, -1, -1, mimeType); + } + + /** + * Tells if a media is cached + * + * @param url the media URL + * @param width the media width + * @param height the media height + * @param mimeType the media mime type + * @return the media file is cached + */ + public boolean isMediaCached(String url, int width, int height, String mimeType) { + return null != mediaCacheFile(url, width, height, mimeType); + } + + /** + * Create a temporary decrypted copy of a media. + * It must be released when it is not used anymore with clearTmpDecryptedMediaCache(). + * + * @param url the media url + * @param mimeType the media mime type + * @param encryptedFileInfo the encryption information + * @param callback the asynchronous callback + * @return true if the file is cached + */ + public boolean createTmpDecryptedMediaFile(String url, + String mimeType, + EncryptedFileInfo encryptedFileInfo, + ApiCallback callback) { + return createTmpDecryptedMediaFile(url, + -1, + -1, + mimeType, + encryptedFileInfo, + callback); + } + + /** + * Create a temporary decrypted copy of a media. + * It must be released when it is not used anymore with clearTmpDecryptedMediaCache(). + * + * @param url the media URL + * @param width the media width + * @param height the media height + * @param mimeType the media mime type + * @param encryptedFileInfo the encryption information + * @param callback the asynchronous callback + * @return true if the file is cached + */ + public boolean createTmpDecryptedMediaFile(String url, + int width, + int height, + String mimeType, + final EncryptedFileInfo encryptedFileInfo, + final ApiCallback callback) { + final File file = mediaCacheFile(url, width, height, mimeType); + + if (null != file) { + mDecryptingHandler.post(new Runnable() { + @Override + public void run() { + final File tmpFile = new File(mTmpFolderFile, file.getName()); + + // create it only if it does not exist yet + if (!tmpFile.exists()) { + try { + InputStream fis = new FileInputStream(file); + + if (null != encryptedFileInfo) { + InputStream is = MXEncryptedAttachments.decryptAttachment(fis, encryptedFileInfo); + fis.close(); + fis = is; + } + + FileOutputStream fos = new FileOutputStream(tmpFile); + byte[] buf = new byte[2048]; + int len; + while ((len = fis.read(buf)) != -1) { + fos.write(buf, 0, len); + } + + fis.close(); + fos.close(); + } catch (Exception e) { + Log.e(LOG_TAG, "## createTmpDecryptedMediaFile() failed " + e.getMessage(), e); + } + } + + mUIHandler.post(new Runnable() { + @Override + public void run() { + callback.onSuccess(tmpFile); + } + }); + } + }); + } + return (null != file); + } + + /** + * Clear the temporary decrypted media cache folder + */ + public void clearTmpDecryptedMediaCache() { + Log.d(LOG_TAG, "clearTmpDecryptedMediaCache()"); + + if (mTmpFolderFile.exists()) { + ContentUtils.deleteDirectory(mTmpFolderFile); + } + + if (!mTmpFolderFile.exists()) { + mTmpFolderFile.mkdirs(); + } + } + + /** + * Move a decrypted media file to the /share folder, to avoid this file to be deleted if in the /tmp folder + * + * @param fileToMove The file to move + * @param filename the filename, without path + * @return The copied file in the Share folder location + */ + public File moveToShareFolder(final File fileToMove, + final String filename) { + File dstFile = new File(mShareFolderFile, filename); + + if (dstFile.exists()) { + if (!dstFile.delete()) { + Log.w(LOG_TAG, "Unable to delete file"); + } + } + + if (!fileToMove.renameTo(dstFile)) { + Log.w(LOG_TAG, "Unable to rename file"); + + // Return the original file + return fileToMove; + } + + return dstFile; + } + + /** + * Clear the temporary shared decrypted media cache folder + */ + public void clearShareDecryptedMediaCache() { + Log.d(LOG_TAG, "clearShareDecryptedMediaCache()"); + + if (mShareFolderFile.exists()) { + ContentUtils.deleteDirectory(mShareFolderFile); + } + + if (!mShareFolderFile.exists()) { + mShareFolderFile.mkdirs(); + } + } + + /** + * Save a bitmap to the local cache + * it could be used for unsent media to allow them to be resent. + * + * @param bitmap the bitmap to save + * @param defaultFileName the filename is provided, if null, a filename will be generated + * @return the media cache URL + */ + public String saveBitmap(Bitmap bitmap, String defaultFileName) { + String filename = "file" + System.currentTimeMillis() + ".jpg"; + String cacheURL = null; + + try { + if (null != defaultFileName) { + File file = new File(getFolderFile(null), defaultFileName); + file.delete(); + + filename = Uri.fromFile(file).getLastPathSegment(); + } + + File file = new File(getFolderFile(null), filename); + FileOutputStream fos = new FileOutputStream(file.getPath()); + + // We got an java.lang.IllegalStateException: Can't compress a recycled bitmap + if (bitmap.isRecycled()) { + Log.w(LOG_TAG, "Trying to compress a recycled Bitmap. Create a copy first."); + bitmap = Bitmap.createBitmap(bitmap); + } + + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos); + + fos.flush(); + fos.close(); + + cacheURL = Uri.fromFile(file).toString(); + } catch (Exception e) { + Log.e(LOG_TAG, "saveBitmap failed " + e.getMessage(), e); + } + + return cacheURL; + } + + /** + * Save a media to the local cache + * it could be used for unsent media to allow them to be resent. + * + * @param stream the file stream to save + * @param defaultFileName the filename is provided, if null, a filename will be generated + * @param mimeType the mime type. + * @return the media cache URL + */ + public String saveMedia(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) { + if (mimeType.lastIndexOf("/") >= 0) { + extension = mimeType.substring(mimeType.lastIndexOf("/") + 1); + } + } + + if (!TextUtils.isEmpty(extension)) { + filename += "." + extension; + } + } + } + + String cacheURL = null; + + try { + File file = new File(getFolderFile(mimeType), 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, "saveMedia failed " + e.getMessage(), e); + } + + fos.flush(); + fos.close(); + stream.close(); + + cacheURL = Uri.fromFile(file).toString(); + } catch (Exception e) { + Log.e(LOG_TAG, "saveMedia failed " + e.getMessage(), e); + + } + + return cacheURL; + } + + /** + * Replace a media cache by a file content. + * + * @param mediaUrl the mediaUrl + * @param mimeType the mimeType. + * @param fileUrl the file which replaces the cached media. + */ + public void saveFileMediaForUrl(String mediaUrl, + String fileUrl, + String mimeType) { + saveFileMediaForUrl(mediaUrl, + fileUrl, + -1, + -1, + mimeType); + } + + /** + * Replace a media cache by a file content. + * MediaUrl is the same model as the one used in loadBitmap. + * + * @param mediaUrl the mediaUrl + * @param fileUrl the file which replaces the cached media. + * @param width the expected image width + * @param height the expected image height + * @param mimeType the mimeType. + */ + public void saveFileMediaForUrl(String mediaUrl, + String fileUrl, + int width, + int height, + String mimeType) { + saveFileMediaForUrl(mediaUrl, + fileUrl, + width, + height, + mimeType, + false); + } + + /** + * Copy or Replace a media cache by a file content. + * MediaUrl is the same model as the one used in loadBitmap. + * + * @param mediaUrl the mediaUrl + * @param fileUrl the file which replaces the cached media. + * @param width the expected image width + * @param height the expected image height + * @param mimeType the mimeType. + * @param keepSource keep the source file + */ + public void saveFileMediaForUrl(String mediaUrl, + String fileUrl, + int width, + int height, + String mimeType, + boolean keepSource) { + // We use the download task id to define a cache id + String cacheId = mContentManager.downloadTaskIdForMatrixMediaContent(mediaUrl); + if (null != cacheId) { + if ((width > 0) && (height > 0)) { + cacheId += "_w_" + width + "_h_" + height; + } + String filename = MXMediaDownloadWorkerTask.buildFileName(cacheId, mimeType); + + try { + // delete the current content + File destFile = new File(getFolderFile(mimeType), filename); + + if (destFile.exists()) { + try { + destFile.delete(); + } catch (Exception e) { + Log.e(LOG_TAG, "saveFileMediaForUrl delete failed " + e.getMessage(), e); + } + } + + Uri uri = Uri.parse(fileUrl); + File srcFile = new File(uri.getPath()); + + if (keepSource) { + InputStream in = new FileInputStream(srcFile); + OutputStream out = new FileOutputStream(destFile); + + // Transfer bytes from in to out + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + in.close(); + out.close(); + } else { + srcFile.renameTo(destFile); + } + + } catch (Exception e) { + Log.e(LOG_TAG, "saveFileMediaForUrl failed " + e.getMessage(), e); + } + } + } + + /** + * Load an avatar thumbnail. + * The imageView image is updated when the bitmap is loaded or downloaded. + * + * @param hsConfig the home server config. + * @param imageView Ihe imageView to update with the image. + * @param url the image url + * @param side the avatar thumbnail side + * @return a download identifier if the image is not cached else null. + */ + public String loadAvatarThumbnail(HomeServerConnectionConfig hsConfig, + ImageView imageView, + String url, + int side) { + return loadBitmap(imageView.getContext(), + hsConfig, + imageView, + url, + side, + side, + 0, + ExifInterface.ORIENTATION_UNDEFINED, + null, + getThumbnailsFolderFile(), + null); + } + + /** + * Load an avatar thumbnail. + * The imageView image is updated when the bitmap is loaded or downloaded. + * + * @param hsConfig the home server config. + * @param imageView Ihe imageView to update with the image. + * @param url the image url + * @param side the avatar thumbnail side + * @param aDefaultAvatar the avatar to use when the Url is not reachable. + * @return a download identifier if the image is not cached else null. + */ + public String loadAvatarThumbnail(HomeServerConnectionConfig hsConfig, + ImageView imageView, + String url, + int side, + Bitmap aDefaultAvatar) { + return loadBitmap(imageView.getContext(), + hsConfig, + imageView, + url, + side, + side, + 0, + ExifInterface.ORIENTATION_UNDEFINED, + null, + getThumbnailsFolderFile(), + aDefaultAvatar, + null); + } + + /** + * Tells if the avatar is cached + * + * @param url the avatar url to test + * @param size the thumbnail size + * @return true if the avatar bitmap is cached. + */ + public boolean isAvatarThumbnailCached(String url, int size) { + boolean isCached = false; + + // We use the download task id to define a cache id + String thumbnailCacheId = mContentManager.downloadTaskIdForMatrixMediaContent(url); + if (null != thumbnailCacheId) { + if (size > 0) { + thumbnailCacheId += "_w_" + size + "_h_" + size; + } + isCached = MXMediaDownloadWorkerTask.isMediaCached(thumbnailCacheId); + + if (!isCached) { + try { + isCached = (new File(getThumbnailsFolderFile(), MXMediaDownloadWorkerTask.buildFileName(thumbnailCacheId, "image/jpeg"))).exists(); + } catch (Throwable t) { + Log.e(LOG_TAG, "## isAvatarThumbnailCached() : failed " + t.getMessage(), t); + } + } + } + + return isCached; + } + + /** + * Tells if the media URL is unreachable. + * + * @param url the url to test. + * @return true if the media URL is unreachable. + */ + public static boolean isMediaUrlUnreachable(String url) { + return MXMediaDownloadWorkerTask.isMediaUrlUnreachable(url); + } + + /** + * Load a bitmap from the url. + * The imageView image is updated when the bitmap is loaded or downloaded. + * + * @param hsConfig The home server config. + * @param imageView The imageView to update with the image. + * @param url the image url + * @param rotationAngle the rotation angle (degrees) + * @param orientation the orientation (ExifInterface.ORIENTATION_XXX value) + * @param mimeType the mimeType. + * @param encryptionInfo the encryption file info + * @return a download identifier if the image is not cached else null. + */ + public String loadBitmap(HomeServerConnectionConfig hsConfig, + ImageView imageView, + String url, + int rotationAngle, + int orientation, + String mimeType, + EncryptedFileInfo encryptionInfo) { + return loadBitmap(hsConfig, + imageView, + url, + -1, + -1, + rotationAngle, + orientation, + mimeType, + encryptionInfo); + } + + /** + * Load a bitmap from the url. + * The imageView image is updated when the bitmap is loaded or downloaded. + * + * @param hsConfig The home server config. + * @param context The context + * @param url the image url + * @param rotationAngle the rotation angle (degrees) + * @param orientation the orientation (ExifInterface.ORIENTATION_XXX value) + * @param mimeType the mimeType. + * @param encryptionInfo the encryption file info + * @return a download identifier if the image is not cached. + */ + public String loadBitmap(Context context, + HomeServerConnectionConfig hsConfig, + String url, + int rotationAngle, + int orientation, + String mimeType, + EncryptedFileInfo encryptionInfo) { + return loadBitmap(context, + hsConfig, + null, + url, + -1, + -1, + rotationAngle, + orientation, + mimeType, + getFolderFile(mimeType), + encryptionInfo); + } + + /** + * Load a bitmap from an url. + * The imageView image is updated when the bitmap is loaded or downloaded. + * The width/height parameters are optional. If they are positive, download a thumbnail. + * rotationAngle is set to Integer.MAX_VALUE when undefined : the EXIF metadata must be checked. + * + * @param hsConfig The home server config. + * @param imageView The imageView to fill when the image is downloaded + * @param url the image url + * @param width the expected image width + * @param height the expected image height + * @param rotationAngle the rotation angle (degrees) + * @param orientation the orientation (ExifInterface.ORIENTATION_XXX value) + * @param mimeType the mimeType. + * @param encryptionInfo the encryption file info + * @return a download identifier if the image is not cached + */ + public String loadBitmap(HomeServerConnectionConfig hsConfig, + ImageView imageView, + String url, + int width, + int height, + int rotationAngle, + int orientation, + String mimeType, + EncryptedFileInfo encryptionInfo) { + return loadBitmap(imageView.getContext(), + hsConfig, + imageView, + url, + width, + height, + rotationAngle, + orientation, + mimeType, + getFolderFile(mimeType), + encryptionInfo); + } + + // some tasks have been stacked because there are too many running ones. + private final List mSuspendedTasks = new ArrayList<>(); + + /** + * Check whether a download is in progress for the content at a Matrix media content URI + * (in the form of "mxc://..."). Returns the identifier of the download task if any. + * + * @param contentUrl the matrix media url + * @return the download ID if there is a pending download or null + */ + @Nullable + public String downloadIdFromUrl(String contentUrl) { + // Check and resolve the provided URL, the resulting URL is used as download identifier. + String downloadId = mContentManager.downloadTaskIdForMatrixMediaContent(contentUrl); + + if (null != downloadId && null != MXMediaDownloadWorkerTask.getMediaDownloadWorkerTask(downloadId)) { + return downloadId; + } + + return null; + } + + /** + * Download a media. + * + * @param context the application context + * @param hsConfig the home server config. + * @param url the media url + * @param mimeType the media mimetype + * @param encryptionInfo the encryption information + * @return the download identifier if there is a pending download else null + */ + public String downloadMedia(Context context, + HomeServerConnectionConfig hsConfig, + String url, + String mimeType, + EncryptedFileInfo encryptionInfo) { + return downloadMedia(context, + hsConfig, + url, + mimeType, + encryptionInfo, + null); + } + + /** + * Download a media. + * + * @param context the application context + * @param hsConfig the home server config. + * @param url the media url + * @param mimeType the media mimetype + * @param encryptionInfo the encryption information + * @param listener the encryption information + * @return the download identifier if there is a pending download else null + */ + public String downloadMedia(Context context, + HomeServerConnectionConfig hsConfig, + String url, + String mimeType, + EncryptedFileInfo encryptionInfo, + IMXMediaDownloadListener listener) { + // sanity checks + if ((null == mimeType) || (null == context)) { + return null; + } + + // Check the provided URL + String downloadId = mContentManager.downloadTaskIdForMatrixMediaContent(url); + + // Return if the media url is not valid, or if the media is already downloaded + if (null == downloadId || isMediaCached(url, mimeType)) { + return null; + } + + // is the media downloading? + MXMediaDownloadWorkerTask task = MXMediaDownloadWorkerTask.getMediaDownloadWorkerTask(downloadId); + if (null != task) { + task.addDownloadListener(listener); + return downloadId; + } + + // Download it in background + String downloadableUrl = mContentManager.getDownloadableUrl(url, null != encryptionInfo); + task = new MXMediaDownloadWorkerTask(context, + hsConfig, + mNetworkConnectivityReceiver, + getFolderFile(mimeType), + downloadableUrl, + downloadId, + 0, + mimeType, + encryptionInfo, + mMediaScanRestClient, + mContentManager.isAvScannerEnabled()); + task.addDownloadListener(listener); + + // avoid crash if there are too many running task + try { + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } catch (RejectedExecutionException e) { + // too many tasks have been launched + synchronized (mSuspendedTasks) { + task.cancel(true); + // create a new task from the existing one + task = new MXMediaDownloadWorkerTask(task); + mSuspendedTasks.add(task); + // privacy + //Log.e(LOG_TAG, "Suspend the task " + task.getUrl()); + Log.e(LOG_TAG, "Suspend the task ", e); + } + + } catch (Exception e) { + Log.e(LOG_TAG, "downloadMedia failed " + e.getMessage(), e); + synchronized (mSuspendedTasks) { + task.cancel(true); + } + } + + return downloadId; + } + + /** + * Start any suspended task + */ + private void launchSuspendedTask() { + synchronized (mSuspendedTasks) { + // some task have been suspended because there were too many running ones ? + if (!mSuspendedTasks.isEmpty()) { + MXMediaDownloadWorkerTask task = mSuspendedTasks.get(0); + + // privacy + //Log.d(LOG_TAG, "Restart the task " + task.getUrl()); + Log.d(LOG_TAG, "Restart a task "); + + // avoid crash if there are too many running task + try { + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + mSuspendedTasks.remove(task); + } catch (RejectedExecutionException e) { + task.cancel(true); + + mSuspendedTasks.remove(task); + // create a new task from the existing one + task = new MXMediaDownloadWorkerTask(task); + mSuspendedTasks.add(task); + + // privacy + //Log.d(LOG_TAG, "Suspend again the task " + task.getUrl() + " - " + task.getStatus()); + Log.d(LOG_TAG, "Suspend again the task " + task.getStatus()); + } catch (Exception e) { + Log.e(LOG_TAG, "Try to Restart a task fails " + e.getMessage(), e); + } + } + } + } + + /** + * The default bitmap to use when the media cannot be retrieved. + */ + private static Bitmap mDefaultBitmap = null; + + /** + * Load a bitmap from an url. + * The imageView image is updated when the bitmap is loaded or downloaded. + * The width/height parameters are optional. If they are positive, download a thumbnail. + *

+ * The rotation angle is checked first. + * If rotationAngle is set to Integer.MAX_VALUE, check the orientation is defined to a valid value. + * If the orientation is defined, request the properly oriented image to the server + * + * @param context the context + * @param hsConfig the home server config + * @param imageView the imageView to fill when the image is downloaded + * @param url the image url + * @param width the expected image width + * @param height the expected image height + * @param rotationAngle the rotation angle (degrees) + * @param orientation the orientation (ExifInterface.ORIENTATION_XXX value) + * @param mimeType the mimeType. + * @param folderFile the folder where the media should be stored + * @param encryptionInfo the encryption file information. + * @return a download identifier if the image is not cached + */ + public String loadBitmap(Context context, + HomeServerConnectionConfig hsConfig, + final ImageView imageView, + String url, + int width, + int height, + int rotationAngle, + int orientation, + String mimeType, + File folderFile, + EncryptedFileInfo encryptionInfo) { + return loadBitmap(context, + hsConfig, + imageView, + url, + width, + height, + rotationAngle, + orientation, + mimeType, + folderFile, + null, + encryptionInfo); + } + + /** + * Load a bitmap from an url. + * The imageView image is updated when the bitmap is loaded or downloaded. + * The width/height parameters are optional. If they are positive, download a thumbnail. + *

+ * The rotation angle is checked first. + * If rotationAngle is set to Integer.MAX_VALUE, check the orientation is defined to a valid value. + * If the orientation is defined, request the properly oriented image to the server + * + * @param context the context + * @param hsConfig the home server config + * @param imageView the imageView to fill when the image is downloaded + * @param url the image url + * @param width the expected image width + * @param height the expected image height + * @param rotationAngle the rotation angle (degrees) + * @param orientation the orientation (ExifInterface.ORIENTATION_XXX value) + * @param mimeType the mimeType. + * @param folderFile the folder where the media should be stored + * @param aDefaultBitmap the default bitmap to use when the url media cannot be retrieved. + * @param encryptionInfo the file encryption info + * @return a download identifier if the image is not cached + */ + public String loadBitmap(Context context, + HomeServerConnectionConfig hsConfig, + final ImageView imageView, + String url, + int width, + int height, + int rotationAngle, + int orientation, + String mimeType, + File folderFile, + Bitmap aDefaultBitmap, + EncryptedFileInfo encryptionInfo) { + // Check invalid bitmap size + if ((0 == width) || (0 == height)) { + return null; + } + + if (null == mDefaultBitmap) { + mDefaultBitmap = BitmapFactory.decodeResource(context.getResources(), android.R.drawable.ic_menu_gallery); + } + + final Bitmap defaultBitmap = (null == aDefaultBitmap) ? mDefaultBitmap : aDefaultBitmap; + + // Check whether the url is valid + String downloadId = mContentManager.downloadTaskIdForMatrixMediaContent(url); + if (null == downloadId) { + // Nothing to do + if (null != imageView) { + imageView.setImageBitmap(defaultBitmap); + } + return null; + } + + // Resolve the provided URL. + // Note: it is not possible to resize an encrypted image. + String downloadableUrl; + if (null == encryptionInfo && width > 0 && height > 0) { + downloadableUrl = mContentManager.getDownloadableThumbnailUrl(url, width, height, ContentManager.METHOD_SCALE); + downloadId += "_w_" + width + "_h_" + height; + } else { + downloadableUrl = mContentManager.getDownloadableUrl(url, null != encryptionInfo); + } + + // the thumbnail params are ignored when encrypted + if ((null == encryptionInfo) + && (rotationAngle == Integer.MAX_VALUE) + && (orientation != ExifInterface.ORIENTATION_UNDEFINED) + && (orientation != ExifInterface.ORIENTATION_NORMAL)) { + if (downloadableUrl.contains("?")) { + downloadableUrl += "&apply_orientation=true"; + } else { + downloadableUrl += "?apply_orientation=true"; + } + downloadId += "_apply_orientation"; + } + + final String fDownloadId = downloadId; + + if (null != imageView) { + imageView.setTag(fDownloadId); + } + + // if the mime type is not provided, assume it is a jpeg file + if (null == mimeType) { + mimeType = "image/jpeg"; + } + + boolean isCached = MXMediaDownloadWorkerTask.bitmapForURL(context.getApplicationContext(), + folderFile, downloadableUrl, downloadId, rotationAngle, mimeType, encryptionInfo, new SimpleApiCallback() { + @Override + public void onSuccess(Bitmap bitmap) { + if (null != imageView) { + if (TextUtils.equals(fDownloadId, (String) imageView.getTag())) { + // display it + imageView.setImageBitmap((null != bitmap) ? bitmap : defaultBitmap); + } + } + } + }); + + if (isCached) { + downloadId = null; + } else { + MXMediaDownloadWorkerTask currentTask = MXMediaDownloadWorkerTask.getMediaDownloadWorkerTask(downloadId); + + if (null != currentTask) { + if (null != imageView) { + currentTask.addImageView(imageView); + } + } else { + // Download it in background + MXMediaDownloadWorkerTask task = new MXMediaDownloadWorkerTask(context, + hsConfig, + mNetworkConnectivityReceiver, + folderFile, + downloadableUrl, + downloadId, + rotationAngle, + mimeType, + encryptionInfo, + mMediaScanRestClient, + mContentManager.isAvScannerEnabled()); + + if (null != imageView) { + task.addImageView(imageView); + } + + task.setDefaultBitmap(defaultBitmap); + + // check at the end of the download, if a suspended task can be launched again. + task.addDownloadListener(new MXMediaDownloadListener() { + @Override + public void onDownloadComplete(String downloadId) { + launchSuspendedTask(); + } + }); + + + // avoid crash if there are too many running task + try { + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } catch (RejectedExecutionException e) { + // too many tasks have been launched + synchronized (mSuspendedTasks) { + task.cancel(true); + // create a new task from the existing one + task = new MXMediaDownloadWorkerTask(task); + mSuspendedTasks.add(task); + // privacy + //Log.e(LOG_TAG, "Suspend the task " + task.getUrl()); + Log.e(LOG_TAG, "Suspend a task", e); + } + + } catch (Exception e) { + Log.e(LOG_TAG, "loadBitmap failed " + e.getMessage(), e); + } + } + } + + return downloadId; + } + + /** + * Returns the download progress (percentage). + * + * @param downloadId the downloadId provided by loadBitmap; + * @return the download progress + */ + public int getProgressValueForDownloadId(String downloadId) { + MXMediaDownloadWorkerTask currentTask = MXMediaDownloadWorkerTask.getMediaDownloadWorkerTask(downloadId); + + if (null != currentTask) { + return currentTask.getProgress(); + } + return -1; + } + + /** + * Returns the download stats for a dedicated download id. + * + * @param downloadId the downloadId provided by loadBitmap; + * @return the download stats + */ + @Nullable + public IMXMediaDownloadListener.DownloadStats getStatsForDownloadId(String downloadId) { + MXMediaDownloadWorkerTask task = MXMediaDownloadWorkerTask.getMediaDownloadWorkerTask(downloadId); + + if (null != task) { + return task.getDownloadStats(); + } + + return null; + } + + /** + * Add a download listener for an downloadId. + * + * @param downloadId The uploadId. + * @param listener the download listener. + */ + public void addDownloadListener(String downloadId, IMXMediaDownloadListener listener) { + MXMediaDownloadWorkerTask task = MXMediaDownloadWorkerTask.getMediaDownloadWorkerTask(downloadId); + + if (null != task) { + task.addDownloadListener(listener); + } + // Else consider calling listener.onDownloadComplete(downloadId) ? + } + + /** + * Cancel a download. + * + * @param downloadId the download id. + */ + public void cancelDownload(String downloadId) { + MXMediaDownloadWorkerTask task = MXMediaDownloadWorkerTask.getMediaDownloadWorkerTask(downloadId); + + if (null != task) { + task.cancelDownload(); + } + } + + /** + * Upload a file + * + * @param contentStream the stream to upload + * @param filename the dst filename + * @param mimeType the mimetype + * @param uploadId the upload id + * @param listener the upload progress listener + */ + public void uploadContent(InputStream contentStream, + String filename, + String mimeType, + String uploadId, + IMXMediaUploadListener listener) { + try { + new MXMediaUploadWorkerTask(mContentManager, + contentStream, + mimeType, + uploadId, + filename, + listener) + .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } catch (Exception e) { + // cannot start the task + if (null != listener) { + listener.onUploadError(uploadId, -1, null); + } + } + } + + /** + * Returns the upload progress (percentage) for a dedicated uploadId + * + * @param uploadId The uploadId. + * @return the upload percentage. -1 means there is no pending upload. + */ + public int getProgressValueForUploadId(String uploadId) { + MXMediaUploadWorkerTask task = MXMediaUploadWorkerTask.getMediaUploadWorkerTask(uploadId); + + if (null != task) { + return task.getProgress(); + } + + return -1; + } + + /** + * Returns the upload stats for a dedicated uploadId + * + * @param uploadId The uploadId. + * @return the upload stats + */ + public IMXMediaUploadListener.UploadStats getStatsForUploadId(String uploadId) { + MXMediaUploadWorkerTask task = MXMediaUploadWorkerTask.getMediaUploadWorkerTask(uploadId); + + if (null != task) { + return task.getStats(); + } + + return null; + } + + + /** + * Add an upload listener for an uploadId. + * + * @param uploadId The uploadId. + * @param listener the upload listener + */ + public void addUploadListener(String uploadId, IMXMediaUploadListener listener) { + MXMediaUploadWorkerTask task = MXMediaUploadWorkerTask.getMediaUploadWorkerTask(uploadId); + + if (null != task) { + task.addListener(listener); + } + } + + /** + * Cancel an upload. + * + * @param uploadId the upload Id + */ + public void cancelUpload(String uploadId) { + MXMediaUploadWorkerTask task = MXMediaUploadWorkerTask.getMediaUploadWorkerTask(uploadId); + + if (null != task) { + task.cancelUpload(); + } + } + + /** + * Set MediaScan rest client + * + * @param mediaScanRestClient + */ + public void setMediaScanRestClient(MediaScanRestClient mediaScanRestClient) { + mMediaScanRestClient = mediaScanRestClient; + } +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/groups/GroupsManager.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/groups/GroupsManager.java new file mode 100644 index 0000000000..4030a3a0d4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/groups/GroupsManager.java @@ -0,0 +1,802 @@ +/* + * 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.groups; + +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; +import android.text.TextUtils; + +import im.vector.matrix.android.internal.legacy.MXDataHandler; +import im.vector.matrix.android.internal.legacy.MXPatterns; +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.client.GroupsRestClient; +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.rest.model.group.CreateGroupParams; +import im.vector.matrix.android.internal.legacy.rest.model.group.Group; +import im.vector.matrix.android.internal.legacy.rest.model.group.GroupProfile; +import im.vector.matrix.android.internal.legacy.rest.model.group.GroupRooms; +import im.vector.matrix.android.internal.legacy.rest.model.group.GroupSummary; +import im.vector.matrix.android.internal.legacy.rest.model.group.GroupSyncProfile; +import im.vector.matrix.android.internal.legacy.rest.model.group.GroupUsers; +import im.vector.matrix.android.internal.legacy.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * This class manages the groups + */ +public class GroupsManager { + private static final String LOG_TAG = GroupsManager.class.getSimpleName(); + + private MXDataHandler mDataHandler; + private GroupsRestClient mGroupsRestClient; + private IMXStore mStore; + + // callbacks + private Set> mRefreshProfilesCallback = new HashSet<>(); + + // + private final Map> mPendingJoinGroups = new HashMap<>(); + private final Map> mPendingLeaveGroups = new HashMap<>(); + + // publicise management + private Map>>> mPendingPubliciseRequests = new HashMap<>(); + private Map> mPubliciseByUserId = new HashMap<>(); + + private Handler mUIHandler; + + /** + * Constructor + * + * @param dataHandler the data handler + * @param restClient the group rest client + */ + public GroupsManager(MXDataHandler dataHandler, GroupsRestClient restClient) { + mDataHandler = dataHandler; + mStore = mDataHandler.getStore(); + mGroupsRestClient = restClient; + + mUIHandler = new Handler(Looper.getMainLooper()); + } + + /** + * @return the groups rest client + */ + public GroupsRestClient getGroupsRestClient() { + return mGroupsRestClient; + } + + + /** + * Call when the session is paused + */ + public void onSessionPaused() { + mPubliciseByUserId.clear(); + } + + /** + * Call when the session is resumed + */ + public void onSessionResumed() { + refreshGroupProfiles((ApiCallback) null); + getUserPublicisedGroups(mDataHandler.getUserId(), true, new SimpleApiCallback>() { + @Override + public void onSuccess(Set info) { + // Ignore + } + }); + + mGroupProfileByGroupId.clear(); + mGroupProfileCallback.clear(); + } + + /** + * Retrieve the group from a group id + * + * @param groupId the group id + * @return the group if it exists + */ + public Group getGroup(String groupId) { + return mStore.getGroup(groupId); + } + + /** + * @return the existing groups + */ + public Collection getGroups() { + return mStore.getGroups(); + } + + /** + * @return the groups list in which the user is invited + */ + public Collection getInvitedGroups() { + List invitedGroups = new ArrayList<>(); + Collection groups = getGroups(); + + for (Group group : groups) { + if (group.isInvited()) { + invitedGroups.add(group); + } + } + + return invitedGroups; + } + + /** + * @return the joined groups + */ + public Collection getJoinedGroups() { + List joinedGroups = new ArrayList<>(getGroups()); + joinedGroups.removeAll(getInvitedGroups()); + + return joinedGroups; + } + + /** + * Manage the group joining. + * + * @param groupId the group id + * @param notify true to notify + */ + public void onJoinGroup(final String groupId, final boolean notify) { + Group group = getGroup(groupId); + + if (null == group) { + group = new Group(groupId); + } + + if (TextUtils.equals(RoomMember.MEMBERSHIP_JOIN, group.getMembership())) { + Log.d(LOG_TAG, "## onJoinGroup() : the group " + groupId + " was already joined"); + return; + } + + group.setMembership(RoomMember.MEMBERSHIP_JOIN); + mStore.storeGroup(group); + + // try retrieve the summary + mGroupsRestClient.getGroupSummary(groupId, new ApiCallback() { + /** + * Common method + */ + private void onDone() { + if (notify) { + mDataHandler.onJoinGroup(groupId); + } + } + + @Override + public void onSuccess(GroupSummary groupSummary) { + Group group = getGroup(groupId); + + if (null != group) { + group.setGroupSummary(groupSummary); + mStore.flushGroup(group); + onDone(); + + if (null != mPendingJoinGroups.get(groupId)) { + mPendingJoinGroups.get(groupId).onSuccess(null); + mPendingJoinGroups.remove(groupId); + } + } + } + + @Override + public void onNetworkError(Exception e) { + Log.e(LOG_TAG, "## onJoinGroup() : failed " + e.getMessage(), e); + onDone(); + + if (null != mPendingJoinGroups.get(groupId)) { + mPendingJoinGroups.get(groupId).onNetworkError(e); + mPendingJoinGroups.remove(groupId); + } + } + + @Override + public void onMatrixError(MatrixError e) { + Log.e(LOG_TAG, "## onMatrixError() : failed " + e.getMessage()); + onDone(); + + if (null != mPendingJoinGroups.get(groupId)) { + mPendingJoinGroups.get(groupId).onMatrixError(e); + mPendingJoinGroups.remove(groupId); + } + } + + @Override + public void onUnexpectedError(Exception e) { + Log.e(LOG_TAG, "## onUnexpectedError() : failed " + e.getMessage(), e); + onDone(); + + if (null != mPendingJoinGroups.get(groupId)) { + mPendingJoinGroups.get(groupId).onUnexpectedError(e); + mPendingJoinGroups.remove(groupId); + } + } + }); + } + + /** + * Create a group from an invitation. + * + * @param groupId the group id + * @param profile the profile + * @param inviter the inviter + * @param notify true to notify + */ + public void onNewGroupInvitation(final String groupId, final GroupSyncProfile profile, final String inviter, final boolean notify) { + Group group = getGroup(groupId); + + // it should always be null + if (null == group) { + group = new Group(groupId); + } + + GroupSummary summary = new GroupSummary(); + summary.profile = new GroupProfile(); + if (null != profile) { + summary.profile.name = profile.name; + summary.profile.avatarUrl = profile.avatarUrl; + } + + group.setGroupSummary(summary); + group.setInviter(inviter); + group.setMembership(RoomMember.MEMBERSHIP_INVITE); + + mStore.storeGroup(group); + + if (notify) { + mUIHandler.post(new Runnable() { + @Override + public void run() { + mDataHandler.onNewGroupInvitation(groupId); + } + }); + } + } + + /** + * Remove a group. + * + * @param groupId the group id. + * @param notify true to notify + */ + public void onLeaveGroup(final String groupId, final boolean notify) { + if (null != mStore.getGroup(groupId)) { + mStore.deleteGroup(groupId); + + mUIHandler.post(new Runnable() { + @Override + public void run() { + if (notify) { + mDataHandler.onLeaveGroup(groupId); + } + + if (mPendingLeaveGroups.containsKey(groupId)) { + mPendingLeaveGroups.get(groupId).onSuccess(null); + mPendingLeaveGroups.remove(groupId); + } + } + }); + } + } + + /** + * Refresh the group profiles + * + * @param callback the asynchronous callback + */ + public void refreshGroupProfiles(ApiCallback callback) { + if (!mRefreshProfilesCallback.isEmpty()) { + Log.d(LOG_TAG, "## refreshGroupProfiles() : there already is a pending request"); + mRefreshProfilesCallback.add(callback); + return; + } + + mRefreshProfilesCallback.add(callback); + refreshGroupProfiles(getGroups().iterator()); + } + + /** + * Internal method to refresh the group profiles. + * + * @param iterator the iterator. + */ + private void refreshGroupProfiles(final Iterator iterator) { + if (!iterator.hasNext()) { + for (ApiCallback callback : mRefreshProfilesCallback) { + try { + if (null != callback) { + callback.onSuccess(null); + } + } catch (Exception e) { + Log.e(LOG_TAG, "## refreshGroupProfiles() failed " + e.getMessage(), e); + } + } + mRefreshProfilesCallback.clear(); + return; + } + + final String groupId = iterator.next().getGroupId(); + + mGroupsRestClient.getGroupProfile(groupId, new ApiCallback() { + private void onDone() { + refreshGroupProfiles(iterator); + } + + @Override + public void onSuccess(GroupProfile profile) { + Group group = getGroup(groupId); + + if (null != group) { + group.setGroupProfile(profile); + mStore.flushGroup(group); + } + + mDataHandler.onGroupProfileUpdate(groupId); + mGroupProfileByGroupId.put(groupId, profile); + onDone(); + } + + @Override + public void onNetworkError(Exception e) { + Log.e(LOG_TAG, "## refreshGroupProfiles() : failed " + e.getMessage(), e); + onDone(); + } + + @Override + public void onMatrixError(MatrixError e) { + Log.e(LOG_TAG, "## refreshGroupProfiles() : failed " + e.getMessage()); + onDone(); + } + + @Override + public void onUnexpectedError(Exception e) { + Log.e(LOG_TAG, "## refreshGroupProfiles() : failed " + e.getMessage(), e); + onDone(); + } + }); + } + + /** + * Join a group. + * + * @param groupId the group id + * @param callback the asynchronous callback + */ + public void joinGroup(final String groupId, final ApiCallback callback) { + getGroupsRestClient().joinGroup(groupId, new SimpleApiCallback(callback) { + @Override + public void onSuccess(Void info) { + Group group = getGroup(groupId); + // not yet synced -> wait it is synced + if ((null == group) || TextUtils.equals(group.getMembership(), RoomMember.MEMBERSHIP_INVITE)) { + mPendingJoinGroups.put(groupId, callback); + onJoinGroup(groupId, true); + } else { + callback.onSuccess(null); + } + } + }); + } + + /** + * Leave a group. + * + * @param groupId the group id + * @param callback the asynchronous callback + */ + public void leaveGroup(final String groupId, final ApiCallback callback) { + getGroupsRestClient().leaveGroup(groupId, new SimpleApiCallback(callback) { + @Override + public void onSuccess(Void info) { + Group group = getGroup(groupId); + // not yet synced -> wait it is synced + if (null != group) { + mPendingLeaveGroups.put(groupId, callback); + onLeaveGroup(groupId, true); + } else { + callback.onSuccess(null); + } + } + }); + } + + /** + * Create a group. + * + * @param localPart the local part + * @param groupName the group human name + * @param callback the asynchronous callback + */ + public void createGroup(String localPart, String groupName, final ApiCallback callback) { + final CreateGroupParams params = new CreateGroupParams(); + params.localpart = localPart; + params.profile = new GroupProfile(); + params.profile.name = groupName; + + getGroupsRestClient().createGroup(params, new SimpleApiCallback(callback) { + @Override + public void onSuccess(String groupId) { + Group group = getGroup(groupId); + + // if the group does not exist, create it + if (null == group) { + group = new Group(groupId); + group.setGroupProfile(params.profile); + group.setMembership(RoomMember.MEMBERSHIP_JOIN); + mStore.storeGroup(group); + } + + callback.onSuccess(groupId); + } + }); + } + + /** + * Refresh the group data i.e the invited users list, the users list and the rooms list. + * + * @param group the group + * @param callback the asynchronous callback + */ + public void refreshGroupData(Group group, ApiCallback callback) { + refreshGroupData(group, GROUP_REFRESH_STEP_PROFILE, callback); + } + + private static final int GROUP_REFRESH_STEP_PROFILE = 0; + private static final int GROUP_REFRESH_STEP_ROOMS_LIST = 1; + private static final int GROUP_REFRESH_STEP_USERS_LIST = 2; + private static final int GROUP_REFRESH_STEP_INVITED_USERS_LIST = 3; + + /** + * Internal method to refresh the group informations. + * + * @param group the group + * @param step the current step + * @param callback the asynchronous callback + */ + private void refreshGroupData(final Group group, final int step, final ApiCallback callback) { + if (step == GROUP_REFRESH_STEP_PROFILE) { + getGroupsRestClient().getGroupProfile(group.getGroupId(), new SimpleApiCallback(callback) { + @Override + public void onSuccess(GroupProfile groupProfile) { + group.setGroupProfile(groupProfile); + mStore.flushGroup(group); + mDataHandler.onGroupProfileUpdate(group.getGroupId()); + refreshGroupData(group, GROUP_REFRESH_STEP_ROOMS_LIST, callback); + } + }); + + return; + } + + if (step == GROUP_REFRESH_STEP_ROOMS_LIST) { + getGroupsRestClient().getGroupRooms(group.getGroupId(), new SimpleApiCallback(callback) { + @Override + public void onSuccess(GroupRooms groupRooms) { + group.setGroupRooms(groupRooms); + mStore.flushGroup(group); + mDataHandler.onGroupRoomsListUpdate(group.getGroupId()); + refreshGroupData(group, GROUP_REFRESH_STEP_USERS_LIST, callback); + } + }); + return; + } + + if (step == GROUP_REFRESH_STEP_USERS_LIST) { + getGroupsRestClient().getGroupUsers(group.getGroupId(), new SimpleApiCallback(callback) { + @Override + public void onSuccess(GroupUsers groupUsers) { + group.setGroupUsers(groupUsers); + mStore.flushGroup(group); + mDataHandler.onGroupUsersListUpdate(group.getGroupId()); + refreshGroupData(group, GROUP_REFRESH_STEP_INVITED_USERS_LIST, callback); + } + }); + return; + } + + + //if (step == GROUP_REFRESH_STEP_INVITED_USERS_LIST) + + getGroupsRestClient().getGroupInvitedUsers(group.getGroupId(), new SimpleApiCallback(callback) { + @Override + public void onSuccess(GroupUsers groupUsers) { + group.setInvitedGroupUsers(groupUsers); + + if (null != mStore.getGroup(group.getGroupId())) { + mStore.flushGroup(group); + } + mDataHandler.onGroupInvitedUsersListUpdate(group.getGroupId()); + callback.onSuccess(null); + } + }); + } + + /** + * Retrieves the cached publicisedGroups for an userId. + * + * @param userId the user id + * @return a set if there is a cached one, else null + */ + public Set getUserPublicisedGroups(final String userId) { + if (mPubliciseByUserId.containsKey(userId)) { + return new HashSet<>(mPubliciseByUserId.get(userId)); + } + + return null; + } + + /** + * Request the publicised groups for an user. + * + * @param userId the user id + * @param forceRefresh true to do not use the cached data + * @param callback the asynchronous callback. + */ + public void getUserPublicisedGroups(final String userId, + final boolean forceRefresh, + @NonNull final ApiCallback> callback) { + Log.d(LOG_TAG, "## getUserPublicisedGroups() : " + userId); + + // sanity check + if (!MXPatterns.isUserId(userId)) { + mUIHandler.post(new Runnable() { + @Override + public void run() { + callback.onSuccess(new HashSet()); + } + }); + + return; + } + + // already cached + if (forceRefresh) { + mPubliciseByUserId.remove(userId); + } else { + if (mPubliciseByUserId.containsKey(userId)) { + mUIHandler.post(new Runnable() { + @Override + public void run() { + Log.d(LOG_TAG, "## getUserPublicisedGroups() : " + userId + " --> cached data " + mPubliciseByUserId.get(userId)); + // reported by a rage shake + if (mPubliciseByUserId.containsKey(userId)) { + callback.onSuccess(new HashSet<>(mPubliciseByUserId.get(userId))); + } else { + callback.onSuccess(new HashSet()); + } + } + }); + + return; + } + } + + // request in progress + if (mPendingPubliciseRequests.containsKey(userId)) { + Log.d(LOG_TAG, "## getUserPublicisedGroups() : " + userId + " request in progress"); + mPendingPubliciseRequests.get(userId).add(callback); + return; + } + + mPendingPubliciseRequests.put(userId, new HashSet>>()); + mPendingPubliciseRequests.get(userId).add(callback); + + mGroupsRestClient.getUserPublicisedGroups(userId, new ApiCallback>() { + private void onDone(Set groupIdsSet) { + + // cache only if the request succeeds + // else it will be tried later + if (null != groupIdsSet) { + mPubliciseByUserId.put(userId, new HashSet<>(groupIdsSet)); + } else { + groupIdsSet = new HashSet<>(); + } + + Log.d(LOG_TAG, "## getUserPublicisedGroups() : " + userId + " -- " + groupIdsSet); + + Set>> callbacks = mPendingPubliciseRequests.get(userId); + mPendingPubliciseRequests.remove(userId); + + if (null != callbacks) { + for (ApiCallback> callback : callbacks) { + if (null != callback) { + try { + callback.onSuccess(new HashSet<>(groupIdsSet)); + } catch (Throwable t) { + Log.d(LOG_TAG, "## getUserPublicisedGroups() : callback failed " + t.getMessage()); + } + } + } + } + } + + @Override + public void onSuccess(List groupIdsList) { + onDone((null == groupIdsList) ? new HashSet() : new HashSet<>(groupIdsList)); + } + + @Override + public void onNetworkError(Exception e) { + Log.e(LOG_TAG, "## getUserPublicisedGroups() : request failed " + e.getMessage(), e); + onDone(null); + } + + @Override + public void onMatrixError(MatrixError e) { + Log.e(LOG_TAG, "## getUserPublicisedGroups() : request failed " + e.getMessage()); + onDone(null); + } + + @Override + public void onUnexpectedError(Exception e) { + Log.e(LOG_TAG, "## getUserPublicisedGroups() : request failed " + e.getMessage(), e); + onDone(null); + } + }); + } + + /** + * Update a group publicity status. + * + * @param groupId the group id + * @param publicity the new publicity status + * @param callback the asynchronous callback. + */ + public void updateGroupPublicity(final String groupId, final boolean publicity, final ApiCallback callback) { + getGroupsRestClient().updateGroupPublicity(groupId, publicity, new SimpleApiCallback(callback) { + @Override + public void onSuccess(Void info) { + if (mPubliciseByUserId.containsKey(groupId)) { + if (publicity) { + mPubliciseByUserId.get(groupId).add(groupId); + } else { + mPubliciseByUserId.get(groupId).remove(groupId); + } + } + + if (null != callback) { + callback.onSuccess(null); + } + } + }); + } + + Map mGroupProfileByGroupId = new HashMap<>(); + Map>> mGroupProfileCallback = new HashMap<>(); + + + /** + * Retrieve the cached group profile + * + * @param groupId the group id + * @return the cached GroupProfile if it exits, else null + */ + public GroupProfile getGroupProfile(final String groupId) { + return mGroupProfileByGroupId.get(groupId); + } + + /** + * Request the profile of a group. + * + * @param groupId the group id + * @param callback the asynchronous callback + */ + public void getGroupProfile(final String groupId, final ApiCallback callback) { + // sanity check + if (null == callback) { + return; + } + + // valid group id + if (TextUtils.isEmpty(groupId) || !MXPatterns.isGroupId(groupId)) { + mUIHandler.post(new Runnable() { + @Override + public void run() { + callback.onSuccess(new GroupProfile()); + } + }); + + return; + } + + // already downloaded + if (mGroupProfileByGroupId.containsKey(groupId)) { + mUIHandler.post(new Runnable() { + @Override + public void run() { + callback.onSuccess(mGroupProfileByGroupId.get(groupId)); + } + }); + + return; + } + + // in progress + if (mGroupProfileCallback.containsKey(groupId)) { + mGroupProfileCallback.get(groupId).add(callback); + return; + } + + mGroupProfileCallback.put(groupId, new ArrayList<>(Arrays.asList(callback))); + + mGroupsRestClient.getGroupProfile(groupId, new ApiCallback() { + @Override + public void onSuccess(GroupProfile groupProfile) { + mGroupProfileByGroupId.put(groupId, groupProfile); + List> callbacks = mGroupProfileCallback.get(groupId); + mGroupProfileCallback.remove(groupId); + + if (null != callbacks) { + for (ApiCallback c : callbacks) { + c.onSuccess(groupProfile); + } + } + } + + @Override + public void onNetworkError(Exception e) { + List> callbacks = mGroupProfileCallback.get(groupId); + mGroupProfileCallback.remove(groupId); + + if (null != callbacks) { + for (ApiCallback c : callbacks) { + c.onNetworkError(e); + } + } + } + + @Override + public void onMatrixError(MatrixError e) { + List> callbacks = mGroupProfileCallback.get(groupId); + mGroupProfileCallback.remove(groupId); + + if (null != callbacks) { + for (ApiCallback c : callbacks) { + c.onMatrixError(e); + } + } + } + + @Override + public void onUnexpectedError(Exception e) { + List> callbacks = mGroupProfileCallback.get(groupId); + mGroupProfileCallback.remove(groupId); + + if (null != callbacks) { + for (ApiCallback c : callbacks) { + c.onUnexpectedError(e); + } + } + } + }); + + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/interfaces/DatedObject.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/interfaces/DatedObject.java new file mode 100644 index 0000000000..6c43ab4417 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/interfaces/DatedObject.java @@ -0,0 +1,25 @@ +/* + * 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.interfaces; + +/** + * Can be implemented by any object containing a timestamp. + * This interface can be use to sort such object + */ +public interface DatedObject { + long getDate(); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/interfaces/HtmlToolbox.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/interfaces/HtmlToolbox.java new file mode 100644 index 0000000000..0fe4c3a41d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/interfaces/HtmlToolbox.java @@ -0,0 +1,49 @@ +/* + * 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.interfaces; + +import android.support.annotation.Nullable; +import android.text.Html; + +public interface HtmlToolbox { + + /** + * Convert a html String + * Example: remove not supported html tags, etc. + * + * @param html the source HTML + * @return the converted HTML + */ + String convert(String html); + + /** + * Get a HTML Image Getter + * + * @return a HTML Image Getter or null + */ + @Nullable + Html.ImageGetter getImageGetter(); + + /** + * Get a HTML Tag Handler + * + * @param html the source HTML + * @return a HTML Tag Handler or null + */ + @Nullable + Html.TagHandler getTagHandler(String html); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/IMXEventListener.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/IMXEventListener.java new file mode 100644 index 0000000000..7a4c6f72c3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/IMXEventListener.java @@ -0,0 +1,265 @@ +/* + * 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.listeners; + +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.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 java.util.List; + +public interface IMXEventListener { + /** + * The store is ready. + */ + void onStoreReady(); + + /** + * User presence was updated. + * + * @param event The presence event. + * @param user The new user value. + */ + void onPresenceUpdate(Event event, User user); + + /** + * The self user has been updated (display name, avatar url...). + * + * @param myUser The updated myUser + */ + void onAccountInfoUpdate(MyUser myUser); + + /** + * The ignored users list has been updated. + */ + void onIgnoredUsersListUpdate(); + + /** + * The direct chat rooms list have been updated. + */ + void onDirectMessageChatRoomsListUpdate(); + + /** + * A live room event was received. + * + * @param event the event + * @param roomState the room state right before the event + */ + void onLiveEvent(Event event, RoomState roomState); + + /** + * The live events from a chunk are performed. + * + * @param fromToken the start sync token + * @param toToken the up-to sync token + */ + void onLiveEventsChunkProcessed(String fromToken, String toToken); + + /** + * A received event fulfills the bing rules + * The first matched bing rule is provided in paramater to perform + * dedicated action like playing a notification sound. + * + * @param event the event + * @param roomState the room state right before the event + * @param bingRule the bing rule + */ + void onBingEvent(Event event, RoomState roomState, BingRule bingRule); + + /** + * The state of an event has been updated. + * + * @param event the event + */ + void onEventSentStateUpdated(Event event); + + /** + * An event has been sent. + * prevEventId defines the event id set before getting the server new one. + * + * @param event the event + * @param prevEventId the previous eventId + */ + void onEventSent(Event event, String prevEventId); + + /** + * An event has been decrypted + * + * @param event the decrypted event + */ + void onEventDecrypted(Event event); + + /** + * The bing rules have been updated + */ + void onBingRulesUpdate(); + + /** + * The initial sync is complete and the store can be queried for current state. + * + * @param toToken the up-to sync token + */ + void onInitialSyncComplete(String toToken); + + /** + * The sync has encountered an error + * + * @param matrixError the error + */ + void onSyncError(MatrixError matrixError); + + /** + * The crypto sync is complete + */ + void onCryptoSyncComplete(); + + /** + * A new room has been created. + * + * @param roomId the roomID + */ + void onNewRoom(String roomId); + + /** + * The user joined a room. + * + * @param roomId the roomID + */ + void onJoinRoom(String roomId); + + /** + * The messages of an existing room has been flushed during server sync. + * This flush may be due to a limited timeline in the room sync, or the redaction of a state event. + * + * @param roomId the room Id + */ + void onRoomFlush(String roomId); + + /** + * The room data has been internally updated. + * It could be triggered when a request failed. + * + * @param roomId the roomID + */ + void onRoomInternalUpdate(String roomId); + + /** + * The notification count of a dedicated room + * has been updated. + * + * @param roomId the room ID + */ + void onNotificationCountUpdate(String roomId); + + /** + * The user left the room. + * + * @param roomId the roomID + */ + void onLeaveRoom(String roomId); + + /** + * The user has been kicked or banned. + * + * @param roomId the roomID + */ + void onRoomKick(String roomId); + + /** + * A receipt event has been received. + * It could be triggered when a request failed. + * + * @param roomId the roomID + * @param senderIds the list of the + */ + void onReceiptEvent(String roomId, List senderIds); + + /** + * A Room Tag event has been received. + * + * @param roomId the roomID + */ + void onRoomTagEvent(String roomId); + + /** + * A read marker has been updated + * + * @param roomId thr room id. + */ + void onReadMarkerEvent(String roomId); + + /** + * An event was sent to the current device. + * + * @param event the event + */ + void onToDeviceEvent(Event event); + + /** + * The user has been invited to a new group. + * + * @param groupId the group id + */ + void onNewGroupInvitation(String groupId); + + /** + * A group has been joined. + * + * @param groupId the group id + */ + void onJoinGroup(String groupId); + + /** + * A group has been left. + * + * @param groupId the group id + */ + void onLeaveGroup(String groupId); + + /** + * The group file has been updated. + * + * @param groupId the group id + */ + void onGroupProfileUpdate(String groupId); + + /** + * The group rooms list has been updated. + * + * @param groupId the group id + */ + void onGroupRoomsListUpdate(String groupId); + + /** + * The group users id list has been updated. + * + * @param groupId the group id + */ + void onGroupUsersListUpdate(String groupId); + + /** + * The group invited users id list has been updated. + * + * @param groupId the group id + */ + void onGroupInvitedUsersListUpdate(String groupId); +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/IMXMediaDownloadListener.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/IMXMediaDownloadListener.java new file mode 100644 index 0000000000..342a9d4867 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/IMXMediaDownloadListener.java @@ -0,0 +1,115 @@ +/* + * 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.listeners; + +import com.google.gson.JsonElement; + +/** + * Interface to monitor a media download. + */ +public interface IMXMediaDownloadListener { + /** + * provide some download stats + */ + class DownloadStats { + /** + * The download id + */ + public String mDownloadId; + + /** + * the download progress in percentage + */ + public int mProgress; + + /** + * The downloaded size in bytes + */ + public int mDownloadedSize; + + /** + * The file size in bytes. + */ + public int mFileSize; + + /** + * time in seconds since the download started + */ + public int mElapsedTime; + + /** + * estimated remained time in seconds to download the media + */ + public int mEstimatedRemainingTime; + + /** + * download bit rate in KB/s + */ + public int mBitRate; + + @Override + public java.lang.String toString() { + String res = ""; + + res += "mProgress : " + mProgress + "%\n"; + res += "mDownloadedSize : " + mDownloadedSize + " bytes\n"; + res += "mFileSize : " + mFileSize + "bytes\n"; + res += "mElapsedTime : " + mProgress + " seconds\n"; + res += "mEstimatedRemainingTime : " + mEstimatedRemainingTime + " seconds\n"; + res += "mBitRate : " + mBitRate + " KB/s\n"; + + return res; + } + } + + /** + * The download starts. + * + * @param downloadId the download Identifier + */ + void onDownloadStart(String downloadId); + + /** + * The download stats have been updated. + * + * @param downloadId the download Identifier + * @param stats the download stats + */ + void onDownloadProgress(String downloadId, DownloadStats stats); + + /** + * The download is completed. + * + * @param downloadId the download Identifier + */ + void onDownloadComplete(String downloadId); + + /** + * The download failed. + * + * @param downloadId the download Identifier + * @param jsonElement the error + */ + void onDownloadError(String downloadId, JsonElement jsonElement); + + /** + * The download has been cancelled. + * + * @param downloadId the download Identifier + */ + void onDownloadCancel(String downloadId); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/IMXMediaUploadListener.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/IMXMediaUploadListener.java new file mode 100644 index 0000000000..a89d4b6b24 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/IMXMediaUploadListener.java @@ -0,0 +1,116 @@ +/* + * 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.listeners; + +/** + * Interface to monitor a media upload. + */ +public interface IMXMediaUploadListener { + + /** + * Provide some upload stats + */ + class UploadStats { + /** + * The upload id + */ + public String mUploadId; + + /** + * the upload progress in percentage + */ + public int mProgress; + + /** + * The uploaded size in bytes + */ + public int mUploadedSize; + + /** + * The file size in bytes. + */ + public int mFileSize; + + /** + * time in seconds since the upload started + */ + public int mElapsedTime; + + /** + * estimated remained time in seconds to upload the media + */ + public int mEstimatedRemainingTime; + + /** + * upload bit rate in KB/s + */ + public int mBitRate; + + @Override + public java.lang.String toString() { + String res = ""; + + res += "mProgress : " + mProgress + "%\n"; + res += "mUploadedSize : " + mUploadedSize + " bytes\n"; + res += "mFileSize : " + mFileSize + " bytes\n"; + res += "mElapsedTime : " + mProgress + " seconds\n"; + res += "mEstimatedRemainingTime : " + mEstimatedRemainingTime + " seconds\n"; + res += "mBitRate : " + mBitRate + " KB/s\n"; + + return res; + } + } + + /** + * The upload starts. + * + * @param uploadId the upload Identifier + */ + void onUploadStart(String uploadId); + + /** + * The media upload is in progress. + * + * @param uploadId the upload Identifier + * @param uploadStats the upload stats + */ + void onUploadProgress(String uploadId, UploadStats uploadStats); + + /** + * The upload has been cancelled. + * + * @param uploadId the upload Identifier + */ + void onUploadCancel(String uploadId); + + /** + * The upload fails. + * + * @param uploadId the upload identifier + * @param serverResponseCode the server response code + * @param serverErrorMessage the server error message. + */ + void onUploadError(String uploadId, int serverResponseCode, String serverErrorMessage); + + /** + * The upload failed. + * + * @param uploadId the upload identifier + * @param contentUri the media URI on server. + */ + void onUploadComplete(String uploadId, String contentUri); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/IMXNetworkEventListener.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/IMXNetworkEventListener.java new file mode 100644 index 0000000000..64091b06a6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/IMXNetworkEventListener.java @@ -0,0 +1,26 @@ +/* + * 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.listeners; + + +public interface IMXNetworkEventListener { + /** + * The network connection has been updated + * + * @param isConnected true if the device uses a data connection. + */ + void onNetworkConnectionUpdate(boolean isConnected); +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/MXEventListener.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/MXEventListener.java new file mode 100644 index 0000000000..39bff6e98d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/MXEventListener.java @@ -0,0 +1,166 @@ +/* + * Copyright 2014 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.listeners; + +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.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 java.util.List; + +/** + * A no-op class implementing {@link IMXEventListener} so listeners can just implement the methods + * that they require. + */ +public class MXEventListener implements IMXEventListener { + + @Override + public void onStoreReady() { + } + + @Override + public void onPresenceUpdate(Event event, User user) { + } + + @Override + public void onAccountInfoUpdate(MyUser myUser) { + } + + @Override + public void onLiveEvent(Event event, RoomState roomState) { + + } + + @Override + public void onLiveEventsChunkProcessed(String fromToken, String toToken) { + } + + @Override + public void onBingEvent(Event event, RoomState roomState, BingRule bingRule) { + } + + @Override + public void onEventSent(final Event event, final String prevEventId) { + } + + @Override + public void onEventSentStateUpdated(Event event) { + } + + @Override + public void onEventDecrypted(Event event) { + } + + @Override + public void onBingRulesUpdate() { + } + + @Override + public void onInitialSyncComplete(String toToken) { + } + + @Override + public void onSyncError(MatrixError matrixError) { + } + + @Override + public void onCryptoSyncComplete() { + } + + @Override + public void onNewRoom(String roomId) { + } + + @Override + public void onJoinRoom(String roomId) { + } + + @Override + public void onRoomInternalUpdate(String roomId) { + } + + @Override + public void onNotificationCountUpdate(String roomId) { + } + + @Override + public void onLeaveRoom(String roomId) { + } + + @Override + public void onRoomKick(String roomId) { + } + + @Override + public void onReceiptEvent(String roomId, List senderIds) { + } + + @Override + public void onRoomTagEvent(String roomId) { + } + + @Override + public void onReadMarkerEvent(String roomId) { + } + + @Override + public void onRoomFlush(String roomId) { + } + + @Override + public void onIgnoredUsersListUpdate() { + } + + @Override + public void onToDeviceEvent(Event event) { + } + + @Override + public void onDirectMessageChatRoomsListUpdate() { + } + + @Override + public void onNewGroupInvitation(String groupId) { + } + + @Override + public void onJoinGroup(String groupId) { + } + + @Override + public void onLeaveGroup(String groupId) { + } + + @Override + public void onGroupProfileUpdate(String groupId) { + } + + @Override + public void onGroupRoomsListUpdate(String groupId) { + } + + @Override + public void onGroupUsersListUpdate(String groupId) { + } + + @Override + public void onGroupInvitedUsersListUpdate(String groupId) { + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/MXMediaDownloadListener.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/MXMediaDownloadListener.java new file mode 100644 index 0000000000..05a66dfbe5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/MXMediaDownloadListener.java @@ -0,0 +1,45 @@ +/* + * 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.listeners; + +import com.google.gson.JsonElement; + +/** + * A no-op class implementing {@link IMXMediaDownloadListener} so listeners can just implement the methods + * that they require. + */ +public class MXMediaDownloadListener implements IMXMediaDownloadListener { + + @Override + public void onDownloadStart(String downloadId) { + } + + @Override + public void onDownloadProgress(String downloadId, DownloadStats stats) { + } + + @Override + public void onDownloadComplete(String downloadId) { + } + + @Override + public void onDownloadError(String downloadId, JsonElement jsonElement) { + } + + @Override + public void onDownloadCancel(String downloadId) { + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/MXMediaUploadListener.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/MXMediaUploadListener.java new file mode 100644 index 0000000000..5c771b1839 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/MXMediaUploadListener.java @@ -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.listeners; + +/** + * A no-op class implementing {@link IMXMediaUploadListener} so listeners can just implement the methods + * that they require. + */ +public class MXMediaUploadListener implements IMXMediaUploadListener { + @Override + public void onUploadStart(String uploadId) { + } + + @Override + public void onUploadProgress(String uploadId, UploadStats uploadStats) { + } + + @Override + public void onUploadCancel(String uploadId) { + } + + @Override + public void onUploadError(String uploadId, int serverResponseCode, String serverErrorMessage) { + } + + @Override + public void onUploadComplete(String uploadId, String contentUri) { + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/MXRoomEventListener.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/MXRoomEventListener.java new file mode 100644 index 0000000000..ffcf8b9149 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/listeners/MXRoomEventListener.java @@ -0,0 +1,237 @@ +/* + * 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.listeners; + +import android.support.annotation.NonNull; +import android.text.TextUtils; + +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.rest.model.Event; +import im.vector.matrix.android.internal.legacy.rest.model.User; +import im.vector.matrix.android.internal.legacy.util.Log; + +import java.util.List; + +/** + * A listener which filter event for a specific room + */ +public class MXRoomEventListener extends MXEventListener { + + private static final String LOG_TAG = MXRoomEventListener.class.getSimpleName(); + + private final String mRoomId; + private final IMXEventListener mEventListener; + private final Room mRoom; + + public MXRoomEventListener(@NonNull Room room, + @NonNull IMXEventListener eventListener) { + mRoom = room; + mRoomId = room.getRoomId(); + mEventListener = eventListener; + } + + @Override + public void onPresenceUpdate(Event event, User user) { + // Only pass event through if the user is a member of the room + // FIXME LazyLoading. We cannot rely on getMember nullity anymore + if (mRoom.getMember(user.user_id) != null) { + try { + mEventListener.onPresenceUpdate(event, user); + } catch (Exception e) { + Log.e(LOG_TAG, "onPresenceUpdate exception " + e.getMessage(), e); + } + } + } + + @Override + public void onLiveEvent(Event event, RoomState roomState) { + // Filter out events for other rooms and events while we are joining (before the room is ready) + if (TextUtils.equals(mRoomId, event.roomId) && mRoom.isReady()) { + try { + mEventListener.onLiveEvent(event, roomState); + } catch (Exception e) { + Log.e(LOG_TAG, "onLiveEvent exception " + e.getMessage(), e); + } + } + } + + @Override + public void onLiveEventsChunkProcessed(String fromToken, String toToken) { + try { + mEventListener.onLiveEventsChunkProcessed(fromToken, toToken); + } catch (Exception e) { + Log.e(LOG_TAG, "onLiveEventsChunkProcessed exception " + e.getMessage(), e); + } + } + + @Override + public void onEventSentStateUpdated(Event event) { + // Filter out events for other rooms + if (TextUtils.equals(mRoomId, event.roomId)) { + try { + mEventListener.onEventSentStateUpdated(event); + } catch (Exception e) { + Log.e(LOG_TAG, "onEventSentStateUpdated exception " + e.getMessage(), e); + } + } + } + + @Override + public void onEventDecrypted(Event event) { + // Filter out events for other rooms + if (TextUtils.equals(mRoomId, event.roomId)) { + try { + mEventListener.onEventDecrypted(event); + } catch (Exception e) { + Log.e(LOG_TAG, "onDecryptedEvent exception " + e.getMessage(), e); + } + } + } + + @Override + public void onEventSent(final Event event, final String prevEventId) { + // Filter out events for other rooms + if (TextUtils.equals(mRoomId, event.roomId)) { + try { + mEventListener.onEventSent(event, prevEventId); + } catch (Exception e) { + Log.e(LOG_TAG, "onEventSent exception " + e.getMessage(), e); + } + } + } + + @Override + public void onRoomInternalUpdate(String roomId) { + // Filter out events for other rooms + if (TextUtils.equals(mRoomId, roomId)) { + try { + mEventListener.onRoomInternalUpdate(roomId); + } catch (Exception e) { + Log.e(LOG_TAG, "onRoomInternalUpdate exception " + e.getMessage(), e); + } + } + } + + @Override + public void onNotificationCountUpdate(String roomId) { + // Filter out events for other rooms + if (TextUtils.equals(mRoomId, roomId)) { + try { + mEventListener.onNotificationCountUpdate(roomId); + } catch (Exception e) { + Log.e(LOG_TAG, "onNotificationCountUpdate exception " + e.getMessage(), e); + } + } + } + + @Override + public void onNewRoom(String roomId) { + // Filter out events for other rooms + if (TextUtils.equals(mRoomId, roomId)) { + try { + mEventListener.onNewRoom(roomId); + } catch (Exception e) { + Log.e(LOG_TAG, "onNewRoom exception " + e.getMessage(), e); + } + } + } + + @Override + public void onJoinRoom(String roomId) { + // Filter out events for other rooms + if (TextUtils.equals(mRoomId, roomId)) { + try { + mEventListener.onJoinRoom(roomId); + } catch (Exception e) { + Log.e(LOG_TAG, "onJoinRoom exception " + e.getMessage(), e); + } + } + } + + @Override + public void onReceiptEvent(String roomId, List senderIds) { + // Filter out events for other rooms + if (TextUtils.equals(mRoomId, roomId)) { + try { + mEventListener.onReceiptEvent(roomId, senderIds); + } catch (Exception e) { + Log.e(LOG_TAG, "onReceiptEvent exception " + e.getMessage(), e); + } + } + } + + @Override + public void onRoomTagEvent(String roomId) { + // Filter out events for other rooms + if (TextUtils.equals(mRoomId, roomId)) { + try { + mEventListener.onRoomTagEvent(roomId); + } catch (Exception e) { + Log.e(LOG_TAG, "onRoomTagEvent exception " + e.getMessage(), e); + } + } + } + + @Override + public void onReadMarkerEvent(String roomId) { + // Filter out events for other rooms + if (TextUtils.equals(mRoomId, roomId)) { + try { + mEventListener.onReadMarkerEvent(roomId); + } catch (Exception e) { + Log.e(LOG_TAG, "onReadMarkerEvent exception " + e.getMessage(), e); + } + } + } + + @Override + public void onRoomFlush(String roomId) { + // Filter out events for other rooms + if (TextUtils.equals(mRoomId, roomId)) { + try { + mEventListener.onRoomFlush(roomId); + } catch (Exception e) { + Log.e(LOG_TAG, "onRoomFlush exception " + e.getMessage(), e); + } + } + } + + @Override + public void onLeaveRoom(String roomId) { + // Filter out events for other rooms + if (TextUtils.equals(mRoomId, roomId)) { + try { + mEventListener.onLeaveRoom(roomId); + } catch (Exception e) { + Log.e(LOG_TAG, "onLeaveRoom exception " + e.getMessage(), e); + } + } + } + + @Override + public void onRoomKick(String roomId) { + // Filter out events for other rooms + if (TextUtils.equals(mRoomId, roomId)) { + try { + mEventListener.onRoomKick(roomId); + } catch (Exception e) { + Log.e(LOG_TAG, "onRoomKick exception " + e.getMessage(), e); + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/network/NetworkConnectivityReceiver.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/network/NetworkConnectivityReceiver.java new file mode 100644 index 0000000000..4e24097b21 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/network/NetworkConnectivityReceiver.java @@ -0,0 +1,317 @@ +/* + * 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.network; + +import android.annotation.SuppressLint; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkInfo; +import android.os.Build; +import android.os.Bundle; +import android.telephony.TelephonyManager; + +import im.vector.matrix.android.internal.legacy.util.Log; + +import im.vector.matrix.android.internal.legacy.listeners.IMXNetworkEventListener; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +public class NetworkConnectivityReceiver extends BroadcastReceiver { + + private static final String LOG_TAG = NetworkConnectivityReceiver.class.getSimpleName(); + + // any network state listener + private final List mNetworkEventListeners = new ArrayList<>(); + + // the one call listeners are listeners which are expected to be called ONCE + // the device is connected to a data network + private final List mOnNetworkConnectedEventListeners = new ArrayList<>(); + + private boolean mIsConnected = false; + private boolean mIsUseWifiConnection = false; + private int mNetworkSubType = TelephonyManager.NETWORK_TYPE_UNKNOWN; + + @Override + public void onReceive(final Context context, final Intent intent) { + NetworkInfo networkInfo = null; + + if (null != intent) { + + Log.d(LOG_TAG, "## onReceive() : action " + intent.getAction()); + + Bundle extras = intent.getExtras(); + + if (null != extras) { + Set keys = extras.keySet(); + + for (String key : keys) { + Log.d(LOG_TAG, "## onReceive() : " + key + " -> " + extras.get(key)); + } + + if (extras.containsKey("networkInfo")) { + Object networkInfoAsVoid = extras.get("networkInfo"); + + if (networkInfoAsVoid instanceof NetworkInfo) { + networkInfo = (NetworkInfo) networkInfoAsVoid; + } + } + } + } else { + Log.d(LOG_TAG, "## onReceive()"); + } + + checkNetworkConnection(context, networkInfo); + } + + /** + * Check if there is a connection update. + * + * @param context the context + */ + public void checkNetworkConnection(Context context) { + checkNetworkConnection(context, null); + } + + /** + * Check if there is a connection update. + * + * @param context the context + */ + private void checkNetworkConnection(Context context, NetworkInfo aNetworkInfo) { + synchronized (LOG_TAG) { + try { + NetworkInfo networkInfo = aNetworkInfo; + + // https://issuetracker.google.com/issues/37137911 + // it seems that getActiveNetworkInfo does not provide the true active network connection + if (null == networkInfo) { + ConnectivityManager connMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + networkInfo = connMgr.getActiveNetworkInfo(); + } + boolean isConnected = (networkInfo != null) && networkInfo.isConnectedOrConnecting(); + + if (isConnected) { + Log.d(LOG_TAG, "## checkNetworkConnection() : Connected to " + networkInfo); + } else if (null != networkInfo) { + Log.d(LOG_TAG, "## checkNetworkConnection() : there is a default connection but it is not connected " + networkInfo); + listNetworkConnections(context); + } else { + Log.d(LOG_TAG, "## checkNetworkConnection() : there is no connection"); + listNetworkConnections(context); + } + + mIsUseWifiConnection = (null != networkInfo) && (networkInfo.getType() == ConnectivityManager.TYPE_WIFI); + mNetworkSubType = (null != networkInfo) ? networkInfo.getSubtype() : TelephonyManager.NETWORK_TYPE_UNKNOWN; + + // avoid triggering useless info + if (mIsConnected != isConnected) { + Log.d(LOG_TAG, "## checkNetworkConnection() : Warn there is a connection update"); + mIsConnected = isConnected; + onNetworkUpdate(); + } else { + Log.d(LOG_TAG, "## checkNetworkConnection() : No network update"); + } + } catch (Exception e) { + Log.e(LOG_TAG, "Failed to report :" + e.getMessage(), e); + } + } + } + + /** + * List the available network connections + * + * @param context the context + */ + @SuppressLint("deprecation") + private static void listNetworkConnections(Context context) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + List networkInfos = new ArrayList<>(); + + // + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Network[] activeNetworks = cm.getAllNetworks(); + if (null != activeNetworks) { + for (Network network : activeNetworks) { + NetworkInfo networkInfo = cm.getNetworkInfo(network); + if (null != networkInfo) { + networkInfos.add(networkInfo); + } + } + } + } else { + NetworkInfo[] info = cm.getAllNetworkInfo(); + + if (info != null) { + networkInfos.addAll(Arrays.asList(info)); + } + } + + Log.d(LOG_TAG, "## listNetworkConnections() : " + networkInfos.size() + " connections"); + + for (NetworkInfo networkInfo : networkInfos) { + Log.d(LOG_TAG, "-> " + networkInfo); + } + } + + /** + * Add a network event listener. + * + * @param networkEventListener the event listener to add + */ + public void addEventListener(final IMXNetworkEventListener networkEventListener) { + if (null != networkEventListener) { + mNetworkEventListeners.add(networkEventListener); + } + } + + /** + * Add a ONE CALL network event listener. + * The listener is called when a data connection is established. + * The listener is removed from the listeners list once its callback is called. + * + * @param networkEventListener the event listener to add + */ + public void addOnConnectedEventListener(final IMXNetworkEventListener networkEventListener) { + if (null != networkEventListener) { + synchronized (LOG_TAG) { + mOnNetworkConnectedEventListeners.add(networkEventListener); + } + } + } + + /** + * Remove a network event listener. + * + * @param networkEventListener the event listener to remove + */ + public void removeEventListener(final IMXNetworkEventListener networkEventListener) { + synchronized (LOG_TAG) { + mNetworkEventListeners.remove(networkEventListener); + mOnNetworkConnectedEventListeners.remove(networkEventListener); + } + } + + /** + * Remove all registered listeners + */ + public void removeListeners() { + synchronized (LOG_TAG) { + mNetworkEventListeners.clear(); + mOnNetworkConnectedEventListeners.clear(); + } + } + + /** + * Warn the listener that a network updated has been triggered + */ + private synchronized void onNetworkUpdate() { + for (IMXNetworkEventListener listener : mNetworkEventListeners) { + try { + listener.onNetworkConnectionUpdate(mIsConnected); + } catch (Exception e) { + Log.e(LOG_TAG, "## onNetworkUpdate() : onNetworkConnectionUpdate failed " + e.getMessage(), e); + } + } + + // onConnected listeners are called once + // and only when there is an available network connection + if (mIsConnected) { + for (IMXNetworkEventListener listener : mOnNetworkConnectedEventListeners) { + try { + listener.onNetworkConnectionUpdate(true); + } catch (Exception e) { + Log.e(LOG_TAG, "## onNetworkUpdate() : onNetworkConnectionUpdate failed " + e.getMessage(), e); + } + } + + mOnNetworkConnectedEventListeners.clear(); + } + } + + /** + * @return true if the application is connected to a data network + */ + public boolean isConnected() { + boolean res; + + synchronized (LOG_TAG) { + res = mIsConnected; + } + + Log.d(LOG_TAG, "## isConnected() : " + res); + + return res; + } + + + /** + * Tells if the connection is a wifi one + * + * @return true if a wifi connection is used + */ + public boolean useWifiConnection() { + boolean res; + + synchronized (LOG_TAG) { + res = mIsUseWifiConnection; + } + + Log.d(LOG_TAG, "## useWifiConnection() : " + res); + + return res; + } + + /** + * Provides a scale factor to apply to the request timeouts. + * + * @return the scale factor + */ + public float getTimeoutScale() { + float scale; + + synchronized (LOG_TAG) { + switch (mNetworkSubType) { + case TelephonyManager.NETWORK_TYPE_GPRS: + scale = 3.0f; + break; + case TelephonyManager.NETWORK_TYPE_EDGE: + scale = 2.5f; + break; + case TelephonyManager.NETWORK_TYPE_UMTS: + case TelephonyManager.NETWORK_TYPE_CDMA: + case TelephonyManager.NETWORK_TYPE_HSPA: + case TelephonyManager.NETWORK_TYPE_HSDPA: + case TelephonyManager.NETWORK_TYPE_HSUPA: + case TelephonyManager.NETWORK_TYPE_HSPAP: + scale = 2.0f; + break; + case TelephonyManager.NETWORK_TYPE_LTE: + scale = 1.5f; + break; + default: + scale = 1.0f; + break; + } + } + return scale; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/AccountDataApi.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/AccountDataApi.java new file mode 100644 index 0000000000..b25af37e40 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/AccountDataApi.java @@ -0,0 +1,48 @@ +/* + * 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.rest.api; + +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.POST; +import retrofit2.http.PUT; +import retrofit2.http.Path; + +public interface AccountDataApi { + + /** + * Set some account_data for the client. + * + * @param userId the user id + * @param type the type + * @param params the put params + */ + @PUT("user/{userId}/account_data/{type}") + Call setAccountData(@Path("userId") String userId, @Path("type") String type, @Body Object params); + + /** + * Gets a bearer token from the homeserver that the user can + * present to a third party in order to prove their ownership + * of the Matrix account they are logged into. + * + * @param userId the user id + * @param body the body content + */ + @POST("user/{userId}/openid/request_token") + Call> openIdToken(@Path("userId") String userId, @Body Map body); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/CallRulesApi.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/CallRulesApi.java new file mode 100644 index 0000000000..6c5a806906 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/CallRulesApi.java @@ -0,0 +1,26 @@ +/* + * 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.rest.api; + +import com.google.gson.JsonObject; + +import retrofit2.Call; +import retrofit2.http.GET; + +public interface CallRulesApi { + @GET("voip/turnServer") + Call getTurnServer(); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/CryptoApi.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/CryptoApi.java new file mode 100644 index 0000000000..58e37e628d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/CryptoApi.java @@ -0,0 +1,112 @@ +/* + * Copyright 2014 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.rest.api; + + +import im.vector.matrix.android.internal.legacy.rest.model.pid.DeleteDeviceParams; +import im.vector.matrix.android.internal.legacy.rest.model.sync.DevicesListResponse; +import im.vector.matrix.android.internal.legacy.rest.model.crypto.KeyChangesResponse; +import im.vector.matrix.android.internal.legacy.rest.model.crypto.KeysClaimResponse; +import im.vector.matrix.android.internal.legacy.rest.model.crypto.KeysQueryResponse; +import im.vector.matrix.android.internal.legacy.rest.model.crypto.KeysUploadResponse; + +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.HTTP; +import retrofit2.http.POST; +import retrofit2.http.PUT; +import retrofit2.http.Path; +import retrofit2.http.Query; + +public interface CryptoApi { + + /** + * Upload device and/or one-time keys. + * @param params the params. + */ + @POST("keys/upload") + Call uploadKeys(@Body Map params); + + /** + * Upload device and/or one-time keys. + * + * @param deviceId the deviceId + * @param params the params. + */ + @POST("keys/upload/{deviceId}") + Call uploadKeys(@Path("deviceId") String deviceId, @Body Map params); + + /** + * Download device keys. + * @param params the params. + */ + @POST("keys/query") + Call downloadKeysForUsers(@Body Map params); + + /** + * Claim one-time keys. + * @param params the params. + */ + @POST("keys/claim") + Call claimOneTimeKeysForUsersDevices(@Body Map params); + + /** + * Send an event to a specific list of devices + * + * @param eventType the type of event to send + * @param transactionId the random path item + * @param params the params + */ + @PUT("sendToDevice/{eventType}/{random}") + Call sendToDevice(@Path("eventType") String eventType, @Path("random") String transactionId, @Body Map params); + + /** + * Get the devices list + */ + @GET("devices") + Call getDevices(); + + /** + * Delete a device. + * + * @param deviceId the device id + * @param params the deletion parameters + */ + @HTTP(path = "devices/{device_id}", method = "DELETE", hasBody = true) + Call deleteDevice(@Path("device_id") String deviceId, @Body DeleteDeviceParams params); + + /** + * Update the device information. + * + * @param deviceId the device id + * @param params the params + */ + @PUT("devices/{device_id}") + Call updateDeviceInfo(@Path("device_id") String deviceId, @Body Map params); + + /** + * Get the update devices list from two sync token. + * + * @param oldToken the start token. + * @param newToken the up-to token. + */ + @GET("keys/changes") + Call getKeyChanges(@Query("from") String oldToken, @Query("to") String newToken); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/EventsApi.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/EventsApi.java new file mode 100644 index 0000000000..414616fae4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/EventsApi.java @@ -0,0 +1,101 @@ +/* + * Copyright 2014 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.rest.api; + +import im.vector.matrix.android.internal.legacy.RestClient; +import im.vector.matrix.android.internal.legacy.rest.model.Event; +import im.vector.matrix.android.internal.legacy.rest.model.pid.ThirdPartyProtocol; +import im.vector.matrix.android.internal.legacy.rest.model.publicroom.PublicRoomsParams; +import im.vector.matrix.android.internal.legacy.rest.model.publicroom.PublicRoomsResponse; +import im.vector.matrix.android.internal.legacy.rest.model.search.SearchParams; +import im.vector.matrix.android.internal.legacy.rest.model.search.SearchResponse; +import im.vector.matrix.android.internal.legacy.rest.model.search.SearchUsersParams; +import im.vector.matrix.android.internal.legacy.rest.model.search.SearchUsersRequestResponse; +import im.vector.matrix.android.internal.legacy.rest.model.sync.SyncResponse; + +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.Path; +import retrofit2.http.Query; +import retrofit2.http.QueryMap; + +/** + * The events API. + */ +public interface EventsApi { + + /** + * Perform the initial sync to find the rooms that concern the user, the participants' presence, etc. + * + * @param params the GET params. + */ + @GET(RestClient.URI_API_PREFIX_PATH_R0 + "sync") + Call sync(@QueryMap Map params); + + /** + * Retrieve an event from its event id + * + * @param eventId the event Id + */ + @GET(RestClient.URI_API_PREFIX_PATH_R0 + "events/{eventId}") + Call getEvent(@Path("eventId") String eventId); + + /** + * Get the third party server protocols. + */ + @GET(RestClient.URI_API_PREFIX_PATH_UNSTABLE + "thirdparty/protocols") + Call> thirdPartyProtocols(); + + /** + * Get the list of public rooms. + * + * @param server the server (might be null) + * @param publicRoomsParams the request params + */ + @POST(RestClient.URI_API_PREFIX_PATH_R0 + "publicRooms") + Call publicRooms(@Query("server") String server, @Body PublicRoomsParams publicRoomsParams); + + /** + * Perform a search. + * + * @param searchParams the search params. + * @param nextBatch the next batch token + */ + @POST(RestClient.URI_API_PREFIX_PATH_R0 + "search") + Call searchEvents(@Body SearchParams searchParams, @Query("next_batch") String nextBatch); + + /** + * Perform an users search. + * + * @param searchUsersParams the search params. + */ + @POST(RestClient.URI_API_PREFIX_PATH_R0 + "user_directory/search") + Call searchUsers(@Body SearchUsersParams searchUsersParams); + + /** + * Retrieve the preview information of an URL. + * + * @param url the URL + * @param ts the ts + */ + @GET(RestClient.URI_API_PREFIX_PATH_MEDIA_R0 + "preview_url") + Call> getURLPreview(@Query("url") String url, @Query("ts") long ts); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/FilterApi.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/FilterApi.java new file mode 100644 index 0000000000..4acade8dd5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/FilterApi.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018 Matthias Kesler + * 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.rest.api; + +import im.vector.matrix.android.internal.legacy.rest.model.filter.FilterBody; +import im.vector.matrix.android.internal.legacy.rest.model.filter.FilterResponse; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.Path; + +public interface FilterApi { + + /** + * Upload FilterBody to get a filter_id which can be used for /sync requests + * + * @param userId the user id + * @param body the Json representation of a FilterBody object + */ + @POST("user/{userId}/filter") + Call uploadFilter(@Path("userId") String userId, @Body FilterBody body); + + /** + * Gets a filter with a given filterId from the homeserver + * + * @param userId the user id + * @param filterId the filterID + * @return Filter + */ + @GET("user/{userId}/filter/{filterId}") + Call getFilterById(@Path("userId") String userId, @Path("filterId") String filterId); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/GroupsApi.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/GroupsApi.java new file mode 100644 index 0000000000..bfde2f6825 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/GroupsApi.java @@ -0,0 +1,196 @@ +/* + * Copyright 2014 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.rest.api; + +import im.vector.matrix.android.internal.legacy.rest.model.group.AcceptGroupInvitationParams; +import im.vector.matrix.android.internal.legacy.rest.model.group.AddGroupParams; +import im.vector.matrix.android.internal.legacy.rest.model.group.CreateGroupParams; +import im.vector.matrix.android.internal.legacy.rest.model.group.CreateGroupResponse; +import im.vector.matrix.android.internal.legacy.rest.model.group.GetGroupsResponse; +import im.vector.matrix.android.internal.legacy.rest.model.group.GetPublicisedGroupsResponse; +import im.vector.matrix.android.internal.legacy.rest.model.group.GroupInviteUserParams; +import im.vector.matrix.android.internal.legacy.rest.model.group.GroupInviteUserResponse; +import im.vector.matrix.android.internal.legacy.rest.model.group.GroupKickUserParams; +import im.vector.matrix.android.internal.legacy.rest.model.group.GroupProfile; +import im.vector.matrix.android.internal.legacy.rest.model.group.GroupRooms; +import im.vector.matrix.android.internal.legacy.rest.model.group.GroupSummary; +import im.vector.matrix.android.internal.legacy.rest.model.group.GroupUsers; +import im.vector.matrix.android.internal.legacy.rest.model.group.LeaveGroupParams; +import im.vector.matrix.android.internal.legacy.rest.model.group.UpdatePubliciseParams; + +import java.util.List; +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.DELETE; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.PUT; +import retrofit2.http.Path; + +/** + * The groups API. + */ +public interface GroupsApi { + + /** + * Create a group + * + * @param params the group creation params + */ + @POST("create_group") + Call createGroup(@Body CreateGroupParams params); + + /** + * Invite an user to a group. + * + * @param groupId the group id + * @param userId the user id + * @param params the invitation parameters + */ + @PUT("groups/{groupId}/admin/users/invite/{userId}") + Call inviteUser(@Path("groupId") String groupId, @Path("userId") String userId, @Body GroupInviteUserParams params); + + /** + * Kick an user from a group. + * + * @param groupId the group id + * @param userId the user id + * @param params the kick parameters + */ + @PUT("groups/{groupId}/users/remove/{userId}") + Call kickUser(@Path("groupId") String groupId, @Path("userId") String userId, @Body GroupKickUserParams params); + + /** + * Add a room in a group. + * + * @param groupId the group id + * @param roomId the room id + * @param params the kick parameters + */ + @PUT("groups/{groupId}/admin/rooms/{roomId}") + Call addRoom(@Path("groupId") String groupId, @Path("roomId") String roomId, @Body AddGroupParams params); + + /** + * Remove a room from a group. + * + * @param groupId the group id + * @param roomId the room id + */ + @DELETE("groups/{groupId}/admin/rooms/{roomId}") + Call removeRoom(@Path("groupId") String groupId, @Path("roomId") String roomId); + + /** + * Update the group profile. + * + * @param groupId the group id + * @param profile the group profile + */ + @POST("groups/{groupId}/profile") + Call updateProfile(@Path("groupId") String groupId, @Body GroupProfile profile); + + /** + * Get the group profile. + * + * @param groupId the group id + */ + @GET("groups/{groupId}/profile") + Call getProfile(@Path("groupId") String groupId); + + /** + * Request the invited users list. + * + * @param groupId the group id + */ + @GET("groups/{groupId}/invited_users") + Call getInvitedUsers(@Path("groupId") String groupId); + + /** + * Request the users list. + * + * @param groupId the group id + */ + @GET("groups/{groupId}/users") + Call getUsers(@Path("groupId") String groupId); + + /** + * Request the rooms list. + * + * @param groupId the group id + */ + @GET("groups/{groupId}/rooms") + Call getRooms(@Path("groupId") String groupId); + + /** + * Request a group summary + * + * @param groupId the group id + */ + @GET("groups/{groupId}/summary") + Call getSummary(@Path("groupId") String groupId); + + /** + * Accept an invitation in a group. + * + * @param groupId the group id + * @param params the parameters + */ + @PUT("groups/{groupId}/self/accept_invite") + Call acceptInvitation(@Path("groupId") String groupId, @Body AcceptGroupInvitationParams params); + + /** + * Leave a group + * + * @param groupId the group id + * @param params the parameters + */ + @PUT("groups/{groupId}/self/leave") + Call leave(@Path("groupId") String groupId, @Body LeaveGroupParams params); + + /** + * Update the publicity status. + * + * @param groupId the group id + * @param params the parameters + */ + @PUT("groups/{groupId}/self/update_publicity") + Call updatePublicity(@Path("groupId") String groupId, @Body UpdatePubliciseParams params); + + /** + * Request the joined group list. + */ + @GET("joined_groups") + Call getJoinedGroupIds(); + + // NOT FEDERATED + /** + * Request the publicised groups for an user id. + * + * @param userId the user id + */ + //@GET("publicised_groups/{userId}") + //Call getUserPublicisedGroups(@Path("userId") String userId); + + /** + * Request the publicised groups for user ids. + * + * @param params the request params + */ + @POST("publicised_groups") + Call getPublicisedGroups(@Body Map> params); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/LoginApi.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/LoginApi.java new file mode 100644 index 0000000000..ef9ea941d0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/LoginApi.java @@ -0,0 +1,70 @@ +/* + * 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.rest.api; + +import com.google.gson.JsonObject; + +import im.vector.matrix.android.internal.legacy.RestClient; +import im.vector.matrix.android.internal.legacy.rest.model.Versions; +import im.vector.matrix.android.internal.legacy.rest.model.login.LoginFlowResponse; +import im.vector.matrix.android.internal.legacy.rest.model.login.LoginParams; +import im.vector.matrix.android.internal.legacy.rest.model.login.RegistrationParams; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.POST; + +/** + * The login REST API. + */ +public interface LoginApi { + + /** + * Get the different login flows supported by the server. + */ + @GET(RestClient.URI_API_PREFIX_PATH + "versions") + Call versions(); + + /** + * Get the different login flows supported by the server. + */ + @GET(RestClient.URI_API_PREFIX_PATH_R0 + "login") + Call login(); + + /** + * Try to create an account + * + * @param params the registration params + */ + @POST(RestClient.URI_API_PREFIX_PATH_R0 + "register") + Call register(@Body RegistrationParams params); + + /** + * Pass params to the server for the current login phase. + * + * @param loginParams the login parameters + */ + @POST(RestClient.URI_API_PREFIX_PATH_R0 + "login") + Call login(@Body LoginParams loginParams); + + /** + * Invalidate the access token, so that it can no longer be used for authorization. + */ + @POST(RestClient.URI_API_PREFIX_PATH_R0 + "logout") + Call logout(); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/MediaScanApi.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/MediaScanApi.java new file mode 100644 index 0000000000..6b30215fbb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/MediaScanApi.java @@ -0,0 +1,63 @@ +/* + * 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.rest.api; + +import im.vector.matrix.android.internal.legacy.rest.model.EncryptedMediaScanBody; +import im.vector.matrix.android.internal.legacy.rest.model.EncryptedMediaScanEncryptedBody; +import im.vector.matrix.android.internal.legacy.rest.model.MediaScanPublicKeyResult; +import im.vector.matrix.android.internal.legacy.rest.model.MediaScanResult; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.Path; + +/** + * The matrix content scanner REST API. + */ +public interface MediaScanApi { + /** + * Get the current public curve25519 key that the AV server is advertising. + */ + @GET("public_key") + Call getServerPublicKey(); + + /** + * Scan an unencrypted file. + * + * @param domain the server name + * @param mediaId the user id + */ + @GET("scan/{domain}/{mediaId}") + Call scanUnencrypted(@Path("domain") String domain, @Path("mediaId") String mediaId); + + /** + * Scan an encrypted file. + * + * @param encryptedMediaScanBody the encryption information required to decrypt the content before scanning it. + */ + @POST("scan_encrypted") + Call scanEncrypted(@Body EncryptedMediaScanBody encryptedMediaScanBody); + + /** + * Scan an encrypted file, sending an encrypted body. + * + * @param encryptedMediaScanEncryptedBody the encrypted encryption information required to decrypt the content before scanning it. + */ + @POST("scan_encrypted") + Call scanEncrypted(@Body EncryptedMediaScanEncryptedBody encryptedMediaScanEncryptedBody); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/PresenceApi.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/PresenceApi.java new file mode 100644 index 0000000000..8a27048485 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/PresenceApi.java @@ -0,0 +1,35 @@ +/* + * 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.rest.api; + +import im.vector.matrix.android.internal.legacy.rest.model.User; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Path; + +/** + * The presence REST API. + */ +public interface PresenceApi { + /** + * Get a user's presence state. + * + * @param userId the user id + */ + @GET("presence/{userId}/status") + Call presenceStatus(@Path("userId") String userId); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/ProfileApi.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/ProfileApi.java new file mode 100644 index 0000000000..41d3fd8a7c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/ProfileApi.java @@ -0,0 +1,176 @@ +/* + * 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.rest.api; + +import im.vector.matrix.android.internal.legacy.RestClient; +import im.vector.matrix.android.internal.legacy.rest.model.DeactivateAccountParams; +import im.vector.matrix.android.internal.legacy.rest.model.RequestEmailValidationParams; +import im.vector.matrix.android.internal.legacy.rest.model.RequestEmailValidationResponse; +import im.vector.matrix.android.internal.legacy.rest.model.RequestPhoneNumberValidationParams; +import im.vector.matrix.android.internal.legacy.rest.model.RequestPhoneNumberValidationResponse; +import im.vector.matrix.android.internal.legacy.rest.model.pid.AccountThreePidsResponse; +import im.vector.matrix.android.internal.legacy.rest.model.pid.AddThreePidsParams; +import im.vector.matrix.android.internal.legacy.rest.model.ChangePasswordParams; +import im.vector.matrix.android.internal.legacy.rest.model.pid.DeleteThreePidParams; +import im.vector.matrix.android.internal.legacy.rest.model.ForgetPasswordParams; +import im.vector.matrix.android.internal.legacy.rest.model.ForgetPasswordResponse; +import im.vector.matrix.android.internal.legacy.rest.model.User; +import im.vector.matrix.android.internal.legacy.rest.model.login.TokenRefreshParams; +import im.vector.matrix.android.internal.legacy.rest.model.login.TokenRefreshResponse; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.PUT; +import retrofit2.http.Path; + + +/** + * The profile REST API. + */ +public interface ProfileApi { + + /** + * Update a user's display name. + * + * @param userId the user id + * @param user the user object containing the new display name + */ + @PUT(RestClient.URI_API_PREFIX_PATH_R0 + "profile/{userId}/displayname") + Call displayname(@Path("userId") String userId, @Body User user); + + /** + * Get a user's display name. + * + * @param userId the user id + */ + @GET(RestClient.URI_API_PREFIX_PATH_R0 + "profile/{userId}/displayname") + Call displayname(@Path("userId") String userId); + + /** + * Update a user's avatar URL. + * + * @param userId the user id + * @param user the user object containing the new avatar url + */ + @PUT(RestClient.URI_API_PREFIX_PATH_R0 + "profile/{userId}/avatar_url") + Call avatarUrl(@Path("userId") String userId, @Body User user); + + /** + * Get a user's avatar URL. + * + * @param userId the user id + */ + @GET(RestClient.URI_API_PREFIX_PATH_R0 + "profile/{userId}/avatar_url") + Call avatarUrl(@Path("userId") String userId); + + /** + * Update the password + * + * @param passwordParams the new password + */ + @POST(RestClient.URI_API_PREFIX_PATH_R0 + "account/password") + Call updatePassword(@Body ChangePasswordParams passwordParams); + + /** + * Reset the password server side. + * + * @param params the forget password params + */ + @POST(RestClient.URI_API_PREFIX_PATH_R0 + "account/password/email/requestToken") + Call forgetPassword(@Body ForgetPasswordParams params); + + /** + * Deactivate the user account + * + * @param params the deactivate account params + */ + @POST(RestClient.URI_API_PREFIX_PATH_R0 + "account/deactivate") + Call deactivate(@Body DeactivateAccountParams params); + + /** + * Pass params to the server for the token refresh phase. + * + * @param refreshParams the refresh token parameters + */ + @POST(RestClient.URI_API_PREFIX_PATH_R0 + "tokenrefresh") + Call tokenrefresh(@Body TokenRefreshParams refreshParams); + + /** + * List all 3PIDs linked to the Matrix user account. + */ + @GET(RestClient.URI_API_PREFIX_PATH_R0 + "account/3pid") + Call threePIDs(); + + /** + * Add an 3Pid to a user + * + * @param params the params + */ + @POST(RestClient.URI_API_PREFIX_PATH_R0 + "account/3pid") + Call add3PID(@Body AddThreePidsParams params); + + /** + * Delete a 3Pid of a user + * + * @param params the params + */ + @POST(RestClient.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/delete") + Call delete3PID(@Body DeleteThreePidParams params); + + /** + * Request a validation token for an email + * Note: Proxies the identity server API validate/email/requestToken, but first checks that + * the given email address is not already associated with an account on this Home Server. + * + * @param params the parameters + */ + @POST(RestClient.URI_API_PREFIX_PATH_R0 + "account/3pid/email/requestToken") + Call requestEmailValidation(@Body RequestEmailValidationParams params); + + /** + * Request a validation token for an email being added during registration process + * Note: Proxies the identity server API validate/email/requestToken, but first checks that + * the given email address is not already associated with an account on this Home Server. + * + * @param params the parameters + */ + @POST(RestClient.URI_API_PREFIX_PATH_R0 + "register/email/requestToken") + Call requestEmailValidationForRegistration(@Body RequestEmailValidationParams params); + + /** + * Request a validation token for a phone number + * Note: Proxies the identity server API validate/msisdn/requestToken, but first checks that + * the given phone number is not already associated with an account on this Home Server. + * + * @param params the parameters + */ + @POST(RestClient.URI_API_PREFIX_PATH_R0 + "account/3pid/msisdn/requestToken") + Call requestPhoneNumberValidation(@Body RequestPhoneNumberValidationParams params); + + /** + * Request a validation token for a phone number being added during registration process + * Note: Proxies the identity server API validate/msisdn/requestToken, but first checks that + * the given phone number is not already associated with an account on this Home Server. + * + * @param params the parameters + */ + @POST(RestClient.URI_API_PREFIX_PATH_R0 + "register/msisdn/requestToken") + Call requestPhoneNumberValidationForRegistration(@Body RequestPhoneNumberValidationParams params); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/PushRulesApi.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/PushRulesApi.java new file mode 100644 index 0000000000..251db04d79 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/PushRulesApi.java @@ -0,0 +1,78 @@ +/* + * 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.rest.api; + +import com.google.gson.JsonElement; + +import im.vector.matrix.android.internal.legacy.rest.model.bingrules.PushRulesResponse; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.DELETE; +import retrofit2.http.GET; +import retrofit2.http.PUT; +import retrofit2.http.Path; + +public interface PushRulesApi { + + /** + * Get all push rules + */ + @GET("pushrules/") + Call getAllRules(); + + /** + * Update the ruleID enable status + * + * @param kind the notification kind (sender, room...) + * @param ruleId the ruleId + * @param enable the new enable status + */ + @PUT("pushrules/global/{kind}/{ruleId}/enabled") + Call updateEnableRuleStatus(@Path("kind") String kind, @Path("ruleId") String ruleId, @Body Boolean enable); + + + /** + * Update the ruleID enable status + * + * @param kind the notification kind (sender, room...) + * @param ruleId the ruleId + * @param actions the actions + */ + @PUT("pushrules/global/{kind}/{ruleId}/actions") + Call updateRuleActions(@Path("kind") String kind, @Path("ruleId") String ruleId, @Body Object actions); + + + /** + * Update the ruleID enable status + * + * @param kind the notification kind (sender, room...) + * @param ruleId the ruleId + */ + @DELETE("pushrules/global/{kind}/{ruleId}") + Call deleteRule(@Path("kind") String kind, @Path("ruleId") String ruleId); + + /** + * Add the ruleID enable status + * + * @param kind the notification kind (sender, room...) + * @param ruleId the ruleId. + * @param rule the rule to add. + */ + @PUT("pushrules/global/{kind}/{ruleId}") + Call addRule(@Path("kind") String kind, @Path("ruleId") String ruleId, @Body JsonElement rule); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/PushersApi.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/PushersApi.java new file mode 100644 index 0000000000..e7e72b6618 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/PushersApi.java @@ -0,0 +1,36 @@ +/* + * 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.rest.api; + +import im.vector.matrix.android.internal.legacy.data.Pusher; +import im.vector.matrix.android.internal.legacy.rest.model.PushersResponse; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.POST; + +/** + * The pusher API + */ +public interface PushersApi { + @POST("pushers/set") + Call set(@Body Pusher pusher); + + @GET("pushers") + Call get(); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/RoomsApi.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/RoomsApi.java new file mode 100644 index 0000000000..f9370133f6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/RoomsApi.java @@ -0,0 +1,410 @@ +/* + * 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.rest.api; + +import android.support.annotation.Nullable; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import im.vector.matrix.android.internal.legacy.rest.model.BannedUser; +import im.vector.matrix.android.internal.legacy.rest.model.ChunkEvents; +import im.vector.matrix.android.internal.legacy.rest.model.CreateRoomParams; +import im.vector.matrix.android.internal.legacy.rest.model.CreateRoomResponse; +import im.vector.matrix.android.internal.legacy.rest.model.CreatedEvent; +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.PowerLevels; +import im.vector.matrix.android.internal.legacy.rest.model.ReportContentParams; +import im.vector.matrix.android.internal.legacy.rest.model.RoomAliasDescription; +import im.vector.matrix.android.internal.legacy.rest.model.RoomDirectoryVisibility; +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.Typing; +import im.vector.matrix.android.internal.legacy.rest.model.User; +import im.vector.matrix.android.internal.legacy.rest.model.message.Message; +import im.vector.matrix.android.internal.legacy.rest.model.sync.RoomResponse; + +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.DELETE; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.PUT; +import retrofit2.http.Path; +import retrofit2.http.Query; + +/** + * The rooms REST API. + */ +public interface RoomsApi { + + /** + * Send an event to a room. + * + * @param txId the transaction Id + * @param roomId the room id + * @param eventType the event type + * @param content the event content + */ + @PUT("rooms/{roomId}/send/{eventType}/{txId}") + Call send(@Path("txId") String txId, @Path("roomId") String roomId, @Path("eventType") String eventType, @Body JsonObject content); + + /** + * Send a message to the specified room. + * + * @param txId the transaction Id + * @param roomId the room id + * @param message the message + */ + @PUT("rooms/{roomId}/send/m.room.message/{txId}") + Call sendMessage(@Path("txId") String txId, @Path("roomId") String roomId, @Body Message message); + + /** + * Update the power levels + * + * @param roomId the room id + * @param powerLevels the new power levels + */ + @PUT("rooms/{roomId}/state/m.room.power_levels") + Call setPowerLevels(@Path("roomId") String roomId, @Body PowerLevels powerLevels); + + /** + * Send a generic state events + * + * @param roomId the room id. + * @param stateEventType the state event type + * @param params the request parameters + */ + @PUT("rooms/{roomId}/state/{state_event_type}") + Call sendStateEvent(@Path("roomId") String roomId, @Path("state_event_type") String stateEventType, @Body Map params); + + /** + * Send a generic state events + * + * @param roomId the room id. + * @param stateEventType the state event type + * @param stateKey the state keys + * @param params the request parameters + */ + @PUT("rooms/{roomId}/state/{state_event_type}/{stateKey}") + Call sendStateEvent(@Path("roomId") String roomId, + @Path("state_event_type") String stateEventType, + @Path("stateKey") String stateKey, + @Body Map params); + + /** + * Looks up the contents of a state event in a room + * + * @param roomId the room id + * @param eventType the event type + */ + @GET("rooms/{roomId}/state/{eventType}") + Call getStateEvent(@Path("roomId") String roomId, @Path("eventType") String eventType); + + /** + * Looks up the contents of a state event in a room + * + * @param roomId the room id + * @param eventType the event type + * @param stateKey the key of the state to look up + */ + @GET("rooms/{roomId}/state/{eventType}/{stateKey}") + Call getStateEvent(@Path("roomId") String roomId, @Path("eventType") String eventType, @Path("stateKey") String stateKey); + + /** + * Invite a user to the given room. + * + * @param roomId the room id + * @param user a user object that just needs a user id + */ + @POST("rooms/{roomId}/invite") + Call invite(@Path("roomId") String roomId, @Body User user); + + /** + * Trigger an invitation with a parameters set. + * + * @param roomId the room id + * @param params the parameters + */ + @POST("rooms/{roomId}/invite") + Call invite(@Path("roomId") String roomId, @Body Map params); + + /** + * Join the given room. + * + * @param roomId the room id + * @param content the request body + */ + @POST("rooms/{roomId}/join") + Call join(@Path("roomId") String roomId, @Body JsonObject content); + + /** + * Join the room with a room id or an alias. + * + * @param roomAliasOrId a room alias (or room id) + * @param params the extra join param + */ + @POST("join/{roomAliasOrId}") + Call joinRoomByAliasOrId(@Path("roomAliasOrId") String roomAliasOrId, @Body Map params); + + /** + * Leave the given room. + * + * @param roomId the room id + * @param content the request body + */ + @POST("rooms/{roomId}/leave") + Call leave(@Path("roomId") String roomId, @Body JsonObject content); + + /** + * Forget the given room. + * + * @param roomId the room id + * @param content the request body + */ + @POST("rooms/{roomId}/forget") + Call forget(@Path("roomId") String roomId, @Body JsonObject content); + + /** + * Ban a user from the given room. + * + * @param roomId the room id + * @param user the banned user object (userId and reason for ban) + */ + @POST("rooms/{roomId}/ban") + Call ban(@Path("roomId") String roomId, @Body BannedUser user); + + /** + * unban a user from the given room. + * + * @param roomId the room id + * @param user the banned user object (userId and reason for unban) + */ + @POST("rooms/{roomId}/unban") + Call unban(@Path("roomId") String roomId, @Body BannedUser user); + + /** + * Change the membership state for a user in a room. + * + * @param roomId the room id + * @param userId the user id + * @param member object containing the membership field to set + */ + @PUT("rooms/{roomId}/state/m.room.member/{userId}") + Call updateRoomMember(@Path("roomId") String roomId, @Path("userId") String userId, @Body RoomMember member); + + /** + * Update the typing notification + * + * @param roomId the room id + * @param userId the user id + * @param typing the typing notification + */ + @PUT("rooms/{roomId}/typing/{userId}") + Call setTypingNotification(@Path("roomId") String roomId, @Path("userId") String userId, @Body Typing typing); + + /** + * Create a room. + * + * @param createRoomRequest the creation room request + */ + @POST("createRoom") + Call createRoom(@Body CreateRoomParams createRoomRequest); + + /** + * Get a list of messages starting from a reference. + * + * @param roomId the room id + * @param from the token identifying where to start. Required. + * @param dir The direction to return messages from. Required. + * @param limit the maximum number of messages to retrieve. Optional. + * @param filter A JSON RoomEventFilter to filter returned events with. Optional. + */ + @GET("rooms/{roomId}/messages") + Call getRoomMessagesFrom(@Path("roomId") String roomId, + @Query("from") String from, + @Query("dir") String dir, + @Query("limit") int limit, + @Nullable @Query("filter") String filter); + + /** + * Get the initial information concerning a specific room. + * + * @param roomId the room id + * @param limit the maximum number of messages to retrieve + */ + @GET("rooms/{roomId}/initialSync") + Call initialSync(@Path("roomId") String roomId, @Query("limit") int limit); + + /** + * Get the context surrounding an event. + * + * @param roomId the room id + * @param eventId the event Id + * @param limit the maximum number of messages to retrieve + * @param filter A JSON RoomEventFilter to filter returned events with. Optional. + */ + @GET("rooms/{roomId}/context/{eventId}") + Call getContextOfEvent(@Path("roomId") String roomId, + @Path("eventId") String eventId, + @Query("limit") int limit, + @Nullable @Query("filter") String filter); + + /** + * Retrieve an event from its room id / events id + * + * @param roomId the room id + * @param eventId the event Id + */ + @GET("rooms/{roomId}/event/{eventId}") + Call getEvent(@Path("roomId") String roomId, @Path("eventId") String eventId); + + /** + * Redact an event from the room>. + * + * @param roomId the room id + * @param eventId the event id of the event to redact + * @param reason the reason + */ + @POST("rooms/{roomId}/redact/{eventId}") + Call redactEvent(@Path("roomId") String roomId, @Path("eventId") String eventId, @Body JsonObject reason); + + /** + * Report an event content. + * + * @param roomId the room id + * @param eventId the event id of the event to redact + * @param param the request parameters + */ + @POST("rooms/{roomId}/report/{eventId}") + Call reportEvent(@Path("roomId") String roomId, @Path("eventId") String eventId, @Body ReportContentParams param); + + /** + * Send a read receipt. + * + * @param roomId the room id + * @param EventId the latest eventId + * @param content the event content + */ + @POST("rooms/{roomId}/receipt/m.read/{eventId}") + Call sendReadReceipt(@Path("roomId") String roomId, @Path("eventId") String EventId, @Body JsonObject content); + + /** + * Send read markers. + * + * @param roomId the room id + * @param markers the read markers + */ + @POST("rooms/{roomId}/read_markers") + Call sendReadMarker(@Path("roomId") String roomId, @Body Map markers); + + /** + * Add a tag to a room + * + * @param userId the userId + * @param roomId the room id + * @param tag the new room tag + * @param content the event content + */ + @PUT("user/{userId}/rooms/{roomId}/tags/{tag}") + Call addTag(@Path("userId") String userId, @Path("roomId") String roomId, @Path("tag") String tag, @Body Map content); + + /** + * Remove a tag to a room + * + * @param userId the userId + * @param roomId the room id + * @param tag the new room tag + */ + @DELETE("user/{userId}/rooms/{roomId}/tags/{tag}") + Call removeTag(@Path("userId") String userId, @Path("roomId") String roomId, @Path("tag") String tag); + + /** + * Update a dedicated account data field + * + * @param userId the userId + * @param roomId the room id + * @param subPath the url sub path + * @param content the event content + */ + @PUT("user/{userId}/rooms/{roomId}/account_data/{tag}") + Call updateAccountData(@Path("userId") String userId, + @Path("roomId") String roomId, + @Path("tag") String subPath, + @Body Map content); + + /** + * Get the room ID associated to the room alias. + * + * @param roomAlias the room alias. + */ + @GET("directory/room/{roomAlias}") + Call getRoomIdByAlias(@Path("roomAlias") String roomAlias); + + /** + * Associate a room alias with a room ID. + * + * @param roomAlias the room alias. + * @param description the alias description containing the room ID + */ + @PUT("directory/room/{roomAlias}") + Call setRoomIdByAlias(@Path("roomAlias") String roomAlias, @Body RoomAliasDescription description); + + /** + * Get the room ID corresponding to this room alias. + * + * @param roomAlias the room alias. + */ + @DELETE("directory/room/{roomAlias}") + Call removeRoomAlias(@Path("roomAlias") String roomAlias); + + /** + * Set the visibility of the given room in the list directory. If the visibility is set to public, the room + * name is listed among the directory list. + * + * @param roomId the room id where to apply the request + * @param roomDirectoryVisibility the put params containing the new "visibility" field + */ + @PUT("directory/list/room/{roomId}") + Call setRoomDirectoryVisibility(@Path("roomId") String roomId, RoomDirectoryVisibility roomDirectoryVisibility); + + /** + * Get the visibility of the given room in the list directory. + * + * @param roomId the room id where to apply the request + */ + @GET("directory/list/room/{roomId}") + Call getRoomDirectoryVisibility(@Path("roomId") String roomId); + + /** + * Get all members of a room + * + * @param roomId the room id where to get the members + * @param syncToken the sync token (optional) + * @param membership to include only one type of membership (optional) + * @param notMembership to exclude one type of membership (optional) + */ + @GET("rooms/{roomId}/members") + Call getMembers(@Path("roomId") String roomId, + @Nullable @Query("at") String syncToken, + @Nullable @Query("membership") String membership, + @Nullable @Query("not_membership") String notMembership); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/ThirdPidApi.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/ThirdPidApi.java new file mode 100644 index 0000000000..57203b4fd5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/api/ThirdPidApi.java @@ -0,0 +1,68 @@ +/* + * Copyright 2014 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.rest.api; + +import im.vector.matrix.android.internal.legacy.rest.model.BulkLookupParams; +import im.vector.matrix.android.internal.legacy.rest.model.BulkLookupResponse; +import im.vector.matrix.android.internal.legacy.rest.model.RequestEmailValidationParams; +import im.vector.matrix.android.internal.legacy.rest.model.pid.PidResponse; + +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.Path; +import retrofit2.http.Query; + +public interface ThirdPidApi { + + /** + * Get the 3rd party id from a medium + * + * @param address the address. + * @param medium the medium. + */ + @GET("lookup") + Call lookup3Pid(@Query("address") String address, + @Query("medium") String medium); + + /** + * Request a bunch of 3PIDs + * + * @param body the body request + */ + @POST("bulk_lookup") + Call bulkLookup(@Body BulkLookupParams body); + + + /** + * Request the ownership validation of an email address or a phone number previously set + * by {@link ProfileApi#requestEmailValidation(RequestEmailValidationParams)} + * + * @param medium the medium of the 3pid + * @param token the token generated by the requestToken call + * @param clientSecret the client secret which was supplied in the requestToken call + * @param sid the sid for the session + */ + @POST("validate/{medium}/submitToken") + Call> requestOwnershipValidation(@Path("medium") String medium, + @Query("token") String token, + @Query("client_secret") String clientSecret, + @Query("sid") String sid); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/ApiCallback.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/ApiCallback.java new file mode 100644 index 0000000000..8b9d7b4add --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/ApiCallback.java @@ -0,0 +1,26 @@ +/* + * 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.rest.callback; + +/** + * Generic callback interface for asynchronously returning information. + * + * @param the type of information to return on success + */ +public interface ApiCallback extends SuccessCallback, ApiFailureCallback { + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/ApiFailureCallback.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/ApiFailureCallback.java new file mode 100644 index 0000000000..29b04200ec --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/ApiFailureCallback.java @@ -0,0 +1,45 @@ +/* + * 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.rest.callback; + +import im.vector.matrix.android.internal.legacy.rest.model.MatrixError; + +/** + * Callback interface for asynchronously returning API call failures. + */ +public interface ApiFailureCallback { + + /** + * Called if there is a network error. + * + * @param e the exception + */ + void onNetworkError(Exception e); + + /** + * Called in case of a Matrix error. + * + * @param e the Matrix error + */ + void onMatrixError(MatrixError e); + + /** + * Called for some other type of error. + * + * @param e the exception + */ + void onUnexpectedError(Exception e); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/DefaultRetrofit2CallbackWrapper.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/DefaultRetrofit2CallbackWrapper.java new file mode 100644 index 0000000000..271e8de7bf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/DefaultRetrofit2CallbackWrapper.java @@ -0,0 +1,48 @@ +package im.vector.matrix.android.internal.legacy.rest.callback; + +import im.vector.matrix.android.internal.legacy.rest.model.HttpError; +import im.vector.matrix.android.internal.legacy.rest.model.HttpException; + +import java.io.IOException; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class DefaultRetrofit2CallbackWrapper + implements Callback, DefaultRetrofit2ResponseHandler.Listener { + + private final ApiCallback apiCallback; + + public DefaultRetrofit2CallbackWrapper(ApiCallback apiCallback) { + this.apiCallback = apiCallback; + } + + public ApiCallback getApiCallback() { + return apiCallback; + } + + @Override public void onResponse(Call call, Response response) { + try { + handleResponse(response); + } catch (IOException e) { + apiCallback.onUnexpectedError(e); + } + } + + private void handleResponse(Response response) throws IOException { + DefaultRetrofit2ResponseHandler.handleResponse(response, this); + } + + @Override public void onFailure(Call call, Throwable t) { + apiCallback.onNetworkError((Exception) t); + } + + @Override public void onSuccess(Response response) { + apiCallback.onSuccess(response.body()); + } + + @Override public void onHttpError(HttpError httpError) { + apiCallback.onNetworkError(new HttpException(httpError)); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/DefaultRetrofit2ResponseHandler.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/DefaultRetrofit2ResponseHandler.java new file mode 100644 index 0000000000..74b5c645e0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/DefaultRetrofit2ResponseHandler.java @@ -0,0 +1,24 @@ +package im.vector.matrix.android.internal.legacy.rest.callback; + +import im.vector.matrix.android.internal.legacy.rest.model.HttpError; + +import java.io.IOException; + +import retrofit2.Response; + +public class DefaultRetrofit2ResponseHandler { + public static void handleResponse(Response response, Listener listener) + throws IOException { + if (response.isSuccessful()) { + listener.onSuccess(response); + } else { + String errorBody = response.errorBody().string(); + listener.onHttpError(new HttpError(errorBody, response.code())); + } + } + + public interface Listener { + void onSuccess(Response response); + void onHttpError(HttpError httpError); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/RestAdapterCallback.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/RestAdapterCallback.java new file mode 100644 index 0000000000..3480831321 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/RestAdapterCallback.java @@ -0,0 +1,274 @@ +/* + * 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.rest.callback; + +import com.google.gson.JsonSyntaxException; +import com.google.gson.stream.MalformedJsonException; + +import im.vector.matrix.android.internal.legacy.rest.model.HttpError; +import im.vector.matrix.android.internal.legacy.rest.model.HttpException; +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 im.vector.matrix.android.internal.legacy.util.UnsentEventsManager; + +import java.io.IOException; + +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class RestAdapterCallback implements Callback { + + private static final String LOG_TAG = "RestAdapterCallback"; + + /** + * Callback when a request failed after a network error. + * This callback should manage the request auto resent. + */ + public interface RequestRetryCallBack { + void onRetry(); + } + + // the event description + private final String mEventDescription; + + // the callback + // FIXME It should be safer if the type was ApiCallback, else onSuccess() has to be overridden + private final ApiCallback mApiCallback; + + // the retry callback + private final RequestRetryCallBack mRequestRetryCallBack; + + // the unsent events manager + private final UnsentEventsManager mUnsentEventsManager; + + // true to do not test if he event time line when sending again + // the request when a data connection is retrieved. + private final boolean mIgnoreEventTimeLifeInOffline; + + /** + * Constructor with unsent events management + * + * @param description the event description + * @param unsentEventsManager the unsent events manager + * @param apiCallback the callback + * @param requestRetryCallBack the retry callback + */ + public RestAdapterCallback(String description, + UnsentEventsManager unsentEventsManager, + ApiCallback apiCallback, + RequestRetryCallBack requestRetryCallBack) { + this(description, unsentEventsManager, false, apiCallback, requestRetryCallBack); + } + + /** + * Constructor with unsent events management + * + * @param description the event description + * @param ignoreEventTimeLifeOffline true to ignore the event time when resending the event. + * @param unsentEventsManager the unsent events manager + * @param apiCallback the callback + * @param requestRetryCallBack the retry callback + */ + public RestAdapterCallback(String description, + UnsentEventsManager unsentEventsManager, + boolean ignoreEventTimeLifeOffline, + ApiCallback apiCallback, + RequestRetryCallBack requestRetryCallBack) { + if (null != description) { + Log.d(LOG_TAG, "Trigger the event [" + description + "]"); + } + + mEventDescription = description; + mIgnoreEventTimeLifeInOffline = ignoreEventTimeLifeOffline; + mApiCallback = apiCallback; + mRequestRetryCallBack = requestRetryCallBack; + mUnsentEventsManager = unsentEventsManager; + } + + /** + * Notify the {@link UnsentEventsManager} that the event has been successfully sent. + * This method must be called each time a REST call succeed, in order to warn + * the {@link UnsentEventsManager} to send the next unsent events. + */ + protected void onEventSent() { + if (null != mUnsentEventsManager) { + try { + // some users reported that their devices were connected + // whereas this receiver was not called + if (!mUnsentEventsManager.getNetworkConnectivityReceiver().isConnected()) { + Log.d(LOG_TAG, "## onEventSent(): request succeed, while network seen as disconnected => ask ConnectivityReceiver to dispatch info"); + mUnsentEventsManager.getNetworkConnectivityReceiver().checkNetworkConnection(mUnsentEventsManager.getContext()); + } + + mUnsentEventsManager.onEventSent(mApiCallback); + } catch (Exception e) { + Log.e(LOG_TAG, "## onEventSent(): Exception " + e.getMessage(), e); + } + } + } + + @Override + public void onResponse(Call call, final Response response) { + try { + handleResponse(response); + } catch (IOException e) { + onFailure(call, e); + } + } + + private void handleResponse(final Response response) throws IOException { + DefaultRetrofit2ResponseHandler.handleResponse( + response, + new DefaultRetrofit2ResponseHandler.Listener() { + @Override + public void onSuccess(Response response) { + success(response.body(), response); + } + + @Override + public void onHttpError(HttpError httpError) { + failure(response, new HttpException(httpError)); + } + } + ); + } + + @Override + public void onFailure(Call call, Throwable t) { + failure(null, (Exception) t); + } + + public void success(T t, Response response) { + if (null != mEventDescription) { + Log.d(LOG_TAG, "## Succeed() : [" + mEventDescription + "]"); + } + + // add try catch to prevent application crashes while managing destroyed object + try { + onEventSent(); + + if (null != mApiCallback) { + try { + mApiCallback.onSuccess(t); + } catch (Exception e) { + Log.e(LOG_TAG, "## succeed() : onSuccess failed " + e.getMessage(), e); + mApiCallback.onUnexpectedError(e); + } + } + } catch (Exception e) { + // privacy + Log.e(LOG_TAG, "## succeed(): Exception " + e.getMessage(), e); + } + } + + /** + * Default failure implementation that calls the right error handler + * + * @param response the retrofit response + * @param exception the retrofit exception + */ + public void failure(Response response, Exception exception) { + if (null != mEventDescription) { + String error = exception != null + ? exception.getMessage() + : (response != null ? response.message() : "unknown"); + + Log.d(LOG_TAG, "## failure(): [" + mEventDescription + "]" + " with error " + error); + } + + boolean retry = true; + + if (null != response) { + retry = (response.code() < 400) || (response.code() > 500); + } + + // do not retry if the response format is not the expected one. + retry &= (null == exception.getCause()) + || !(exception.getCause() instanceof MalformedJsonException || exception.getCause() instanceof JsonSyntaxException); + + if (retry && (null != mUnsentEventsManager)) { + Log.d(LOG_TAG, "Add it to the UnsentEventsManager"); + mUnsentEventsManager.onEventSendingFailed(mEventDescription, mIgnoreEventTimeLifeInOffline, response, exception, mApiCallback, + mRequestRetryCallBack); + } else { + if (exception != null && exception instanceof IOException) { + try { + if (null != mApiCallback) { + try { + mApiCallback.onNetworkError(exception); + } catch (Exception e) { + Log.e(LOG_TAG, "## failure(): onNetworkError " + exception.getLocalizedMessage(), exception); + } + } + } catch (Exception e) { + // privacy + //Log.e(LOG_TAG, "Exception NetworkError " + e.getMessage() + " while managing " + error.getUrl()); + Log.e(LOG_TAG, "## failure(): NetworkError " + e.getMessage(), e); + } + } else { + // Try to convert this into a Matrix error + MatrixError mxError; + try { + HttpError error = ((HttpException) exception).getHttpError(); + ResponseBody errorBody = response.errorBody(); + + String bodyAsString = error.getErrorBody(); + mxError = JsonUtils.getGson(false).fromJson(bodyAsString, MatrixError.class); + + mxError.mStatus = response.code(); + mxError.mReason = response.message(); + mxError.mErrorBodyMimeType = errorBody.contentType(); + mxError.mErrorBody = errorBody; + mxError.mErrorBodyAsString = bodyAsString; + } catch (Exception e) { + mxError = null; + } + if (mxError != null) { + if (MatrixError.LIMIT_EXCEEDED.equals(mxError.errcode) && (null != mUnsentEventsManager)) { + mUnsentEventsManager.onEventSendingFailed(mEventDescription, mIgnoreEventTimeLifeInOffline, response, exception, mApiCallback, + mRequestRetryCallBack); + } else if (MatrixError.isConfigurationErrorCode(mxError.errcode) && (null != mUnsentEventsManager)) { + mUnsentEventsManager.onConfigurationErrorCode(mxError.errcode, mEventDescription); + } else { + try { + if (null != mApiCallback) { + mApiCallback.onMatrixError(mxError); + } + } catch (Exception e) { + // privacy + //Log.e(LOG_TAG, "Exception MatrixError " + e.getMessage() + " while managing " + error.getUrl()); + Log.e(LOG_TAG, "## failure(): MatrixError " + e.getMessage(), e); + } + } + } else { + try { + if (null != mApiCallback) { + mApiCallback.onUnexpectedError(exception); + } + } catch (Exception e) { + // privacy + //Log.e(LOG_TAG, "Exception UnexpectedError " + e.getMessage() + " while managing " + error.getUrl()); + Log.e(LOG_TAG, "## failure(): UnexpectedError " + e.getMessage(), e); + } + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/SimpleApiCallback.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/SimpleApiCallback.java new file mode 100644 index 0000000000..c4a5ff299d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/SimpleApiCallback.java @@ -0,0 +1,139 @@ +/* + * 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.rest.callback; + +import android.app.Activity; +import android.content.Context; + +import im.vector.matrix.android.internal.legacy.util.Log; + +import android.view.View; +import android.widget.Toast; + +import im.vector.matrix.android.internal.legacy.rest.model.MatrixError; + +/** + * A stub implementation of {@link ApiCallback} which only chosen callbacks + * can be implemented. + */ +public abstract class SimpleApiCallback implements ApiCallback { + + private static final String LOG_TAG = "SimpleApiCallback"; + + private Activity mActivity; + + private Context mContext = null; + private View mPostView = null; + + /** + * Failure callback to pass on failures to. + */ + private ApiFailureCallback failureCallback = null; + + /** + * Constructor + */ + public SimpleApiCallback() { + } + + /** + * Constructor + * + * @param activity The context. + */ + public SimpleApiCallback(Activity activity) { + mActivity = activity; + } + + /** + * Constructor + * + * @param context The context. + * @param postOnView the view to post the code to execute + */ + public SimpleApiCallback(Context context, View postOnView) { + mContext = context; + mPostView = postOnView; + } + + /** + * Constructor to delegate failure callback to another object. This allows us to stack failure callback implementations + * in a decorator-type approach. + * + * @param failureCallback the failure callback implementation to delegate to + */ + public SimpleApiCallback(ApiFailureCallback failureCallback) { + this.failureCallback = failureCallback; + } + + private void displayToast(final String message) { + if (null != mActivity) { + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(mActivity, message, Toast.LENGTH_SHORT).show(); + } + }); + } else if ((null != mContext) && (null != mPostView)) { + mPostView.post(new Runnable() { + @Override + public void run() { + Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show(); + } + }); + } + } + + @Override + public void onNetworkError(Exception e) { + if (failureCallback != null) { + try { + failureCallback.onNetworkError(e); + } catch (Exception exception) { + Log.e(LOG_TAG, "## onNetworkError() failed" + exception.getMessage(), exception); + } + } else { + displayToast("Network Error"); + } + } + + @Override + public void onMatrixError(final MatrixError e) { + if (failureCallback != null) { + try { + failureCallback.onMatrixError(e); + } catch (Exception exception) { + Log.e(LOG_TAG, "## onMatrixError() failed" + exception.getMessage(), exception); + } + } else { + displayToast("Matrix Error : " + e.getLocalizedMessage()); + } + } + + @Override + public void onUnexpectedError(final Exception e) { + if (failureCallback != null) { + try { + failureCallback.onUnexpectedError(e); + } catch (Exception exception) { + Log.e(LOG_TAG, "## onUnexpectedError() failed" + exception.getMessage(), exception); + } + } else { + displayToast(e.getLocalizedMessage()); + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/SuccessCallback.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/SuccessCallback.java new file mode 100644 index 0000000000..1e624b69f4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/SuccessCallback.java @@ -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.rest.callback; + +public interface SuccessCallback { + /** + * Called if the result is successful. + * + * @param info the returned information + */ + void onSuccess(T info); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/ToastErrorHandler.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/ToastErrorHandler.java new file mode 100644 index 0000000000..cdfbad4e27 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/callback/ToastErrorHandler.java @@ -0,0 +1,60 @@ +/* + * 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.rest.callback; + +import android.content.Context; +import android.widget.Toast; + +import im.vector.matrix.android.internal.legacy.rest.model.MatrixError; + +/** + * Failure callback that shows different toast messages. + */ +public class ToastErrorHandler implements ApiFailureCallback { + + private final Context context; + private final String msgPrefix; + + /** + * Constructor with context for the toast messages and a common prefix for messages. + * + * @param context the context - needed for toast + * @param msgPrefix the message prefix + */ + public ToastErrorHandler(Context context, String msgPrefix) { + this.context = context; + this.msgPrefix = msgPrefix; + } + + @Override + public void onNetworkError(Exception e) { + Toast.makeText(context, appendPrefix("Connection error"), Toast.LENGTH_LONG).show(); + } + + @Override + public void onMatrixError(MatrixError e) { + Toast.makeText(context, appendPrefix(e.getLocalizedMessage()), Toast.LENGTH_LONG).show(); + } + + @Override + public void onUnexpectedError(Exception e) { + Toast.makeText(context, appendPrefix(null), Toast.LENGTH_LONG).show(); + } + + String appendPrefix(String text) { + return (text == null) ? msgPrefix : msgPrefix + ": " + text; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/AccountDataRestClient.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/AccountDataRestClient.java new file mode 100644 index 0000000000..ea7cfce3b1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/AccountDataRestClient.java @@ -0,0 +1,92 @@ +/* + * 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.rest.client; + +import im.vector.matrix.android.internal.legacy.HomeServerConnectionConfig; +import im.vector.matrix.android.internal.legacy.RestClient; +import im.vector.matrix.android.internal.legacy.rest.api.AccountDataApi; +import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback; +import im.vector.matrix.android.internal.legacy.rest.callback.RestAdapterCallback; + +import java.util.HashMap; +import java.util.Map; + +public class AccountDataRestClient extends RestClient { + /** + * Account data types + */ + public static final String ACCOUNT_DATA_TYPE_IGNORED_USER_LIST = "m.ignored_user_list"; + public static final String ACCOUNT_DATA_TYPE_DIRECT_MESSAGES = "m.direct"; + public static final String ACCOUNT_DATA_TYPE_PREVIEW_URLS = "org.matrix.preview_urls"; + public static final String ACCOUNT_DATA_TYPE_WIDGETS = "m.widgets"; + + /** + * Account data keys + */ + public static final String ACCOUNT_DATA_KEY_IGNORED_USERS = "ignored_users"; + public static final String ACCOUNT_DATA_KEY_URL_PREVIEW_DISABLE = "disable"; + + /** + * {@inheritDoc} + */ + public AccountDataRestClient(HomeServerConnectionConfig hsConfig) { + super(hsConfig, AccountDataApi.class, RestClient.URI_API_PREFIX_PATH_R0, false); + } + + /** + * Set some account_data for the client. + * + * @param userId the user id + * @param type the account data type. + * @param params the put params. + * @param callback the asynchronous callback called when finished + */ + public void setAccountData(final String userId, final String type, final Object params, final ApiCallback callback) { + // privacy + //final String description = "setAccountData userId : " + userId + " type " + type + " params " + params; + final String description = "setAccountData userId : " + userId + " type " + type; + + mApi.setAccountData(userId, type, params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + setAccountData(userId, type, params, callback); + } + })); + } + + /** + * Gets a bearer token from the homeserver that the user can + * present to a third party in order to prove their ownership + * of the Matrix account they are logged into. + * + * @param userId the user id + * @param callback the asynchronous callback called when finished + */ + public void openIdToken(final String userId, final ApiCallback> callback) { + final String description = "openIdToken userId : " + userId; + + mApi.openIdToken(userId, new HashMap<>()) + .enqueue(new RestAdapterCallback>(description, mUnsentEventsManager, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + openIdToken(userId, callback); + } + })); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/CallRestClient.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/CallRestClient.java new file mode 100644 index 0000000000..a833675483 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/CallRestClient.java @@ -0,0 +1,38 @@ +/* + * 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.rest.client; + +import com.google.gson.JsonObject; + +import im.vector.matrix.android.internal.legacy.HomeServerConnectionConfig; +import im.vector.matrix.android.internal.legacy.RestClient; +import im.vector.matrix.android.internal.legacy.rest.api.CallRulesApi; +import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback; +import im.vector.matrix.android.internal.legacy.rest.callback.DefaultRetrofit2CallbackWrapper; + +public class CallRestClient extends RestClient { + + /** + * {@inheritDoc} + */ + public CallRestClient(HomeServerConnectionConfig hsConfig) { + super(hsConfig, CallRulesApi.class, RestClient.URI_API_PREFIX_PATH_R0, false); + } + + public void getTurnServer(final ApiCallback callback) { + mApi.getTurnServer().enqueue(new DefaultRetrofit2CallbackWrapper<>(callback)); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/CryptoRestClient.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/CryptoRestClient.java new file mode 100644 index 0000000000..026ddfdbd6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/CryptoRestClient.java @@ -0,0 +1,308 @@ +/* + * 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.rest.client; + +import android.text.TextUtils; + +import im.vector.matrix.android.internal.legacy.HomeServerConnectionConfig; +import im.vector.matrix.android.internal.legacy.RestClient; +import im.vector.matrix.android.internal.legacy.crypto.data.MXKey; +import im.vector.matrix.android.internal.legacy.crypto.data.MXUsersDevicesMap; +import im.vector.matrix.android.internal.legacy.rest.api.CryptoApi; +import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback; +import im.vector.matrix.android.internal.legacy.rest.callback.RestAdapterCallback; +import im.vector.matrix.android.internal.legacy.rest.model.crypto.KeyChangesResponse; +import im.vector.matrix.android.internal.legacy.rest.model.crypto.KeysClaimResponse; +import im.vector.matrix.android.internal.legacy.rest.model.crypto.KeysQueryResponse; +import im.vector.matrix.android.internal.legacy.rest.model.crypto.KeysUploadResponse; +import im.vector.matrix.android.internal.legacy.rest.model.pid.DeleteDeviceParams; +import im.vector.matrix.android.internal.legacy.rest.model.sync.DevicesListResponse; +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.List; +import java.util.Map; +import java.util.Random; + +import retrofit2.Response; + +public class CryptoRestClient extends RestClient { + + private static final String LOG_TAG = CryptoRestClient.class.getSimpleName(); + + /** + * {@inheritDoc} + */ + public CryptoRestClient(HomeServerConnectionConfig hsConfig) { + super(hsConfig, CryptoApi.class, URI_API_PREFIX_PATH_UNSTABLE, false, false); + } + + /** + * Upload device and/or one-time keys. + * + * @param deviceKeys the device keys to send. + * @param oneTimeKeys the one-time keys to send. + * @param deviceId he explicit device_id to use for upload (default is to use the same as that used during auth). + * @param callback the asynchronous callback + */ + public void uploadKeys(final Map deviceKeys, + final Map oneTimeKeys, + final String deviceId, + final ApiCallback callback) { + final String description = "uploadKeys"; + + String encodedDeviceId = JsonUtils.convertToUTF8(deviceId); + Map params = new HashMap<>(); + + if (null != deviceKeys) { + params.put("device_keys", deviceKeys); + } + + if (null != oneTimeKeys) { + params.put("one_time_keys", oneTimeKeys); + } + + if (!TextUtils.isEmpty(encodedDeviceId)) { + mApi.uploadKeys(encodedDeviceId, params) + .enqueue(new RestAdapterCallback(description, null, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + uploadKeys(deviceKeys, oneTimeKeys, deviceId, callback); + } + })); + } else { + mApi.uploadKeys(params) + .enqueue(new RestAdapterCallback(description, null, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + uploadKeys(deviceKeys, oneTimeKeys, deviceId, callback); + } + })); + } + } + + /** + * Download device keys. + * + * @param userIds list of users to get keys for. + * @param token the up-to token + * @param callback the asynchronous callback + */ + public void downloadKeysForUsers(final List userIds, final String token, final ApiCallback callback) { + final String description = "downloadKeysForUsers"; + + Map> downloadQuery = new HashMap<>(); + + if (null != userIds) { + for (String userId : userIds) { + downloadQuery.put(userId, new HashMap()); + } + } + + Map parameters = new HashMap<>(); + parameters.put("device_keys", downloadQuery); + + if (!TextUtils.isEmpty(token)) { + parameters.put("token", token); + } + + mApi.downloadKeysForUsers(parameters) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + downloadKeysForUsers(userIds, token, callback); + } + })); + } + + /** + * Claim one-time keys. + * + * @param usersDevicesKeyTypesMap a list of users, devices and key types to retrieve keys for. + * @param callback the asynchronous callback + */ + public void claimOneTimeKeysForUsersDevices( + final MXUsersDevicesMap usersDevicesKeyTypesMap, + final ApiCallback> callback) { + final String description = "claimOneTimeKeysForUsersDevices"; + + Map params = new HashMap<>(); + params.put("one_time_keys", usersDevicesKeyTypesMap.getMap()); + + mApi.claimOneTimeKeysForUsersDevices(params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + claimOneTimeKeysForUsersDevices(usersDevicesKeyTypesMap, callback); + } + }) { + @Override + public void success(KeysClaimResponse keysClaimResponse, Response response) { + onEventSent(); + + Map> map = new HashMap<>(); + + if (null != keysClaimResponse.oneTimeKeys) { + for (String userId : keysClaimResponse.oneTimeKeys.keySet()) { + Map>> mapByUserId = keysClaimResponse.oneTimeKeys.get(userId); + + Map keysMap = new HashMap<>(); + + for (String deviceId : mapByUserId.keySet()) { + try { + keysMap.put(deviceId, new MXKey(mapByUserId.get(deviceId))); + } catch (Exception e) { + Log.e(LOG_TAG, "## claimOneTimeKeysForUsersDevices : fail to create a MXKey " + e.getMessage(), e); + } + } + + if (keysMap.size() != 0) { + map.put(userId, keysMap); + } + } + } + + callback.onSuccess(new MXUsersDevicesMap<>(map)); + } + }); + } + + /** + * Send an event to a specific list of devices + * + * @param eventType the type of event to send + * @param contentMap content to send. Map from user_id to device_id to content dictionary. + * @param callback the asynchronous callback. + */ + public void sendToDevice(final String eventType, + final MXUsersDevicesMap> contentMap, final ApiCallback callback) { + sendToDevice(eventType, contentMap, (new Random()).nextInt(Integer.MAX_VALUE) + "", callback); + } + + /** + * Send an event to a specific list of devices + * + * @param eventType the type of event to send + * @param contentMap content to send. Map from user_id to device_id to content dictionary. + * @param transactionId the transactionId + * @param callback the asynchronous callback. + */ + public void sendToDevice(final String eventType, + final MXUsersDevicesMap> contentMap, final String transactionId, + final ApiCallback callback) { + final String description = "sendToDevice " + eventType; + + Map content = new HashMap<>(); + content.put("messages", contentMap.getMap()); + + mApi.sendToDevice(eventType, transactionId, content) + .enqueue(new RestAdapterCallback(description, null, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + sendToDevice(eventType, contentMap, callback); + } + })); + } + + /** + * Retrieves the devices informaty + * + * @param callback the asynchronous callback. + */ + public void getDevices(final ApiCallback callback) { + final String description = "getDevicesListInfo"; + + mApi.getDevices() + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getDevices(callback); + } + })); + } + + /** + * Delete a device. + * + * @param deviceId the device id + * @param params the deletion parameters + * @param callback the asynchronous callback + */ + public void deleteDevice(final String deviceId, final DeleteDeviceParams params, + final ApiCallback callback) { + final String description = "deleteDevice"; + + mApi.deleteDevice(deviceId, params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + deleteDevice(deviceId, params, callback); + } + })); + } + + /** + * Set a device name. + * + * @param deviceId the device id + * @param deviceName the device name + * @param callback the asynchronous callback + */ + public void setDeviceName(final String deviceId, final String deviceName, + final ApiCallback callback) { + final String description = "setDeviceName"; + + Map params = new HashMap<>(); + params.put("display_name", TextUtils.isEmpty(deviceName) ? "" : deviceName); + + mApi.updateDeviceInfo(deviceId, params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + setDeviceName(deviceId, deviceName, callback); + } + })); + } + + /** + * Get the update devices list from two sync token. + * + * @param from the start token. + * @param to the up-to token. + * @param callback the asynchronous callback + */ + public void getKeyChanges(final String from, final String to, + final ApiCallback callback) { + final String description = "getKeyChanges"; + + mApi.getKeyChanges(from, to) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getKeyChanges(from, to, callback); + } + })); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/EventsRestClient.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/EventsRestClient.java new file mode 100644 index 0000000000..987572dad8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/EventsRestClient.java @@ -0,0 +1,603 @@ +/* + * 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.rest.client; + +import android.text.TextUtils; + +import im.vector.matrix.android.internal.legacy.HomeServerConnectionConfig; +import im.vector.matrix.android.internal.legacy.RestClient; +import im.vector.matrix.android.internal.legacy.rest.api.EventsApi; +import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback; +import im.vector.matrix.android.internal.legacy.rest.callback.RestAdapterCallback; +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.URLPreview; +import im.vector.matrix.android.internal.legacy.rest.model.User; +import im.vector.matrix.android.internal.legacy.rest.model.pid.ThirdPartyProtocol; +import im.vector.matrix.android.internal.legacy.rest.model.publicroom.PublicRoomsFilter; +import im.vector.matrix.android.internal.legacy.rest.model.publicroom.PublicRoomsParams; +import im.vector.matrix.android.internal.legacy.rest.model.publicroom.PublicRoomsResponse; +import im.vector.matrix.android.internal.legacy.rest.model.search.SearchParams; +import im.vector.matrix.android.internal.legacy.rest.model.search.SearchResponse; +import im.vector.matrix.android.internal.legacy.rest.model.search.SearchRoomEventCategoryParams; +import im.vector.matrix.android.internal.legacy.rest.model.search.SearchUsersParams; +import im.vector.matrix.android.internal.legacy.rest.model.search.SearchUsersRequestResponse; +import im.vector.matrix.android.internal.legacy.rest.model.search.SearchUsersResponse; +import im.vector.matrix.android.internal.legacy.rest.model.sync.SyncResponse; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Class used to make requests to the events API. + */ +public class EventsRestClient extends RestClient { + + private static final int EVENT_STREAM_TIMEOUT_MS = 30000; + + private String mSearchEventsPatternIdentifier = null; + private String mSearchEventsMediaNameIdentifier = null; + private String mSearchUsersPatternIdentifier = null; + + /** + * {@inheritDoc} + */ + public EventsRestClient(HomeServerConnectionConfig hsConfig) { + super(hsConfig, EventsApi.class, "", false); + } + + protected EventsRestClient(EventsApi api) { + mApi = api; + } + + /** + * Retrieves the third party server protocols + * + * @param callback the asynchronous callback + */ + public void getThirdPartyServerProtocols(final ApiCallback> callback) { + final String description = "getThirdPartyServerProtocols"; + + mApi.thirdPartyProtocols() + .enqueue(new RestAdapterCallback>(description, mUnsentEventsManager, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getThirdPartyServerProtocols(callback); + } + })); + } + + /** + * Get the public rooms count. + * The count can be null. + * + * @param callback the public rooms count callbacks + */ + public void getPublicRoomsCount(final ApiCallback callback) { + getPublicRoomsCount(null, null, false, callback); + } + + /** + * Get the public rooms count. + * The count can be null. + * + * @param server the server url + * @param callback the asynchronous callback + */ + public void getPublicRoomsCount(final String server, final ApiCallback callback) { + getPublicRoomsCount(server, null, false, callback); + } + + /** + * Get the public rooms count. + * The count can be null. + * + * @param server the server url + * @param thirdPartyInstanceId the third party instance id (optional) + * @param includeAllNetworks true to search in all the connected network + * @param callback the asynchronous callback + */ + public void getPublicRoomsCount(final String server, + final String thirdPartyInstanceId, + final boolean includeAllNetworks, + final ApiCallback callback) { + loadPublicRooms(server, thirdPartyInstanceId, includeAllNetworks, null, null, 0, new SimpleApiCallback(callback) { + @Override + public void onSuccess(PublicRoomsResponse publicRoomsResponse) { + callback.onSuccess(publicRoomsResponse.total_room_count_estimate); + } + }); + } + + /** + * Get the list of the public rooms. + * + * @param server search on this home server only (null for any one) + * @param thirdPartyInstanceId the third party instance id (optional) + * @param includeAllNetworks true to search in all the connected network + * @param pattern the pattern to search + * @param since the pagination token + * @param limit the maximum number of public rooms + * @param callback the public rooms callbacks + */ + public void loadPublicRooms(final String server, + final String thirdPartyInstanceId, + final boolean includeAllNetworks, + final String pattern, + final String since, + final int limit, + final ApiCallback callback) { + final String description = "loadPublicRooms"; + + PublicRoomsParams publicRoomsParams = new PublicRoomsParams(); + + publicRoomsParams.thirdPartyInstanceId = thirdPartyInstanceId; + publicRoomsParams.includeAllNetworks = includeAllNetworks; + publicRoomsParams.limit = Math.max(0, limit); + publicRoomsParams.since = since; + + if (!TextUtils.isEmpty(pattern)) { + publicRoomsParams.filter = new PublicRoomsFilter(); + publicRoomsParams.filter.generic_search_term = pattern; + } + + mApi.publicRooms(server, publicRoomsParams) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + loadPublicRooms(server, thirdPartyInstanceId, includeAllNetworks, pattern, since, limit, callback); + } + })); + } + + + /** + * Synchronise the client's state and receive new messages. Based on server sync C-S v2 API. + *

+ * Synchronise the client's state with the latest state on the server. + * Client's use this API when they first log in to get an initial snapshot + * of the state on the server, and then continue to call this API to get + * incremental deltas to the state, and to receive new messages. + * + * @param token the token to stream from (nil in case of initial sync). + * @param serverTimeout the maximum time in ms to wait for an event. + * @param clientTimeout the maximum time in ms the SDK must wait for the server response. + * @param setPresence the optional parameter which controls whether the client is automatically + * marked as online by polling this API. If this parameter is omitted then the client is + * automatically marked as online when it uses this API. Otherwise if + * the parameter is set to "offline" then the client is not marked as + * being online when it uses this API. + * @param filterOrFilterId a JSON filter or the ID of a filter created using the filter API (optional). + * @param callback The request callback + */ + public void syncFromToken(final String token, + final int serverTimeout, + final int clientTimeout, + final String setPresence, + final String filterOrFilterId, + final ApiCallback callback) { + Map params = new HashMap<>(); + int timeout = (EVENT_STREAM_TIMEOUT_MS / 1000); + + if (!TextUtils.isEmpty(token)) { + params.put("since", token); + } + + if (-1 != serverTimeout) { + timeout = serverTimeout; + } + + if (!TextUtils.isEmpty(setPresence)) { + params.put("set_presence", setPresence); + } + + if (!TextUtils.isEmpty(filterOrFilterId)) { + params.put("filter", filterOrFilterId); + } + + params.put("timeout", timeout); + + // increase the timeout because the init sync might require more time to be built + setConnectionTimeout(RestClient.CONNECTION_TIMEOUT_MS * ((null == token) ? 2 : 1)); + + final String description = "syncFromToken"; + // Disable retry because it interferes with clientTimeout + // Let the client manage retries on events streams + mApi.sync(params) + .enqueue(new RestAdapterCallback(description, null, false, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + syncFromToken(token, serverTimeout, clientTimeout, setPresence, filterOrFilterId, callback); + } + })); + } + + /** + * Retrieve an event from its event id. + * + * @param eventId the event id + * @param callback the asynchronous callback. + */ + public void getEventFromEventId(final String eventId, final ApiCallback callback) { + final String description = "getEventFromEventId : eventId " + eventId; + + mApi.getEvent(eventId) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getEventFromEventId(eventId, callback); + } + })); + } + + /** + * Search a text in room messages. + * + * @param text the text to search for. + * @param rooms a list of rooms to search in. nil means all rooms the user is in. + * @param beforeLimit the number of events to get before the matching results. + * @param afterLimit the number of events to get after the matching results. + * @param nextBatch the token to pass for doing pagination from a previous response. + * @param callback the request callback + */ + public void searchMessagesByText(final String text, + final List rooms, + final int beforeLimit, + final int afterLimit, + final String nextBatch, + final ApiCallback callback) { + SearchParams searchParams = new SearchParams(); + SearchRoomEventCategoryParams searchEventParams = new SearchRoomEventCategoryParams(); + + searchEventParams.search_term = text; + searchEventParams.order_by = "recent"; + + searchEventParams.event_context = new HashMap<>(); + searchEventParams.event_context.put("before_limit", beforeLimit); + searchEventParams.event_context.put("after_limit", afterLimit); + searchEventParams.event_context.put("include_profile", true); + + if (null != rooms) { + searchEventParams.filter = new HashMap<>(); + searchEventParams.filter.put("rooms", rooms); + } + + searchParams.search_categories = new HashMap<>(); + searchParams.search_categories.put("room_events", searchEventParams); + + final String description = "searchMessageText"; + + final String uid = System.currentTimeMillis() + ""; + mSearchEventsPatternIdentifier = uid + text; + + // don't retry to send the request + // if the search fails, stop it + mApi.searchEvents(searchParams, nextBatch) + .enqueue(new RestAdapterCallback(description, null, new ApiCallback() { + /** + * Tells if the current response for the latest request. + * + * @return true if it is the response of the latest request. + */ + private boolean isActiveRequest() { + return TextUtils.equals(mSearchEventsPatternIdentifier, uid + text); + } + + @Override + public void onSuccess(SearchResponse response) { + if (isActiveRequest()) { + if (null != callback) { + callback.onSuccess(response); + } + + mSearchEventsPatternIdentifier = null; + } + } + + @Override + public void onNetworkError(Exception e) { + if (isActiveRequest()) { + if (null != callback) { + callback.onNetworkError(e); + } + + mSearchEventsPatternIdentifier = null; + } + } + + @Override + public void onMatrixError(MatrixError e) { + if (isActiveRequest()) { + if (null != callback) { + callback.onMatrixError(e); + } + + mSearchEventsPatternIdentifier = null; + } + } + + @Override + public void onUnexpectedError(Exception e) { + if (isActiveRequest()) { + if (null != callback) { + callback.onUnexpectedError(e); + } + + mSearchEventsPatternIdentifier = null; + } + } + + }, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + searchMessagesByText(text, rooms, beforeLimit, afterLimit, nextBatch, callback); + } + })); + } + + /** + * Search a media from its name. + * + * @param name the text to search for. + * @param rooms a list of rooms to search in. nil means all rooms the user is in. + * @param beforeLimit the number of events to get before the matching results. + * @param afterLimit the number of events to get after the matching results. + * @param nextBatch the token to pass for doing pagination from a previous response. + * @param callback the request callback + */ + public void searchMediasByText(final String name, + final List rooms, + final int beforeLimit, + final int afterLimit, + final String nextBatch, + final ApiCallback callback) { + SearchParams searchParams = new SearchParams(); + SearchRoomEventCategoryParams searchEventParams = new SearchRoomEventCategoryParams(); + + searchEventParams.search_term = name; + searchEventParams.order_by = "recent"; + + searchEventParams.event_context = new HashMap<>(); + searchEventParams.event_context.put("before_limit", beforeLimit); + searchEventParams.event_context.put("after_limit", afterLimit); + searchEventParams.event_context.put("include_profile", true); + + searchEventParams.filter = new HashMap<>(); + + if (null != rooms) { + searchEventParams.filter.put("rooms", rooms); + } + + List types = new ArrayList<>(); + types.add(Event.EVENT_TYPE_MESSAGE); + searchEventParams.filter.put("types", types); + + searchEventParams.filter.put("contains_url", true); + + searchParams.search_categories = new HashMap<>(); + searchParams.search_categories.put("room_events", searchEventParams); + + // other unused filter items + // not_types + // not_rooms + // senders + // not_senders + + final String uid = System.currentTimeMillis() + ""; + mSearchEventsMediaNameIdentifier = uid + name; + + final String description = "searchMediasByText"; + + // don't retry to send the request + // if the search fails, stop it + mApi.searchEvents(searchParams, nextBatch) + .enqueue(new RestAdapterCallback(description, null, new ApiCallback() { + /** + * Tells if the current response for the latest request. + * + * @return true if it is the response of the latest request. + */ + private boolean isActiveRequest() { + return TextUtils.equals(mSearchEventsMediaNameIdentifier, uid + name); + } + + @Override + public void onSuccess(SearchResponse newSearchResponse) { + if (isActiveRequest()) { + callback.onSuccess(newSearchResponse); + mSearchEventsMediaNameIdentifier = null; + } + } + + @Override + public void onNetworkError(Exception e) { + if (isActiveRequest()) { + callback.onNetworkError(e); + mSearchEventsMediaNameIdentifier = null; + } + } + + @Override + public void onMatrixError(MatrixError e) { + if (isActiveRequest()) { + callback.onMatrixError(e); + mSearchEventsMediaNameIdentifier = null; + } + } + + @Override + public void onUnexpectedError(Exception e) { + if (isActiveRequest()) { + callback.onUnexpectedError(e); + mSearchEventsMediaNameIdentifier = null; + } + } + + }, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + searchMediasByText(name, rooms, beforeLimit, afterLimit, nextBatch, callback); + } + })); + } + + + /** + * Search users with a patter, + * + * @param text the text to search for. + * @param limit the maximum nbr of users in the response + * @param userIdsFilter the userIds to exclude from the result + * @param callback the request callback + */ + public void searchUsers(final String text, final Integer limit, final Set userIdsFilter, final ApiCallback callback) { + SearchUsersParams searchParams = new SearchUsersParams(); + + searchParams.search_term = text; + searchParams.limit = limit + ((null != userIdsFilter) ? userIdsFilter.size() : 0); + + final String uid = mSearchUsersPatternIdentifier = System.currentTimeMillis() + " " + text + " " + limit; + final String description = "searchUsers"; + + // don't retry to send the request + // if the search fails, stop it + mApi.searchUsers(searchParams) + .enqueue(new RestAdapterCallback(description, null, + new ApiCallback() { + /** + * Tells if the current response for the latest request. + * + * @return true if it is the response of the latest request. + */ + private boolean isActiveRequest() { + return TextUtils.equals(mSearchUsersPatternIdentifier, uid); + } + + @Override + public void onSuccess(SearchUsersRequestResponse aResponse) { + if (isActiveRequest()) { + SearchUsersResponse response = new SearchUsersResponse(); + response.limited = aResponse.limited; + response.results = new ArrayList<>(); + Set filter = (null != userIdsFilter) ? userIdsFilter : new HashSet(); + + if (null != aResponse.results) { + for (SearchUsersRequestResponse.User user : aResponse.results) { + if ((null != user.user_id) && !filter.contains(user.user_id)) { + User addedUser = new User(); + addedUser.user_id = user.user_id; + addedUser.avatar_url = user.avatar_url; + addedUser.displayname = user.display_name; + response.results.add(addedUser); + } + } + } + + callback.onSuccess(response); + mSearchUsersPatternIdentifier = null; + } + } + + @Override + public void onNetworkError(Exception e) { + if (isActiveRequest()) { + callback.onNetworkError(e); + mSearchUsersPatternIdentifier = null; + } + } + + @Override + public void onMatrixError(MatrixError e) { + if (isActiveRequest()) { + callback.onMatrixError(e); + mSearchUsersPatternIdentifier = null; + } + } + + @Override + public void onUnexpectedError(Exception e) { + if (isActiveRequest()) { + callback.onUnexpectedError(e); + mSearchUsersPatternIdentifier = null; + } + } + + }, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + searchUsers(text, limit, userIdsFilter, callback); + } + })); + } + + /** + * Cancel any pending file search request + */ + public void cancelSearchMediasByText() { + mSearchEventsMediaNameIdentifier = null; + } + + /** + * Cancel any pending search request + */ + public void cancelSearchMessagesByText() { + mSearchEventsPatternIdentifier = null; + } + + /** + * Cancel any pending search request + */ + public void cancelUsersSearch() { + mSearchUsersPatternIdentifier = null; + } + + /** + * Retrieve the URL preview information. + * + * @param url the URL + * @param ts the timestamp + * @param callback the asynchronous callback + */ + public void getURLPreview(final String url, final long ts, final ApiCallback callback) { + final String description = "getURLPreview : URL " + url + " with ts " + ts; + + mApi.getURLPreview(url, ts) + .enqueue(new RestAdapterCallback>(description, null, false, + new SimpleApiCallback>(callback) { + @Override + public void onSuccess(Map map) { + if (null != callback) { + callback.onSuccess(new URLPreview(map, url)); + } + } + }, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getURLPreview(url, ts, callback); + } + })); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/FilterRestClient.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/FilterRestClient.java new file mode 100644 index 0000000000..fb506562cb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/FilterRestClient.java @@ -0,0 +1,73 @@ +/* + * Copyright 2018 Matthias Kesler + * 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.rest.client; + +import im.vector.matrix.android.internal.legacy.HomeServerConnectionConfig; +import im.vector.matrix.android.internal.legacy.RestClient; +import im.vector.matrix.android.internal.legacy.rest.api.FilterApi; +import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback; +import im.vector.matrix.android.internal.legacy.rest.callback.RestAdapterCallback; +import im.vector.matrix.android.internal.legacy.rest.model.filter.FilterBody; +import im.vector.matrix.android.internal.legacy.rest.model.filter.FilterResponse; + +public class FilterRestClient extends RestClient{ + + /** + * {@inheritDoc} + */ + public FilterRestClient(HomeServerConnectionConfig hsConfig) { + super(hsConfig, FilterApi.class, RestClient.URI_API_PREFIX_PATH_R0, false); + } + + /** + * Uploads a FilterBody to homeserver + * + * @param userId the user id + * @param filterBody FilterBody which should be send to server + * @param callback on success callback containing a String with populated filterId + */ + public void uploadFilter(final String userId, final FilterBody filterBody, final ApiCallback callback) { + final String description = "uploadFilter userId : " + userId + " filter : " + filterBody; + + mApi.uploadFilter(userId, filterBody) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + uploadFilter(userId, filterBody, callback); + } + })); + } + + /** + * Get a user's filter by filterId + * + * @param userId the user id + * @param filterId the filter id + * @param callback on success callback containing a User object with populated filterbody + */ + public void getFilter(final String userId, final String filterId, final ApiCallback callback) { + final String description = "getFilter userId : " + userId + " filterId : " + filterId; + + mApi.getFilterById(userId, filterId) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getFilter(userId, filterId, callback); + } + })); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/GroupsRestClient.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/GroupsRestClient.java new file mode 100644 index 0000000000..8471adb41b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/GroupsRestClient.java @@ -0,0 +1,436 @@ +/* + * 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.rest.client; + +import im.vector.matrix.android.internal.legacy.HomeServerConnectionConfig; +import im.vector.matrix.android.internal.legacy.RestClient; +import im.vector.matrix.android.internal.legacy.rest.api.GroupsApi; +import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback; +import im.vector.matrix.android.internal.legacy.rest.callback.RestAdapterCallback; +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.group.AcceptGroupInvitationParams; +import im.vector.matrix.android.internal.legacy.rest.model.group.AddGroupParams; +import im.vector.matrix.android.internal.legacy.rest.model.group.CreateGroupParams; +import im.vector.matrix.android.internal.legacy.rest.model.group.CreateGroupResponse; +import im.vector.matrix.android.internal.legacy.rest.model.group.GetGroupsResponse; +import im.vector.matrix.android.internal.legacy.rest.model.group.GetPublicisedGroupsResponse; +import im.vector.matrix.android.internal.legacy.rest.model.group.GroupInviteUserParams; +import im.vector.matrix.android.internal.legacy.rest.model.group.GroupInviteUserResponse; +import im.vector.matrix.android.internal.legacy.rest.model.group.GroupKickUserParams; +import im.vector.matrix.android.internal.legacy.rest.model.group.GroupProfile; +import im.vector.matrix.android.internal.legacy.rest.model.group.GroupRooms; +import im.vector.matrix.android.internal.legacy.rest.model.group.GroupSummary; +import im.vector.matrix.android.internal.legacy.rest.model.group.GroupUsers; +import im.vector.matrix.android.internal.legacy.rest.model.group.LeaveGroupParams; +import im.vector.matrix.android.internal.legacy.rest.model.group.UpdatePubliciseParams; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import retrofit2.Response; + +/** + * Class used to make requests to the groups API. + */ +public class GroupsRestClient extends RestClient { + + /** + * {@inheritDoc} + */ + public GroupsRestClient(HomeServerConnectionConfig hsConfig) { + super(hsConfig, GroupsApi.class, RestClient.URI_API_PREFIX_PATH_R0, false); + } + + protected GroupsRestClient(GroupsApi api) { + mApi = api; + } + + /** + * Create a group. + * + * @param params the room creation parameters + * @param callback the asynchronous callback. + */ + public void createGroup(final CreateGroupParams params, final ApiCallback callback) { + final String description = "createGroup " + params.localpart; + + mApi.createGroup(params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + createGroup(params, callback); + } + }) { + @Override + public void success(CreateGroupResponse createGroupResponse, Response response) { + onEventSent(); + callback.onSuccess(createGroupResponse.group_id); + } + }); + } + + /** + * Invite an user in a group. + * + * @param groupId the group id + * @param userId the user id + * @param callback the asynchronous callback. + */ + public void inviteUserInGroup(final String groupId, final String userId, final ApiCallback callback) { + final String description = "inviteUserInGroup " + groupId + " - " + userId; + + GroupInviteUserParams params = new GroupInviteUserParams(); + + mApi.inviteUser(groupId, userId, params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + inviteUserInGroup(groupId, userId, callback); + } + }) { + @Override + public void success(GroupInviteUserResponse groupInviteUserResponse, Response response) { + onEventSent(); + callback.onSuccess(groupInviteUserResponse.state); + } + }); + } + + /** + * Kick an user from a group. + * + * @param groupId the group id + * @param userId the user id + * @param callback the asynchronous callback. + */ + public void KickUserFromGroup(final String groupId, final String userId, final ApiCallback callback) { + final String description = "KickFromGroup " + groupId + " " + userId; + + GroupKickUserParams params = new GroupKickUserParams(); + + mApi.kickUser(groupId, userId, params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + KickUserFromGroup(groupId, userId, callback); + } + })); + } + + /** + * Add a room in a group. + * + * @param groupId the group id + * @param roomId the room id + * @param callback the asynchronous callback. + */ + public void addRoomInGroup(final String groupId, final String roomId, final ApiCallback callback) { + final String description = "addRoomInGroup " + groupId + " " + roomId; + + AddGroupParams params = new AddGroupParams(); + + mApi.addRoom(groupId, roomId, params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + addRoomInGroup(groupId, roomId, callback); + } + })); + } + + /** + * Remove a room from a group. + * + * @param groupId the group id + * @param roomId the room id + * @param callback the asynchronous callback. + */ + public void removeRoomFromGroup(final String groupId, final String roomId, final ApiCallback callback) { + final String description = "removeRoomFromGroup " + groupId + " " + roomId; + + mApi.removeRoom(groupId, roomId) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + removeRoomFromGroup(groupId, roomId, callback); + } + })); + } + + /** + * Update a group profile. + * + * @param groupId the group id + * @param profile the profile + * @param callback the asynchronous callback. + */ + public void updateGroupProfile(final String groupId, final GroupProfile profile, final ApiCallback callback) { + final String description = "updateGroupProfile " + groupId; + + mApi.updateProfile(groupId, profile) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + updateGroupProfile(groupId, profile, callback); + } + })); + } + + /** + * Update a group profile. + * + * @param groupId the group id + * @param callback the asynchronous callback. + */ + public void getGroupProfile(final String groupId, final ApiCallback callback) { + final String description = "getGroupProfile " + groupId; + + mApi.getProfile(groupId) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getGroupProfile(groupId, callback); + } + })); + } + + /** + * Request the group invited users. + * + * @param groupId the group id + * @param callback the asynchronous callback. + */ + public void getGroupInvitedUsers(final String groupId, final ApiCallback callback) { + final String description = "getGroupInvitedUsers " + groupId; + + mApi.getInvitedUsers(groupId) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getGroupInvitedUsers(groupId, callback); + } + })); + } + + /** + * Request the group rooms. + * + * @param groupId the group id + * @param callback the asynchronous callback. + */ + public void getGroupRooms(final String groupId, final ApiCallback callback) { + final String description = "getGroupRooms " + groupId; + + mApi.getRooms(groupId) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getGroupRooms(groupId, callback); + } + })); + } + + /** + * Request the group users. + * + * @param groupId the group id + * @param callback the asynchronous callback. + */ + public void getGroupUsers(final String groupId, final ApiCallback callback) { + final String description = "getGroupUsers " + groupId; + + mApi.getUsers(groupId) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getGroupUsers(groupId, callback); + } + })); + } + + /** + * Request a group summary + * + * @param groupId the group id + * @param callback the asynchronous callback. + */ + public void getGroupSummary(final String groupId, final ApiCallback callback) { + final String description = "getGroupSummary " + groupId; + + mApi.getSummary(groupId) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getGroupSummary(groupId, callback); + } + })); + } + + /** + * Join a group. + * + * @param groupId the group id + * @param callback the asynchronous callback. + */ + public void joinGroup(final String groupId, final ApiCallback callback) { + final String description = "joinGroup " + groupId; + + AcceptGroupInvitationParams params = new AcceptGroupInvitationParams(); + + mApi.acceptInvitation(groupId, params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + joinGroup(groupId, callback); + } + })); + } + + /** + * Leave a group. + * + * @param groupId the group id + * @param callback the asynchronous callback. + */ + public void leaveGroup(final String groupId, final ApiCallback callback) { + final String description = "leaveGroup " + groupId; + + LeaveGroupParams params = new LeaveGroupParams(); + + mApi.leave(groupId, params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + leaveGroup(groupId, callback); + } + })); + } + + /** + * Update a group publicity status. + * + * @param groupId the group id + * @param publicity the new publicity status + * @param callback the asynchronous callback. + */ + public void updateGroupPublicity(final String groupId, final boolean publicity, final ApiCallback callback) { + final String description = "updateGroupPublicity " + groupId + " - " + publicity; + + UpdatePubliciseParams params = new UpdatePubliciseParams(); + params.publicise = publicity; + + mApi.updatePublicity(groupId, params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + updateGroupPublicity(groupId, publicity, callback); + } + })); + } + + /** + * Request the joined groups. + * + * @param callback the asynchronous callback. + */ + public void getJoinedGroups(final ApiCallback> callback) { + final String description = "getJoinedGroups"; + + mApi.getJoinedGroupIds() + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getJoinedGroups(callback); + } + }) { + @Override + public void success(GetGroupsResponse getGroupsResponse, Response response) { + onEventSent(); + callback.onSuccess(getGroupsResponse.groupIds); + } + }); + } + + /** + * Request the publicised groups for an user. + * + * @param userId the user id + * @param callback the asynchronous callback. + */ + public void getUserPublicisedGroups(final String userId, final ApiCallback> callback) { + getPublicisedGroups(Arrays.asList(userId), new SimpleApiCallback>>(callback) { + @Override + public void onSuccess(Map> map) { + callback.onSuccess(map.get(userId)); + } + }); + } + + /** + * Request the publicised groups for an users list. + * + * @param userIds the user ids list + * @param callback the asynchronous callback + */ + public void getPublicisedGroups(final List userIds, final ApiCallback>> callback) { + final String description = "getPublicisedGroups " + userIds; + + Map> params = new HashMap<>(); + params.put("user_ids", userIds); + + mApi.getPublicisedGroups(params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getPublicisedGroups(userIds, callback); + } + } + ) { + @Override + public void success(GetPublicisedGroupsResponse getPublicisedGroupsResponse, Response response) { + onEventSent(); + + Map> map = new HashMap<>(); + + for (String userId : userIds) { + List groupIds = null; + + if ((null != getPublicisedGroupsResponse.users) && getPublicisedGroupsResponse.users.containsKey(userId)) { + groupIds = getPublicisedGroupsResponse.users.get(userId); + } + + if (null == groupIds) { + groupIds = new ArrayList<>(); + } + + map.put(userId, groupIds); + } + + callback.onSuccess(map); + } + }); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/LoginRestClient.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/LoginRestClient.java new file mode 100644 index 0000000000..3671ed2dec --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/LoginRestClient.java @@ -0,0 +1,360 @@ +/* + * 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.rest.client; + +import android.os.Build; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import com.google.gson.JsonObject; + +import im.vector.matrix.android.internal.legacy.HomeServerConnectionConfig; +import im.vector.matrix.android.internal.legacy.RestClient; +import im.vector.matrix.android.internal.legacy.rest.api.LoginApi; +import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback; +import im.vector.matrix.android.internal.legacy.rest.callback.RestAdapterCallback; +import im.vector.matrix.android.internal.legacy.rest.model.Versions; +import im.vector.matrix.android.internal.legacy.rest.model.login.Credentials; +import im.vector.matrix.android.internal.legacy.rest.model.login.LoginFlow; +import im.vector.matrix.android.internal.legacy.rest.model.login.LoginFlowResponse; +import im.vector.matrix.android.internal.legacy.rest.model.login.LoginParams; +import im.vector.matrix.android.internal.legacy.rest.model.login.PasswordLoginParams; +import im.vector.matrix.android.internal.legacy.rest.model.login.RegistrationParams; +import im.vector.matrix.android.internal.legacy.rest.model.login.TokenLoginParams; + +import java.util.List; +import java.util.UUID; + +import retrofit2.Response; + +/** + * Class used to make requests to the login API. + */ +public class LoginRestClient extends RestClient { + + public static final String LOGIN_FLOW_TYPE_PASSWORD = "m.login.password"; + public static final String LOGIN_FLOW_TYPE_OAUTH2 = "m.login.oauth2"; + public static final String LOGIN_FLOW_TYPE_EMAIL_CODE = "m.login.email.code"; + public static final String LOGIN_FLOW_TYPE_EMAIL_URL = "m.login.email.url"; + public static final String LOGIN_FLOW_TYPE_EMAIL_IDENTITY = "m.login.email.identity"; + public static final String LOGIN_FLOW_TYPE_MSISDN = "m.login.msisdn"; + public static final String LOGIN_FLOW_TYPE_RECAPTCHA = "m.login.recaptcha"; + public static final String LOGIN_FLOW_TYPE_DUMMY = "m.login.dummy"; + + /** + * Public constructor. + * + * @param hsConfig the home server connection config + */ + public LoginRestClient(HomeServerConnectionConfig hsConfig) { + super(hsConfig, LoginApi.class, "", false); + } + + /** + * Get Versions supported by the server and other server capabilities + * + * @param callback the callback + */ + public void getVersions(final ApiCallback callback) { + final String description = "getVersions"; + + mApi.versions() + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, null)); + } + + /** + * Retrieve the login supported flows. + * It should be done to check before displaying a default login form. + * + * @param callback the callback success and failure callback + */ + public void getSupportedLoginFlows(final ApiCallback> callback) { + final String description = "geLoginSupportedFlows"; + + mApi.login() + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getSupportedLoginFlows(callback); + } + }) { + @Override + public void success(LoginFlowResponse loginFlowResponse, Response response) { + onEventSent(); + callback.onSuccess(loginFlowResponse.flows); + } + }); + } + + /** + * Request an account creation + * + * @param params the registration parameters + * @param callback the callback + */ + public void register(final RegistrationParams params, final ApiCallback callback) { + final String description = "register"; + + // define a default device name only there is a password + if (!TextUtils.isEmpty(params.password) && TextUtils.isEmpty(params.initial_device_display_name)) { + params.initial_device_display_name = Build.MODEL.trim(); + + // Temporary flag to notify the server that we support msisdn flow. Used to prevent old app + // versions to end up in fallback because the HS returns the msisdn flow which they don't support + // Only send it if we send any params at all (the password param is + // mandatory, so if we send any params, we'll send the password param) + params.x_show_msisdn = true; + } else if (params.password == null && params.username == null && params.auth == null) { + // Happens when we call the method to get flows, also add flag in that case + params.x_show_msisdn = true; + } + + mApi.register(params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + register(params, callback); + } + }) { + + @Override + public void success(JsonObject jsonObject, Response response) { + onEventSent(); + mCredentials = gson.fromJson(jsonObject, Credentials.class); + callback.onSuccess(mCredentials); + } + }); + } + + /** + * Attempt to login with username/password + * + * @param user the username + * @param password the password + * @param callback the callback success and failure callback + */ + public void loginWithUser(final String user, final String password, final ApiCallback callback) { + loginWithUser(user, password, null, null, callback); + } + + /** + * Attempt to login with username/password + * + * @param user the username + * @param password the password + * @param deviceName the device name + * @param deviceId the device id, used for e2e encryption + * @param callback the callback success and failure callback + */ + public void loginWithUser(final String user, + final String password, + final String deviceName, + @Nullable final String deviceId, + final ApiCallback callback) { + final String description = "loginWithUser : " + user; + + PasswordLoginParams params = new PasswordLoginParams(); + params.setUserIdentifier(user, password); + params.setDeviceName(deviceName); + params.setDeviceId(deviceId); + + login(params, callback, description); + } + + /** + * Attempt to login with 3pid/password + * + * @param medium the medium of the 3pid + * @param address the address of the 3pid + * @param password the password + * @param callback the callback success and failure callback + */ + public void loginWith3Pid(final String medium, final String address, final String password, final ApiCallback callback) { + loginWith3Pid(medium, address, password, null, null, callback); + } + + /** + * Attempt to login with 3pid/password + * + * @param medium the medium of the 3pid + * @param address the address of the 3pid + * @param password the password + * @param deviceName the device name + * @param deviceId the device id, used for e2e encryption + * @param callback the callback success and failure callback + */ + public void loginWith3Pid(final String medium, + final String address, + final String password, + final String deviceName, + @Nullable final String deviceId, + final ApiCallback callback) { + final String description = "loginWith3pid : " + address; + + PasswordLoginParams params = new PasswordLoginParams(); + params.setThirdPartyIdentifier(medium, address, password); + params.setDeviceName(deviceName); + params.setDeviceId(deviceId); + + login(params, callback, description); + } + + /** + * Attempt to login with phone number/password + * + * @param phoneNumber the phone number + * @param countryCode the ISO country code + * @param password the password + * @param callback the callback success and failure callback + */ + public void loginWithPhoneNumber(final String phoneNumber, final String countryCode, final String password, final ApiCallback callback) { + loginWithPhoneNumber(phoneNumber, countryCode, password, null, null, callback); + } + + /** + * Attempt to login with phone number/password + * + * @param phoneNumber the phone number + * @param countryCode the ISO country code + * @param password the password + * @param deviceName the device name + * @param deviceId the device id, used for e2e encryption + * @param callback the callback success and failure callback + */ + public void loginWithPhoneNumber(final String phoneNumber, + final String countryCode, + final String password, + final String deviceName, + @Nullable final String deviceId, + final ApiCallback callback) { + final String description = "loginWithPhoneNumber : " + phoneNumber; + + PasswordLoginParams params = new PasswordLoginParams(); + params.setPhoneIdentifier(phoneNumber, countryCode, password); + params.setDeviceName(deviceName); + params.setDeviceId(deviceId); + + login(params, callback, description); + } + + /** + * Make a login request. + * + * @param params custom login params + * @param callback the asynchronous callback + */ + public void login(LoginParams params, final ApiCallback callback) { + login(params, callback, "login with a " + params.getClass().getSimpleName() + " object"); + } + + /** + * Make login request + * + * @param params login params + * @param callback the asynchronous callback + * @param description the request description + */ + private void login(final LoginParams params, final ApiCallback callback, final String description) { + mApi.login(params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + login(params, callback, description); + } + }) { + @Override + public void success(JsonObject jsonObject, Response response) { + onEventSent(); + mCredentials = gson.fromJson(jsonObject, Credentials.class); + callback.onSuccess(mCredentials); + } + }); + } + + /** + * Attempt a user/token log in. + * + * @param user the user name + * @param token the token + * @param deviceName the device name + * @param callback the callback success and failure callback + */ + public void loginWithToken(final String user, final String token, final String deviceName, final ApiCallback callback) { + loginWithToken(user, token, UUID.randomUUID().toString(), deviceName, callback); + } + + /** + * Attempt a user/token log in. + * + * @param user the user name + * @param token the token + * @param txn_id the client transaction id to include in the request + * @param deviceName the device name + * @param callback the callback success and failure callback + */ + public void loginWithToken(final String user, final String token, final String txn_id, String deviceName, final ApiCallback callback) { + // privacy + //final String description = "loginWithPassword user : " + user; + final String description = "loginWithPassword user"; + + TokenLoginParams params = new TokenLoginParams(); + params.user = user; + params.token = token; + params.txn_id = txn_id; + + if ((null != deviceName) && !TextUtils.isEmpty(deviceName.trim())) { + params.initial_device_display_name = deviceName.trim(); + } else { + params.initial_device_display_name = Build.MODEL.trim(); + } + + mApi.login(params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + loginWithToken(user, token, txn_id, callback); + } + }) { + @Override + public void success(JsonObject jsonObject, Response response) { + onEventSent(); + mCredentials = gson.fromJson(jsonObject, Credentials.class); + callback.onSuccess(mCredentials); + } + }); + } + + /** + * Invalidate the access token, so that it can no longer be used for authorization. + * + * @param callback the callback success and failure callback + */ + public void logout(final ApiCallback callback) { + // privacy + final String description = "logout user"; + + mApi.logout() + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + logout(callback); + } + })); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/MXRestExecutorService.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/MXRestExecutorService.java new file mode 100644 index 0000000000..fd9692b1c0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/MXRestExecutorService.java @@ -0,0 +1,76 @@ +/* + * 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.rest.client; + +import android.os.HandlerThread; +import android.support.annotation.NonNull; + +import im.vector.matrix.android.internal.legacy.util.MXOsHandler; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * MXRestExecutor is a basic thread executor + */ +public class MXRestExecutorService extends AbstractExecutorService { + private HandlerThread mHandlerThread; + private MXOsHandler mHandler; + + public MXRestExecutorService() { + mHandlerThread = new HandlerThread("MXRestExecutor" + hashCode(), Thread.MIN_PRIORITY); + mHandlerThread.start(); + mHandler = new MXOsHandler(mHandlerThread.getLooper()); + } + + @Override + public void execute(final Runnable r) { + mHandler.post(r); + } + + /** + * Stop any running thread + */ + public void stop() { + if (null != mHandlerThread) { + mHandlerThread.quit(); + } + } + + @Override public void shutdown() { + + } + + @NonNull @Override public List shutdownNow() { + return Collections.emptyList(); + } + + @Override public boolean isShutdown() { + return false; + } + + @Override public boolean isTerminated() { + return false; + } + + @Override + public boolean awaitTermination(long timeout, @NonNull TimeUnit unit) throws InterruptedException { + return false; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/MediaScanRestClient.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/MediaScanRestClient.java new file mode 100644 index 0000000000..34a20f105f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/MediaScanRestClient.java @@ -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.rest.client; + +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import im.vector.matrix.android.internal.legacy.HomeServerConnectionConfig; +import im.vector.matrix.android.internal.legacy.RestClient; +import im.vector.matrix.android.internal.legacy.data.store.IMXStore; +import im.vector.matrix.android.internal.legacy.rest.api.MediaScanApi; +import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback; +import im.vector.matrix.android.internal.legacy.rest.callback.DefaultRetrofit2CallbackWrapper; +import im.vector.matrix.android.internal.legacy.rest.callback.SimpleApiCallback; +import im.vector.matrix.android.internal.legacy.rest.model.EncryptedMediaScanBody; +import im.vector.matrix.android.internal.legacy.rest.model.EncryptedMediaScanEncryptedBody; +import im.vector.matrix.android.internal.legacy.rest.model.MatrixError; +import im.vector.matrix.android.internal.legacy.rest.model.MediaScanPublicKeyResult; +import im.vector.matrix.android.internal.legacy.rest.model.MediaScanResult; +import im.vector.matrix.android.internal.legacy.rest.model.crypto.EncryptedBodyFileInfo; +import im.vector.matrix.android.internal.legacy.util.JsonUtils; +import org.matrix.olm.OlmException; +import org.matrix.olm.OlmPkEncryption; +import org.matrix.olm.OlmPkMessage; + +import retrofit2.Call; + +/** + * Class used to make requests to the anti-virus scanner API. + */ +public class MediaScanRestClient extends RestClient { + + @Nullable + private IMXStore mMxStore; + + /** + * {@inheritDoc} + */ + public MediaScanRestClient(HomeServerConnectionConfig hsConfig) { + super(hsConfig, MediaScanApi.class, RestClient.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE, false, EndPointServer.ANTIVIRUS_SERVER); + } + + /** + * Set MxStore instance + * + * @param mxStore + */ + public void setMxStore(IMXStore mxStore) { + mMxStore = mxStore; + } + + /** + * Get the current public curve25519 key that the AV server is advertising. + * Read the value from cache if any + * + * @param callback on success callback containing the server public key + */ + public void getServerPublicKey(final ApiCallback callback) { + if (mMxStore == null) { + callback.onUnexpectedError(new Exception("MxStore not configured")); + return; + } + + // Check in cache + String keyFromCache = mMxStore.getAntivirusServerPublicKey(); + if (keyFromCache != null) { + callback.onSuccess(keyFromCache); + } else { + mApi.getServerPublicKey().enqueue(new DefaultRetrofit2CallbackWrapper<>(new SimpleApiCallback(callback) { + @Override + public void onSuccess(MediaScanPublicKeyResult info) { + // Store the key in cache for next times + mMxStore.setAntivirusServerPublicKey(info.mCurve25519PublicKey); + + // Note: for some reason info.mCurve25519PublicKey may be null + if (info.mCurve25519PublicKey != null) { + callback.onSuccess(info.mCurve25519PublicKey); + } else { + callback.onUnexpectedError(new Exception("Unable to get server public key from Json")); + } + } + + @Override + public void onMatrixError(MatrixError e) { + // Old Antivirus scanner instance will return a 404 + if (e.mStatus == 404) { + // On 404 consider the public key is not available, so do not encrypt body + mMxStore.setAntivirusServerPublicKey(""); + + callback.onSuccess(""); + } else { + super.onMatrixError(e); + } + } + })); + } + } + + /** + * Reset Antivirus server public key on cache + */ + public void resetServerPublicKey() { + if (mMxStore != null) { + mMxStore.setAntivirusServerPublicKey(null); + } + } + + /** + * Scan an unencrypted file. + * + * @param domain the server name extracted from the matrix content uri + * @param mediaId the media id extracted from the matrix content uri + * @param callback on success callback containing a MediaScanResult object + */ + public void scanUnencryptedFile(final String domain, final String mediaId, final ApiCallback callback) { + mApi.scanUnencrypted(domain, mediaId).enqueue(new DefaultRetrofit2CallbackWrapper<>(callback)); + } + + /** + * Scan an encrypted file. + * + * @param encryptedMediaScanBody the encryption information required to decrypt the content before scanning it. + * @param callback on success callback containing a MediaScanResult object + */ + public void scanEncryptedFile(final EncryptedMediaScanBody encryptedMediaScanBody, final ApiCallback callback) { + // Encrypt encryptedMediaScanBody if the server support it + getServerPublicKey(new SimpleApiCallback(callback) { + @Override + public void onSuccess(String serverPublicKey) { + Call request; + + // Encrypt the data, if antivirus server supports it + if (!TextUtils.isEmpty(serverPublicKey)) { + try { + OlmPkEncryption olmPkEncryption = new OlmPkEncryption(); + olmPkEncryption.setRecipientKey(serverPublicKey); + + String data = JsonUtils.getCanonicalizedJsonString(encryptedMediaScanBody); + + OlmPkMessage message = olmPkEncryption.encrypt(data); + + EncryptedMediaScanEncryptedBody encryptedMediaScanEncryptedBody = new EncryptedMediaScanEncryptedBody(); + encryptedMediaScanEncryptedBody.encryptedBodyFileInfo = new EncryptedBodyFileInfo(message); + + request = mApi.scanEncrypted(encryptedMediaScanEncryptedBody); + } catch (OlmException e) { + // should not happen. Send the error to the caller + request = null; + callback.onUnexpectedError(e); + } + } else { + // No public key on this server, do not encrypt data + request = mApi.scanEncrypted(encryptedMediaScanBody); + } + + if (request != null) { + request.enqueue(new DefaultRetrofit2CallbackWrapper<>(callback)); + } + } + }); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/PresenceRestClient.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/PresenceRestClient.java new file mode 100644 index 0000000000..73a90e840d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/PresenceRestClient.java @@ -0,0 +1,55 @@ +/* + * 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.rest.client; + +import im.vector.matrix.android.internal.legacy.HomeServerConnectionConfig; +import im.vector.matrix.android.internal.legacy.RestClient; +import im.vector.matrix.android.internal.legacy.rest.api.PresenceApi; +import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback; +import im.vector.matrix.android.internal.legacy.rest.callback.RestAdapterCallback; +import im.vector.matrix.android.internal.legacy.rest.model.User; + +/** + * Class used to make requests to the presence API. + */ +public class PresenceRestClient extends RestClient { + + /** + * {@inheritDoc} + */ + public PresenceRestClient(HomeServerConnectionConfig hsConfig) { + super(hsConfig, PresenceApi.class, RestClient.URI_API_PREFIX_PATH_R0, false); + } + + /** + * Get a user's presence state. + * + * @param userId the user id + * @param callback on success callback containing a User object with populated presence and statusMsg fields + */ + public void getPresence(final String userId, final ApiCallback callback) { + final String description = "getPresence userId : " + userId; + + mApi.presenceStatus(userId) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getPresence(userId, callback); + } + })); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/ProfileRestClient.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/ProfileRestClient.java new file mode 100644 index 0000000000..11a456176f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/ProfileRestClient.java @@ -0,0 +1,495 @@ +/* + * 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.rest.client; + +import android.text.TextUtils; + +import im.vector.matrix.android.internal.legacy.HomeServerConnectionConfig; +import im.vector.matrix.android.internal.legacy.RestClient; +import im.vector.matrix.android.internal.legacy.rest.api.ProfileApi; +import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback; +import im.vector.matrix.android.internal.legacy.rest.callback.RestAdapterCallback; +import im.vector.matrix.android.internal.legacy.rest.model.AuthParams; +import im.vector.matrix.android.internal.legacy.rest.model.ChangePasswordParams; +import im.vector.matrix.android.internal.legacy.rest.model.DeactivateAccountParams; +import im.vector.matrix.android.internal.legacy.rest.model.ForgetPasswordParams; +import im.vector.matrix.android.internal.legacy.rest.model.ForgetPasswordResponse; +import im.vector.matrix.android.internal.legacy.rest.model.RequestEmailValidationParams; +import im.vector.matrix.android.internal.legacy.rest.model.RequestEmailValidationResponse; +import im.vector.matrix.android.internal.legacy.rest.model.RequestPhoneNumberValidationParams; +import im.vector.matrix.android.internal.legacy.rest.model.RequestPhoneNumberValidationResponse; +import im.vector.matrix.android.internal.legacy.rest.model.ThreePidCreds; +import im.vector.matrix.android.internal.legacy.rest.model.User; +import im.vector.matrix.android.internal.legacy.rest.model.login.Credentials; +import im.vector.matrix.android.internal.legacy.rest.model.login.TokenRefreshParams; +import im.vector.matrix.android.internal.legacy.rest.model.login.TokenRefreshResponse; +import im.vector.matrix.android.internal.legacy.rest.model.pid.AccountThreePidsResponse; +import im.vector.matrix.android.internal.legacy.rest.model.pid.AddThreePidsParams; +import im.vector.matrix.android.internal.legacy.rest.model.pid.DeleteThreePidParams; +import im.vector.matrix.android.internal.legacy.rest.model.pid.ThirdPartyIdentifier; +import im.vector.matrix.android.internal.legacy.rest.model.pid.ThreePid; + +import java.util.List; +import java.util.Map; + +import retrofit2.Response; + +/** + * Class used to make requests to the profile API. + */ +public class ProfileRestClient extends RestClient { + private static final String LOG_TAG = ProfileRestClient.class.getSimpleName(); + + /** + * {@inheritDoc} + */ + public ProfileRestClient(HomeServerConnectionConfig hsConfig) { + super(hsConfig, ProfileApi.class, "", false); + } + + /** + * Get the user's display name. + * + * @param userId the user id + * @param callback the callback to return the name on success + */ + public void displayname(final String userId, final ApiCallback callback) { + final String description = "display name userId : " + userId; + + mApi.displayname(userId) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + displayname(userId, callback); + } + }) { + @Override + public void success(User user, Response response) { + onEventSent(); + callback.onSuccess(user.displayname); + } + }); + } + + /** + * Update this user's own display name. + * + * @param newName the new name + * @param callback the callback if the call succeeds + */ + public void updateDisplayname(final String newName, final ApiCallback callback) { + // privacy + //final String description = "updateDisplayname newName : " + newName; + final String description = "update display name"; + + // TODO Do not create a User for this + User user = new User(); + user.displayname = newName; + + // don't retry if the network comes back + // let the user chooses what he want to do + mApi.displayname(mCredentials.userId, user) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + updateDisplayname(newName, callback); + } + })); + } + + /** + * Get the user's avatar URL. + * + * @param userId the user id + * @param callback the callback to return the URL on success + */ + public void avatarUrl(final String userId, final ApiCallback callback) { + final String description = "avatarUrl userId : " + userId; + + mApi.avatarUrl(userId) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + avatarUrl(userId, callback); + } + }) { + @Override + public void success(User user, Response response) { + onEventSent(); + callback.onSuccess(user.getAvatarUrl()); + } + }); + } + + /** + * Update this user's own avatar URL. + * + * @param newUrl the new name + * @param callback the callback if the call succeeds + */ + public void updateAvatarUrl(final String newUrl, final ApiCallback callback) { + // privacy + //final String description = "updateAvatarUrl newUrl : " + newUrl; + final String description = "updateAvatarUrl"; + + // TODO Do not create a User for this + User user = new User(); + user.setAvatarUrl(newUrl); + + mApi.avatarUrl(mCredentials.userId, user) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + updateAvatarUrl(newUrl, callback); + } + })); + } + + /** + * Update the password + * + * @param userId the user id + * @param oldPassword the former password + * @param newPassword the new password + * @param callback the callback + */ + public void updatePassword(final String userId, final String oldPassword, final String newPassword, final ApiCallback callback) { + // privacy + //final String description = "update password : " + userId + " oldPassword " + oldPassword + " newPassword " + newPassword; + final String description = "update password"; + + ChangePasswordParams passwordParams = new ChangePasswordParams(); + + passwordParams.auth = new AuthParams(); + passwordParams.auth.type = LoginRestClient.LOGIN_FLOW_TYPE_PASSWORD; + passwordParams.auth.user = userId; + passwordParams.auth.password = oldPassword; + passwordParams.new_password = newPassword; + + mApi.updatePassword(passwordParams) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + updatePassword(userId, oldPassword, newPassword, callback); + } + } + )); + } + + /** + * Reset the password to a new one. + * + * @param newPassword the new password + * @param threepid_creds the three pids. + * @param callback the callback + */ + public void resetPassword(final String newPassword, final Map threepid_creds, final ApiCallback callback) { + // privacy + //final String description = "Reset password : " + threepid_creds + " newPassword " + newPassword; + final String description = "Reset password"; + + ChangePasswordParams passwordParams = new ChangePasswordParams(); + + passwordParams.auth = new AuthParams(); + passwordParams.auth.type = "m.login.email.identity"; + passwordParams.auth.threepid_creds = threepid_creds; + passwordParams.new_password = newPassword; + + mApi.updatePassword(passwordParams) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + resetPassword(newPassword, threepid_creds, callback); + } + })); + } + + /** + * Reset the password server side. + * + * @param email the email to send the password reset. + * @param callback the callback + */ + public void forgetPassword(final String email, final ApiCallback callback) { + final String description = "forget password"; + + if (!TextUtils.isEmpty(email)) { + final ThreePid pid = new ThreePid(email, ThreePid.MEDIUM_EMAIL); + + final ForgetPasswordParams forgetPasswordParams = new ForgetPasswordParams(); + forgetPasswordParams.email = email; + forgetPasswordParams.client_secret = pid.clientSecret; + forgetPasswordParams.send_attempt = 1; + forgetPasswordParams.id_server = mHsConfig.getIdentityServerUri().getHost(); + + mApi.forgetPassword(forgetPasswordParams) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + forgetPassword(email, callback); + } + }) { + @Override + public void success(ForgetPasswordResponse forgetPasswordResponse, Response response) { + onEventSent(); + + pid.sid = forgetPasswordResponse.sid; + callback.onSuccess(pid); + } + }); + } + } + + /** + * Deactivate account + * + * @param type type of authentication + * @param userId current user id + * @param userPassword current password + * @param eraseUserData true to also erase all the user data + * @param callback the callback + */ + public void deactivateAccount(final String type, + final String userId, + final String userPassword, + final boolean eraseUserData, + final ApiCallback callback) { + final String description = "deactivate account"; + + final DeactivateAccountParams params = new DeactivateAccountParams(); + params.auth = new AuthParams(); + params.auth.type = type; + params.auth.user = userId; + params.auth.password = userPassword; + + params.erase = eraseUserData; + + mApi.deactivate(params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + deactivateAccount(type, userId, userPassword, eraseUserData, callback); + } + })); + } + + /** + * Refresh access/refresh tokens, using the current refresh token. + * + * @param callback the callback success and failure callback + */ + public void refreshTokens(final ApiCallback callback) { + final String description = "refreshTokens"; + + TokenRefreshParams params = new TokenRefreshParams(); + params.refresh_token = mCredentials.refreshToken; + + mApi.tokenrefresh(params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, null) { + @Override + public void success(TokenRefreshResponse tokenreponse, Response response) { + onEventSent(); + mCredentials.refreshToken = tokenreponse.refresh_token; + mCredentials.accessToken = tokenreponse.access_token; + if (null != callback) { + callback.onSuccess(mCredentials); + } + } + }); + } + + /** + * List all 3PIDs linked to the Matrix user account. + * + * @param callback the asynchronous callback called with the response + */ + public void threePIDs(final ApiCallback> callback) { + final String description = "threePIDs"; + + mApi.threePIDs() + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, null) { + @Override + public void success(AccountThreePidsResponse accountThreePidsResponse, Response response) { + onEventSent(); + if (null != callback) { + callback.onSuccess(accountThreePidsResponse.threepids); + } + + } + }); + } + + /** + * Request an email validation token. + * + * @param address the email address + * @param clientSecret the client secret number + * @param attempt the attempt count + * @param nextLink the next link + * @param isDuringRegistration true if it occurs during a registration flow + * @param callback the callback + */ + public void requestEmailValidationToken(final String address, final String clientSecret, final int attempt, + final String nextLink, final boolean isDuringRegistration, + final ApiCallback callback) { + final String description = "requestEmailValidationToken"; + + RequestEmailValidationParams params = new RequestEmailValidationParams(); + params.email = address; + params.clientSecret = clientSecret; + params.sendAttempt = attempt; + params.id_server = mHsConfig.getIdentityServerUri().getHost(); + if (!TextUtils.isEmpty(nextLink)) { + params.next_link = nextLink; + } + + final RestAdapterCallback adapterCallback + = new RestAdapterCallback(description, mUnsentEventsManager, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + requestEmailValidationToken(address, clientSecret, attempt, nextLink, isDuringRegistration, callback); + } + } + ) { + @Override + public void success(RequestEmailValidationResponse requestEmailValidationResponse, Response response) { + onEventSent(); + requestEmailValidationResponse.email = address; + requestEmailValidationResponse.clientSecret = clientSecret; + requestEmailValidationResponse.sendAttempt = attempt; + + callback.onSuccess(requestEmailValidationResponse); + } + }; + + if (isDuringRegistration) { + // URL differs in that case + mApi.requestEmailValidationForRegistration(params).enqueue(adapterCallback); + } else { + mApi.requestEmailValidation(params).enqueue(adapterCallback); + } + } + + /** + * Request a phone number validation token. + * + * @param phoneNumber the phone number + * @param countryCode the country code of the phone number + * @param clientSecret the client secret number + * @param attempt the attempt count + * @param isDuringRegistration true if it occurs during a registration flow + * @param callback the callback + */ + public void requestPhoneNumberValidationToken(final String phoneNumber, final String countryCode, + final String clientSecret, final int attempt, + final boolean isDuringRegistration, final ApiCallback callback) { + final String description = "requestPhoneNumberValidationToken"; + + RequestPhoneNumberValidationParams params = new RequestPhoneNumberValidationParams(); + params.phone_number = phoneNumber; + params.country = countryCode; + params.clientSecret = clientSecret; + params.sendAttempt = attempt; + params.id_server = mHsConfig.getIdentityServerUri().getHost(); + + final RestAdapterCallback adapterCallback + = new RestAdapterCallback(description, mUnsentEventsManager, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + requestPhoneNumberValidationToken(phoneNumber, countryCode, clientSecret, attempt, isDuringRegistration, callback); + } + } + ) { + @Override + public void success(RequestPhoneNumberValidationResponse requestPhoneNumberValidationResponse, Response response) { + onEventSent(); + requestPhoneNumberValidationResponse.clientSecret = clientSecret; + requestPhoneNumberValidationResponse.sendAttempt = attempt; + + callback.onSuccess(requestPhoneNumberValidationResponse); + } + }; + + if (isDuringRegistration) { + // URL differs in that case + mApi.requestPhoneNumberValidationForRegistration(params).enqueue(adapterCallback); + } else { + mApi.requestPhoneNumberValidation(params).enqueue(adapterCallback); + } + } + + /** + * Add an 3Pids to an user + * + * @param pid the 3Pid to add + * @param bind bind the email + * @param callback the asynchronous callback called with the response + */ + public void add3PID(final ThreePid pid, final boolean bind, final ApiCallback callback) { + final String description = "add3PID"; + + AddThreePidsParams params = new AddThreePidsParams(); + params.three_pid_creds = new ThreePidCreds(); + + String identityServerHost = mHsConfig.getIdentityServerUri().toString(); + if (identityServerHost.startsWith("http://")) { + identityServerHost = identityServerHost.substring("http://".length()); + } else if (identityServerHost.startsWith("https://")) { + identityServerHost = identityServerHost.substring("https://".length()); + } + + params.three_pid_creds.id_server = identityServerHost; + params.three_pid_creds.sid = pid.sid; + params.three_pid_creds.client_secret = pid.clientSecret; + + params.bind = bind; + + mApi.add3PID(params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + add3PID(pid, bind, callback); + } + })); + } + + /** + * Delete a 3pid of the user + * + * @param pid the 3Pid to delete + * @param callback the asynchronous callback called with the response + */ + public void delete3PID(final ThirdPartyIdentifier pid, final ApiCallback callback) { + final String description = "delete3PID"; + + final DeleteThreePidParams params = new DeleteThreePidParams(); + params.medium = pid.medium; + params.address = pid.address; + + mApi.delete3PID(params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + delete3PID(pid, callback); + } + }) + ); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/PushRulesRestClient.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/PushRulesRestClient.java new file mode 100644 index 0000000000..022a09325a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/PushRulesRestClient.java @@ -0,0 +1,89 @@ +/* + * 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.rest.client; + +import im.vector.matrix.android.internal.legacy.HomeServerConnectionConfig; +import im.vector.matrix.android.internal.legacy.RestClient; +import im.vector.matrix.android.internal.legacy.rest.api.PushRulesApi; +import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback; +import im.vector.matrix.android.internal.legacy.rest.callback.DefaultRetrofit2CallbackWrapper; +import im.vector.matrix.android.internal.legacy.rest.model.bingrules.BingRule; +import im.vector.matrix.android.internal.legacy.rest.model.bingrules.PushRulesResponse; + +public class PushRulesRestClient extends RestClient { + + /** + * {@inheritDoc} + */ + public PushRulesRestClient(HomeServerConnectionConfig hsConfig) { + super(hsConfig, PushRulesApi.class, RestClient.URI_API_PREFIX_PATH_R0, false); + } + + /** + * Retrieve the push rules list. + * + * @param callback the asynchronous callback. + */ + public void getAllRules(final ApiCallback callback) { + mApi.getAllRules().enqueue(new DefaultRetrofit2CallbackWrapper<>(callback)); + } + + /** + * Update the rule enable status. + * + * @param Kind the rule kind + * @param ruleId the rule id + * @param status the rule state + * @param callback the asynchronous callback. + */ + public void updateEnableRuleStatus(String Kind, String ruleId, boolean status, final ApiCallback callback) { + mApi.updateEnableRuleStatus(Kind, ruleId, status).enqueue(new DefaultRetrofit2CallbackWrapper<>(callback)); + } + + /** + * Update the rule actions lists. + * + * @param Kind the rule kind + * @param ruleId the rule id + * @param actions the rule actions list + * @param callback the asynchronous callback + */ + public void updateRuleActions(String Kind, String ruleId, Object actions, final ApiCallback callback) { + mApi.updateRuleActions(Kind, ruleId, actions).enqueue(new DefaultRetrofit2CallbackWrapper<>(callback)); + } + + /** + * Delete a rule. + * + * @param Kind the rule kind + * @param ruleId the rule id + * @param callback the asynchronous callback + */ + public void deleteRule(String Kind, String ruleId, final ApiCallback callback) { + mApi.deleteRule(Kind, ruleId).enqueue(new DefaultRetrofit2CallbackWrapper<>(callback)); + } + + /** + * Add a rule. + * + * @param rule the rule + * @param callback the asynchronous callback + */ + public void addRule(BingRule rule, final ApiCallback callback) { + mApi.addRule(rule.kind, rule.ruleId, rule.toJsonElement()).enqueue(new DefaultRetrofit2CallbackWrapper<>(callback)); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/PushersRestClient.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/PushersRestClient.java new file mode 100644 index 0000000000..2bddac1cd2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/PushersRestClient.java @@ -0,0 +1,166 @@ +/* + * 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.rest.client; + +import im.vector.matrix.android.internal.legacy.HomeServerConnectionConfig; +import im.vector.matrix.android.internal.legacy.RestClient; +import im.vector.matrix.android.internal.legacy.data.Pusher; +import im.vector.matrix.android.internal.legacy.rest.api.PushersApi; +import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback; +import im.vector.matrix.android.internal.legacy.rest.callback.RestAdapterCallback; +import im.vector.matrix.android.internal.legacy.rest.model.PushersResponse; + +import java.util.HashMap; + +/** + * REST client for the Pushers API. + */ +public class PushersRestClient extends RestClient { + private static final String LOG_TAG = PushersRestClient.class.getSimpleName(); + + private static final String PUSHER_KIND_HTTP = "http"; + private static final String DATA_KEY_HTTP_URL = "url"; + + public PushersRestClient(HomeServerConnectionConfig hsConfig) { + super(hsConfig, PushersApi.class, RestClient.URI_API_PREFIX_PATH_R0, true); + } + + /** + * Add a new HTTP pusher. + * + * @param pushkey the pushkey + * @param appId the application id + * @param profileTag the profile tag + * @param lang the language + * @param appDisplayName a human-readable application name + * @param deviceDisplayName a human-readable device name + * @param url the URL that should be used to send notifications + * @param append append the pusher + * @param withEventIdOnly true to limit the push content + * @param callback the asynchronous callback + */ + public void addHttpPusher(final String pushkey, + final String appId, + final String profileTag, + final String lang, + final String appDisplayName, + final String deviceDisplayName, + final String url, + boolean append, + boolean withEventIdOnly, + final ApiCallback callback) { + manageHttpPusher(pushkey, appId, profileTag, lang, appDisplayName, deviceDisplayName, url, append, withEventIdOnly, true, callback); + } + + /** + * remove a new HTTP pusher. + * + * @param pushkey the pushkey + * @param appId the application id + * @param profileTag the profile tag + * @param lang the language + * @param appDisplayName a human-readable application name + * @param deviceDisplayName a human-readable device name + * @param url the URL that should be used to send notifications + * @param callback the asynchronous callback + */ + public void removeHttpPusher(final String pushkey, + final String appId, + final String profileTag, + final String lang, + final String appDisplayName, + final String deviceDisplayName, + final String url, + final ApiCallback callback) { + manageHttpPusher(pushkey, appId, profileTag, lang, appDisplayName, deviceDisplayName, url, false, false, false, callback); + } + + + /** + * add/remove a new HTTP pusher. + * + * @param pushkey the pushkey + * @param appId the application id + * @param profileTag the profile tag + * @param lang the language + * @param appDisplayName a human-readable application name + * @param deviceDisplayName a human-readable device name + * @param url the URL that should be used to send notifications + * @param withEventIdOnly true to limit the push content + * @param addPusher true to add the pusher / false to remove it + * @param callback the asynchronous callback + */ + private void manageHttpPusher(final String pushkey, + final String appId, + final String profileTag, + final String lang, + final String appDisplayName, + final String deviceDisplayName, + final String url, + final boolean append, + final boolean withEventIdOnly, + final boolean addPusher, + final ApiCallback callback) { + Pusher pusher = new Pusher(); + pusher.pushkey = pushkey; + pusher.appId = appId; + pusher.profileTag = profileTag; + pusher.lang = lang; + pusher.kind = addPusher ? PUSHER_KIND_HTTP : null; + pusher.appDisplayName = appDisplayName; + pusher.deviceDisplayName = deviceDisplayName; + pusher.data = new HashMap<>(); + pusher.data.put(DATA_KEY_HTTP_URL, url); + + if (addPusher) { + pusher.append = append; + } + + if (withEventIdOnly) { + pusher.data.put("format", "event_id_only"); + } + + final String description = "manageHttpPusher"; + + mApi.set(pusher) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + manageHttpPusher(pushkey, appId, profileTag, lang, appDisplayName, deviceDisplayName, + url, append, withEventIdOnly, addPusher, callback); + } + })); + } + + /** + * Retrieve the pushers list + * + * @param callback the callback + */ + public void getPushers(final ApiCallback callback) { + final String description = "getPushers"; + + mApi.get() + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getPushers(callback); + } + })); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/RoomsRestClient.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/RoomsRestClient.java new file mode 100644 index 0000000000..8f0caeb274 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/RoomsRestClient.java @@ -0,0 +1,1023 @@ +/* + * 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.rest.client; + +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.HomeServerConnectionConfig; +import im.vector.matrix.android.internal.legacy.RestClient; +import im.vector.matrix.android.internal.legacy.data.RoomState; +import im.vector.matrix.android.internal.legacy.data.timeline.EventTimeline; +import im.vector.matrix.android.internal.legacy.rest.api.RoomsApi; +import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback; +import im.vector.matrix.android.internal.legacy.rest.callback.RestAdapterCallback; +import im.vector.matrix.android.internal.legacy.rest.callback.SimpleApiCallback; +import im.vector.matrix.android.internal.legacy.rest.model.BannedUser; +import im.vector.matrix.android.internal.legacy.rest.model.ChunkEvents; +import im.vector.matrix.android.internal.legacy.rest.model.CreateRoomParams; +import im.vector.matrix.android.internal.legacy.rest.model.CreateRoomResponse; +import im.vector.matrix.android.internal.legacy.rest.model.CreatedEvent; +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.PowerLevels; +import im.vector.matrix.android.internal.legacy.rest.model.ReportContentParams; +import im.vector.matrix.android.internal.legacy.rest.model.RoomAliasDescription; +import im.vector.matrix.android.internal.legacy.rest.model.RoomDirectoryVisibility; +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.Typing; +import im.vector.matrix.android.internal.legacy.rest.model.User; +import im.vector.matrix.android.internal.legacy.rest.model.filter.RoomEventFilter; +import im.vector.matrix.android.internal.legacy.rest.model.message.Message; +import im.vector.matrix.android.internal.legacy.rest.model.sync.RoomResponse; + +import java.util.HashMap; +import java.util.Map; + +/** + * Class used to make requests to the rooms API. + */ +public class RoomsRestClient extends RestClient { + private static final String LOG_TAG = RoomsRestClient.class.getSimpleName(); + + public static final int DEFAULT_MESSAGES_PAGINATION_LIMIT = 30; + + // read marker field names + private static final String READ_MARKER_FULLY_READ = "m.fully_read"; + private static final String READ_MARKER_READ = "m.read"; + + /** + * {@inheritDoc} + */ + public RoomsRestClient(HomeServerConnectionConfig hsConfig) { + super(hsConfig, RoomsApi.class, RestClient.URI_API_PREFIX_PATH_R0, false); + } + + /** + * Send a message to room + * + * @param transactionId the unique transaction id (it should avoid duplicated messages) + * @param roomId the room id + * @param message the message + * @param callback the callback containing the created event if successful + */ + public void sendMessage(final String transactionId, final String roomId, final Message message, final ApiCallback callback) { + // privacy + // final String description = "SendMessage : roomId " + roomId + " - message " + message.body; + final String description = "SendMessage : roomId " + roomId; + + // the messages have their dedicated method in MXSession to be resent if there is no available network + mApi.sendMessage(transactionId, roomId, message) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + sendMessage(transactionId, roomId, message, callback); + } + })); + } + + /** + * Send an event to a room. + * + * @param transactionId the unique transaction id (it should avoid duplicated messages) + * @param roomId the room id + * @param eventType the type of event + * @param content the event content + * @param callback the callback containing the created event if successful + */ + public void sendEventToRoom(final String transactionId, + final String roomId, + final String eventType, + final JsonObject content, + final ApiCallback callback) { + // privacy + //final String description = "sendEvent : roomId " + roomId + " - eventType " + eventType + " content " + content; + final String description = "sendEvent : roomId " + roomId + " - eventType " + eventType; + + // do not retry the call invite + // it might trigger weird behaviour on flaggy networks + if (!TextUtils.equals(eventType, Event.EVENT_TYPE_CALL_INVITE)) { + mApi.send(transactionId, roomId, eventType, content) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + sendEventToRoom(transactionId, roomId, eventType, content, callback); + } + })); + } else { + mApi.send(transactionId, roomId, eventType, content) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, null)); + } + } + + /** + * Get a limited amount of messages, for the given room starting from the given token. + * The amount of message is set to {@link #DEFAULT_MESSAGES_PAGINATION_LIMIT}. + * + * @param roomId the room id + * @param fromToken the token identifying the message to start from Required. + * @param direction the direction. Required. + * @param limit the maximum number of messages to retrieve. + * @param roomEventFilter A RoomEventFilter to filter returned events with. Optional. + * @param callback the callback called with the response. Messages will be returned in reverse order. + */ + public void getRoomMessagesFrom(final String roomId, + final String fromToken, + final EventTimeline.Direction direction, + final int limit, + @Nullable final RoomEventFilter roomEventFilter, + final ApiCallback callback) { + final String description = "messagesFrom : roomId " + roomId + " fromToken " + fromToken + "with direction " + direction + " with limit " + limit; + + mApi.getRoomMessagesFrom(roomId, fromToken, (direction == EventTimeline.Direction.BACKWARDS) ? "b" : "f", limit, toJson(roomEventFilter)) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getRoomMessagesFrom(roomId, fromToken, direction, limit, roomEventFilter, callback); + } + })); + } + + /** + * Invite a user to a room. + * + * @param roomId the room id + * @param userId the user id + * @param callback the async callback + */ + public void inviteUserToRoom(final String roomId, final String userId, final ApiCallback callback) { + final String description = "inviteToRoom : roomId " + roomId + " userId " + userId; + + // TODO Do not create a User for this + User user = new User(); + user.user_id = userId; + + mApi.invite(roomId, user) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + inviteUserToRoom(roomId, userId, callback); + } + })); + } + + /** + * Invite a user by his email address to a room. + * + * @param roomId the room id + * @param email the email + * @param callback the async callback + */ + public void inviteByEmailToRoom(final String roomId, final String email, final ApiCallback callback) { + inviteThreePidToRoom("email", email, roomId, callback); + } + + /** + * Invite an user from a 3Pids. + * + * @param medium the medium + * @param address the address + * @param roomId the room id + * @param callback the async callback + */ + private void inviteThreePidToRoom(final String medium, final String address, final String roomId, final ApiCallback callback) { + // privacy + //final String description = "inviteThreePidToRoom : medium " + medium + " address " + address + " roomId " + roomId; + final String description = "inviteThreePidToRoom : medium " + medium + " roomId " + roomId; + + // This request must not have the protocol part + String identityServer = mHsConfig.getIdentityServerUri().toString(); + + if (identityServer.startsWith("http://")) { + identityServer = identityServer.substring("http://".length()); + } else if (identityServer.startsWith("https://")) { + identityServer = identityServer.substring("https://".length()); + } + + Map parameters = new HashMap<>(); + parameters.put("id_server", identityServer); + parameters.put("medium", medium); + parameters.put("address", address); + + mApi.invite(roomId, parameters) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + inviteThreePidToRoom(medium, address, roomId, callback); + } + })); + } + + /** + * Join a room by its roomAlias or its roomId. + * + * @param roomIdOrAlias the room id or the room alias + * @param callback the async callback + */ + public void joinRoom(final String roomIdOrAlias, final ApiCallback callback) { + joinRoom(roomIdOrAlias, null, callback); + } + + /** + * Join a room by its roomAlias or its roomId with some parameters. + * + * @param roomIdOrAlias the room id or the room alias + * @param params the joining parameters. + * @param callback the async callback + */ + public void joinRoom(final String roomIdOrAlias, final Map params, final ApiCallback callback) { + final String description = "joinRoom : roomId " + roomIdOrAlias; + + mApi.joinRoomByAliasOrId(roomIdOrAlias, (null == params) ? new HashMap() : params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + joinRoom(roomIdOrAlias, params, callback); + } + })); + } + + /** + * Leave a room. + * + * @param roomId the room id + * @param callback the async callback + */ + public void leaveRoom(final String roomId, final ApiCallback callback) { + final String description = "leaveRoom : roomId " + roomId; + + mApi.leave(roomId, new JsonObject()) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + leaveRoom(roomId, callback); + } + })); + } + + /** + * Forget a room. + * + * @param roomId the room id + * @param callback the async callback + */ + public void forgetRoom(final String roomId, final ApiCallback callback) { + final String description = "forgetRoom : roomId " + roomId; + + mApi.forget(roomId, new JsonObject()) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + forgetRoom(roomId, callback); + } + })); + } + + /** + * Kick a user from a room. + * + * @param roomId the room id + * @param userId the user id + * @param callback the async callback + */ + public void kickFromRoom(final String roomId, final String userId, final ApiCallback callback) { + final String description = "kickFromRoom : roomId " + roomId + " userId " + userId; + + // TODO It does not look like this in the Matrix spec + // Kicking is done by posting that the user is now in a "leave" state + RoomMember member = new RoomMember(); + member.membership = RoomMember.MEMBERSHIP_LEAVE; + + mApi.updateRoomMember(roomId, userId, member) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + kickFromRoom(roomId, userId, callback); + } + })); + } + + /** + * Ban a user from a room. + * + * @param roomId the room id + * @param user the banned user object (userId and reason for ban) + * @param callback the async callback + */ + public void banFromRoom(final String roomId, final BannedUser user, final ApiCallback callback) { + final String description = "banFromRoom : roomId " + roomId + " userId " + user.userId; + + mApi.ban(roomId, user) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + banFromRoom(roomId, user, callback); + } + })); + } + + /** + * Unban an user from a room. + * + * @param roomId the room id + * @param user the banned user (userId) + * @param callback the async callback + */ + public void unbanFromRoom(final String roomId, final BannedUser user, final ApiCallback callback) { + final String description = "Unban : roomId " + roomId + " userId " + user.userId; + + mApi.unban(roomId, user) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + unbanFromRoom(roomId, user, callback); + } + })); + } + + /** + * Create a new room. + * + * @param params the room creation parameters + * @param callback the async callback + */ + public void createRoom(final CreateRoomParams params, final ApiCallback callback) { + // privacy + //final String description = "createRoom : name " + name + " topic " + topic; + final String description = "createRoom"; + + mApi.createRoom(params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + createRoom(params, callback); + } + })); + } + + /** + * Perform an initial sync on the room + * + * @param roomId the room id + * @param callback the async callback + */ + public void initialSync(final String roomId, final ApiCallback callback) { + final String description = "initialSync : roomId " + roomId; + + mApi.initialSync(roomId, DEFAULT_MESSAGES_PAGINATION_LIMIT) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + initialSync(roomId, callback); + } + })); + } + + /** + * Retrieve an event from its room id / event id. + * + * @param roomId the room id + * @param eventId the event id + * @param callback the asynchronous callback. + */ + public void getEvent(final String roomId, final String eventId, final ApiCallback callback) { + // try first with roomid / event id + getEventFromRoomIdEventId(roomId, eventId, new SimpleApiCallback(callback) { + @Override + public void onSuccess(Event event) { + callback.onSuccess(event); + } + + @Override + public void onMatrixError(MatrixError e) { + if (TextUtils.equals(e.errcode, MatrixError.UNRECOGNIZED)) { + // Try to retrieve the event using the context API + // It's ok to pass null as a filter here + getContextOfEvent(roomId, eventId, 1, null, new SimpleApiCallback(callback) { + @Override + public void onSuccess(EventContext eventContext) { + callback.onSuccess(eventContext.event); + } + }); + } else { + callback.onMatrixError(e); + } + } + }); + } + + /** + * Retrieve an event from its room id / event id. + * + * @param roomId the room id + * @param eventId the event id + * @param callback the asynchronous callback. + */ + private void getEventFromRoomIdEventId(final String roomId, final String eventId, final ApiCallback callback) { + final String description = "getEventFromRoomIdEventId : roomId " + roomId + " eventId " + eventId; + + mApi.getEvent(roomId, eventId) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getEventFromRoomIdEventId(roomId, eventId, callback); + } + })); + } + + /** + * Get the context surrounding an event. + * + * @param roomId the room id + * @param eventId the event Id + * @param limit the maximum number of messages to retrieve + * @param roomEventFilter A RoomEventFilter to filter returned events with. Optional. + * @param callback the asynchronous callback called with the response + */ + public void getContextOfEvent(final String roomId, + final String eventId, + final int limit, + @Nullable final RoomEventFilter roomEventFilter, + final ApiCallback callback) { + final String description = "getContextOfEvent : roomId " + roomId + " eventId " + eventId + " limit " + limit; + + mApi.getContextOfEvent(roomId, eventId, limit, toJson(roomEventFilter)) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getContextOfEvent(roomId, eventId, limit, roomEventFilter, callback); + } + })); + } + + /** + * Update the room name. + * + * @param roomId the room id + * @param name the room name + * @param callback the async callback + */ + public void updateRoomName(final String roomId, final String name, final ApiCallback callback) { + final String description = "updateName : roomId " + roomId + " name " + name; + + Map params = new HashMap<>(); + params.put("name", name); + + mApi.sendStateEvent(roomId, Event.EVENT_TYPE_STATE_ROOM_NAME, params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + updateRoomName(roomId, name, callback); + } + })); + } + + /** + * Update the canonical alias. + * + * @param roomId the room id + * @param canonicalAlias the canonical alias + * @param callback the async callback + */ + public void updateCanonicalAlias(final String roomId, final String canonicalAlias, final ApiCallback callback) { + final String description = "updateCanonicalAlias : roomId " + roomId + " canonicalAlias " + canonicalAlias; + + Map params = new HashMap<>(); + params.put("alias", canonicalAlias); + + mApi.sendStateEvent(roomId, Event.EVENT_TYPE_STATE_CANONICAL_ALIAS, params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + updateCanonicalAlias(roomId, canonicalAlias, callback); + } + })); + } + + /** + * Update history visibility. + * + * @param roomId the room id + * @param aVisibility the visibility + * @param callback the async callback + */ + public void updateHistoryVisibility(final String roomId, final String aVisibility, final ApiCallback callback) { + final String description = "updateHistoryVisibility : roomId " + roomId + " visibility " + aVisibility; + + Map params = new HashMap<>(); + params.put("history_visibility", aVisibility); + + mApi.sendStateEvent(roomId, Event.EVENT_TYPE_STATE_HISTORY_VISIBILITY, params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + updateHistoryVisibility(roomId, aVisibility, callback); + } + })); + } + + /** + * Update the directory visibility of the room. + * + * @param aRoomId the room id + * @param aDirectoryVisibility the visibility of the room in the directory list + * @param callback the async callback response + */ + public void updateDirectoryVisibility(final String aRoomId, final String aDirectoryVisibility, final ApiCallback callback) { + final String description = "updateRoomDirectoryVisibility : roomId=" + aRoomId + " visibility=" + aDirectoryVisibility; + + RoomDirectoryVisibility roomDirectoryVisibility = new RoomDirectoryVisibility(); + roomDirectoryVisibility.visibility = aDirectoryVisibility; + + mApi.setRoomDirectoryVisibility(aRoomId, roomDirectoryVisibility) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + updateDirectoryVisibility(aRoomId, aDirectoryVisibility, callback); + } + })); + } + + + /** + * Get the directory visibility of the room (see {@link #updateDirectoryVisibility(String, String, ApiCallback)}). + * + * @param aRoomId the room ID + * @param callback on success callback containing a the room directory visibility + */ + public void getDirectoryVisibility(final String aRoomId, final ApiCallback callback) { + final String description = "getDirectoryVisibility roomId=" + aRoomId; + + mApi.getRoomDirectoryVisibility(aRoomId) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getDirectoryVisibility(aRoomId, callback); + } + })); + } + + /** + * Get the room members + * + * @param roomId the room id where to get the members + * @param syncToken the sync token (optional) + * @param membership to include only one type of membership (optional) + * @param notMembership to exclude one type of membership (optional) + * @param callback the callback + */ + public void getRoomMembers(final String roomId, + @Nullable final String syncToken, + @Nullable final String membership, + @Nullable final String notMembership, + final ApiCallback callback) { + final String description = "getRoomMembers roomId=" + roomId; + + mApi.getMembers(roomId, syncToken, membership, notMembership) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getRoomMembers(roomId, syncToken, membership, notMembership, callback); + } + })); + } + + /** + * Update the room topic. + * + * @param roomId the room id + * @param topic the room topic + * @param callback the async callback + */ + public void updateTopic(final String roomId, final String topic, final ApiCallback callback) { + final String description = "updateTopic : roomId " + roomId + " topic " + topic; + + Map params = new HashMap<>(); + params.put("topic", topic); + + mApi.sendStateEvent(roomId, Event.EVENT_TYPE_STATE_ROOM_TOPIC, params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + updateTopic(roomId, topic, callback); + } + })); + } + + /** + * Redact an event. + * + * @param roomId the room id + * @param eventId the event id + * @param callback the callback containing the created event if successful + */ + public void redactEvent(final String roomId, final String eventId, final ApiCallback callback) { + final String description = "redactEvent : roomId " + roomId + " eventId " + eventId; + + mApi.redactEvent(roomId, eventId, new JsonObject()) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + redactEvent(roomId, eventId, callback); + } + })); + } + + /** + * Report an event. + * + * @param roomId the room id + * @param eventId the event id + * @param score the metric to let the user rate the severity of the abuse. It ranges from -100 “most offensive” to 0 “inoffensive” + * @param reason the reason + * @param callback the callback + */ + public void reportEvent(final String roomId, final String eventId, final int score, final String reason, final ApiCallback callback) { + final String description = "report : roomId " + roomId + " eventId " + eventId; + + ReportContentParams content = new ReportContentParams(); + + content.score = score; + content.reason = reason; + + mApi.reportEvent(roomId, eventId, content) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + reportEvent(roomId, eventId, score, reason, callback); + } + })); + } + + /** + * Update the power levels. + * + * @param roomId the room id + * @param powerLevels the new powerLevels + * @param callback the async callback + */ + public void updatePowerLevels(final String roomId, final PowerLevels powerLevels, final ApiCallback callback) { + final String description = "updatePowerLevels : roomId " + roomId + " powerLevels " + powerLevels; + + mApi.setPowerLevels(roomId, powerLevels) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + updatePowerLevels(roomId, powerLevels, callback); + } + })); + } + + /** + * Send a state events. + * + * @param roomId the dedicated room id + * @param eventType the event type + * @param stateKey the state key + * @param params the put parameters + * @param callback the asynchronous callback + */ + public void sendStateEvent(final String roomId, + final String eventType, + @Nullable final String stateKey, + final Map params, + final ApiCallback callback) { + final String description = "sendStateEvent : roomId " + roomId + " - eventType " + eventType; + + if (null != stateKey) { + mApi.sendStateEvent(roomId, eventType, stateKey, params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + sendStateEvent(roomId, eventType, stateKey, params, callback); + } + })); + } else { + mApi.sendStateEvent(roomId, eventType, params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + sendStateEvent(roomId, eventType, null, params, callback); + } + })); + } + } + + /** + * Looks up the contents of a state event in a room + * + * @param roomId the room id + * @param eventType the event type + * @param callback the asynchronous callback + */ + public void getStateEvent(final String roomId, final String eventType, final ApiCallback callback) { + final String description = "getStateEvent : roomId " + roomId + " eventId " + eventType; + + mApi.getStateEvent(roomId, eventType) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getStateEvent(roomId, eventType, callback); + } + })); + } + + /** + * Looks up the contents of a state event in a room + * + * @param roomId the room id + * @param eventType the event type + * @param stateKey the key of the state to look up + * @param callback the asynchronous callback + */ + public void getStateEvent(final String roomId, final String eventType, final String stateKey, final ApiCallback callback) { + final String description = "getStateEvent : roomId " + roomId + " eventId " + eventType + " stateKey " + stateKey; + + mApi.getStateEvent(roomId, eventType, stateKey) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getStateEvent(roomId, eventType, stateKey, callback); + } + })); + } + + /** + * send typing notification. + * + * @param roomId the room id + * @param userId the user id + * @param isTyping true if the user is typing + * @param timeout the typing event timeout + * @param callback the asynchronous callback + */ + public void sendTypingNotification(String roomId, String userId, boolean isTyping, int timeout, ApiCallback callback) { + final String description = "sendTypingNotification : roomId " + roomId + " isTyping " + isTyping; + + Typing typing = new Typing(); + typing.typing = isTyping; + + if (-1 != timeout) { + typing.timeout = timeout; + } + + // never resend typing on network error + mApi.setTypingNotification(roomId, userId, typing) + .enqueue(new RestAdapterCallback(description, null, callback, null)); + } + + /** + * Update the room avatar url. + * + * @param roomId the room id + * @param avatarUrl canonical alias + * @param callback the async callback + */ + public void updateAvatarUrl(final String roomId, final String avatarUrl, final ApiCallback callback) { + final String description = "updateAvatarUrl : roomId " + roomId + " avatarUrl " + avatarUrl; + + Map params = new HashMap<>(); + params.put("url", avatarUrl); + + mApi.sendStateEvent(roomId, Event.EVENT_TYPE_STATE_ROOM_AVATAR, params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + updateAvatarUrl(roomId, avatarUrl, callback); + } + })); + } + + /** + * Send a read markers. + * + * @param roomId the room id + * @param rmEventId the read marker event Id + * @param rrEventId the read receipt event Id + * @param callback the callback + */ + public void sendReadMarker(final String roomId, final String rmEventId, final String rrEventId, final ApiCallback callback) { + final String description = "sendReadMarker : roomId " + roomId + " - rmEventId " + rmEventId + " -- rrEventId " + rrEventId; + final Map params = new HashMap<>(); + + if (!TextUtils.isEmpty(rmEventId)) { + params.put(READ_MARKER_FULLY_READ, rmEventId); + } + + if (!TextUtils.isEmpty(rrEventId)) { + params.put(READ_MARKER_READ, rrEventId); + } + + mApi.sendReadMarker(roomId, params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, true, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + sendReadMarker(roomId, rmEventId, rrEventId, callback); + } + })); + } + + /** + * Add a tag to a room. + * Use this method to update the order of an existing tag. + * + * @param roomId the roomId + * @param tag the new tag to add to the room. + * @param order the order. + * @param callback the operation callback + */ + public void addTag(final String roomId, final String tag, final Double order, final ApiCallback callback) { + final String description = "addTag : roomId " + roomId + " - tag " + tag + " - order " + order; + + Map hashMap = new HashMap<>(); + hashMap.put("order", order); + + mApi.addTag(mCredentials.userId, roomId, tag, hashMap) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + addTag(roomId, tag, order, callback); + } + })); + } + + /** + * Remove a tag to a room. + * + * @param roomId the roomId + * @param tag the new tag to add to the room. + * @param callback the operation callback + */ + public void removeTag(final String roomId, final String tag, final ApiCallback callback) { + final String description = "removeTag : roomId " + roomId + " - tag " + tag; + + mApi.removeTag(mCredentials.userId, roomId, tag) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + removeTag(roomId, tag, callback); + } + })); + } + + /** + * Update the URL preview status + * + * @param roomId the roomId + * @param status the new status + * @param callback the operation callback + */ + public void updateURLPreviewStatus(final String roomId, final boolean status, final ApiCallback callback) { + final String description = "updateURLPreviewStatus : roomId " + roomId + " - status " + status; + + Map params = new HashMap<>(); + params.put(AccountDataRestClient.ACCOUNT_DATA_KEY_URL_PREVIEW_DISABLE, !status); + + mApi.updateAccountData(mCredentials.userId, roomId, Event.EVENT_TYPE_URL_PREVIEW, params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + updateURLPreviewStatus(roomId, status, callback); + } + })); + } + + /** + * Get the room ID corresponding to this room alias. + * + * @param roomAlias the room alias. + * @param callback the operation callback + */ + public void getRoomIdByAlias(final String roomAlias, final ApiCallback callback) { + final String description = "getRoomIdByAlias : " + roomAlias; + + mApi.getRoomIdByAlias(roomAlias) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, + new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + getRoomIdByAlias(roomAlias, callback); + } + })); + } + + /** + * Set the room ID corresponding to a room alias. + * + * @param roomId the room id. + * @param roomAlias the room alias. + * @param callback the operation callback + */ + public void setRoomIdByAlias(final String roomId, final String roomAlias, final ApiCallback callback) { + final String description = "setRoomIdByAlias : roomAlias " + roomAlias + " - roomId : " + roomId; + + RoomAliasDescription roomAliasDescription = new RoomAliasDescription(); + roomAliasDescription.room_id = roomId; + + mApi.setRoomIdByAlias(roomAlias, roomAliasDescription) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + setRoomIdByAlias(roomId, roomAlias, callback); + } + })); + } + + /** + * Remove the room alias. + * + * @param roomAlias the room alias. + * @param callback the room alias description + */ + public void removeRoomAlias(final String roomAlias, final ApiCallback callback) { + final String description = "removeRoomAlias : " + roomAlias; + + mApi.removeRoomAlias(roomAlias) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + removeRoomAlias(roomAlias, callback); + } + })); + } + + /** + * Update the join rule of the room. + * To make the room private, the aJoinRule must be set to {@link RoomState#JOIN_RULE_INVITE}. + * + * @param aRoomId the room id + * @param aJoinRule the join rule: {@link RoomState#JOIN_RULE_PUBLIC} or {@link RoomState#JOIN_RULE_INVITE} + * @param callback the async callback response + */ + public void updateJoinRules(final String aRoomId, final String aJoinRule, final ApiCallback callback) { + final String description = "updateJoinRules : roomId=" + aRoomId + " rule=" + aJoinRule; + + Map params = new HashMap<>(); + params.put("join_rule", aJoinRule); + + mApi.sendStateEvent(aRoomId, Event.EVENT_TYPE_STATE_ROOM_JOIN_RULES, params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + updateJoinRules(aRoomId, aJoinRule, callback); + } + })); + } + + /** + * Update the guest access rule of the room. + * To deny guest access to the room, aGuestAccessRule must be set to {@link RoomState#GUEST_ACCESS_FORBIDDEN} + * + * @param aRoomId the room id + * @param aGuestAccessRule the guest access rule: {@link RoomState#GUEST_ACCESS_CAN_JOIN} or {@link RoomState#GUEST_ACCESS_FORBIDDEN} + * @param callback the async callback response + */ + public void updateGuestAccess(final String aRoomId, final String aGuestAccessRule, final ApiCallback callback) { + final String description = "updateGuestAccess : roomId=" + aRoomId + " rule=" + aGuestAccessRule; + + Map params = new HashMap<>(); + params.put("guest_access", aGuestAccessRule); + + mApi.sendStateEvent(aRoomId, Event.EVENT_TYPE_STATE_ROOM_GUEST_ACCESS, params) + .enqueue(new RestAdapterCallback(description, mUnsentEventsManager, callback, new RestAdapterCallback.RequestRetryCallBack() { + @Override + public void onRetry() { + updateGuestAccess(aRoomId, aGuestAccessRule, callback); + } + })); + } + + @Nullable + private String toJson(@Nullable RoomEventFilter roomEventFilter) { + if (roomEventFilter == null) { + return null; + } + + return roomEventFilter.toJSONString(); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/ThirdPidRestClient.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/ThirdPidRestClient.java new file mode 100644 index 0000000000..b3db9a53eb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/ThirdPidRestClient.java @@ -0,0 +1,255 @@ +/* + * 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.rest.client; + +import im.vector.matrix.android.internal.legacy.HomeServerConnectionConfig; +import im.vector.matrix.android.internal.legacy.RestClient; +import im.vector.matrix.android.internal.legacy.rest.api.ThirdPidApi; +import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback; +import im.vector.matrix.android.internal.legacy.rest.callback.DefaultRetrofit2ResponseHandler; +import im.vector.matrix.android.internal.legacy.rest.model.BulkLookupParams; +import im.vector.matrix.android.internal.legacy.rest.model.BulkLookupResponse; +import im.vector.matrix.android.internal.legacy.rest.model.HttpError; +import im.vector.matrix.android.internal.legacy.rest.model.HttpException; +import im.vector.matrix.android.internal.legacy.rest.model.pid.PidResponse; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class ThirdPidRestClient extends RestClient { + + private static final String KEY_SUBMIT_TOKEN_SUCCESS = "success"; + + /** + * {@inheritDoc} + */ + public ThirdPidRestClient(HomeServerConnectionConfig hsConfig) { + super(hsConfig, ThirdPidApi.class, URI_API_PREFIX_IDENTITY, false, true); + } + + /** + * Retrieve user matrix id from a 3rd party id. + * + * @param address 3rd party id + * @param medium the media. + * @param callback the 3rd party callback + */ + public void lookup3Pid(String address, String medium, final ApiCallback callback) { + mApi.lookup3Pid(address, medium).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + try { + handleLookup3PidResponse(response, callback); + } catch (IOException e) { + onFailure(call, e); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + callback.onUnexpectedError((Exception) t); + } + }); + } + + private void handleLookup3PidResponse( + Response response, + final ApiCallback callback + ) throws IOException { + DefaultRetrofit2ResponseHandler.handleResponse( + response, + new DefaultRetrofit2ResponseHandler.Listener() { + @Override + public void onSuccess(Response response) { + PidResponse pidResponse = response.body(); + callback.onSuccess((null == pidResponse.mxid) ? "" : pidResponse.mxid); + } + + @Override + public void onHttpError(HttpError httpError) { + callback.onNetworkError(new HttpException(httpError)); + } + } + ); + } + + + /** + * Request the ownership validation of an email address or a phone number previously set + * by {@link ProfileRestClient#requestEmailValidationToken(String, String, int, String, boolean, ApiCallback)} + * + * @param medium the medium of the 3pid + * @param token the token generated by the requestEmailValidationToken call + * @param clientSecret the client secret which was supplied in the requestEmailValidationToken call + * @param sid the sid for the session + * @param callback asynchronous callback response + */ + public void submitValidationToken(final String medium, + final String token, + final String clientSecret, + final String sid, + final ApiCallback callback) { + mApi.requestOwnershipValidation(medium, token, clientSecret, sid).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + try { + handleSubmitValidationTokenResponse(response, callback); + } catch (IOException e) { + callback.onUnexpectedError(e); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + callback.onUnexpectedError((Exception) t); + } + }); + } + + private void handleSubmitValidationTokenResponse( + Response> response, + final ApiCallback callback + ) throws IOException { + DefaultRetrofit2ResponseHandler.handleResponse( + response, + new DefaultRetrofit2ResponseHandler.Listener>() { + @Override + public void onSuccess(Response> response) { + Map aDataRespMap = response.body(); + if (aDataRespMap.containsKey(KEY_SUBMIT_TOKEN_SUCCESS)) { + callback.onSuccess((Boolean) aDataRespMap.get(KEY_SUBMIT_TOKEN_SUCCESS)); + } else { + callback.onSuccess(false); + } + } + + @Override + public void onHttpError(HttpError httpError) { + callback.onNetworkError(new HttpException(httpError)); + } + } + ); + } + + /** + * Retrieve user matrix id from a 3rd party id. + * + * @param addresses 3rd party ids + * @param mediums the medias. + * @param callback the 3rd parties callback + */ + public void lookup3Pids(final List addresses, final List mediums, final ApiCallback> callback) { + // sanity checks + if ((null == addresses) || (null == mediums) || (addresses.size() != mediums.size())) { + callback.onUnexpectedError(new Exception("invalid params")); + return; + } + + // nothing to check + if (0 == mediums.size()) { + callback.onSuccess(new ArrayList()); + return; + } + + BulkLookupParams threePidsParams = new BulkLookupParams(); + + List> list = new ArrayList<>(); + + for (int i = 0; i < addresses.size(); i++) { + list.add(Arrays.asList(mediums.get(i), addresses.get(i))); + } + + threePidsParams.threepids = list; + + mApi.bulkLookup(threePidsParams).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + try { + handleBulkLookupResponse(response, addresses, callback); + } catch (IOException e) { + callback.onUnexpectedError(e); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + callback.onUnexpectedError((Exception) t); + } + }); + } + + private void handleBulkLookupResponse( + Response response, + final List addresses, + final ApiCallback> callback + ) throws IOException { + DefaultRetrofit2ResponseHandler.handleResponse( + response, + new DefaultRetrofit2ResponseHandler.Listener() { + @Override + public void onSuccess(Response response) { + handleBulkLookupSuccess(response, addresses, callback); + } + + @Override + public void onHttpError(HttpError httpError) { + callback.onNetworkError(new HttpException(httpError)); + } + } + ); + } + + private void handleBulkLookupSuccess( + Response response, + List addresses, + ApiCallback> callback + ) { + BulkLookupResponse bulkLookupResponse = response.body(); + Map mxidByAddress = new HashMap<>(); + + if (null != bulkLookupResponse.threepids) { + for (int i = 0; i < bulkLookupResponse.threepids.size(); i++) { + List items = bulkLookupResponse.threepids.get(i); + // [0] : medium + // [1] : address + // [2] : matrix id + mxidByAddress.put(items.get(1), items.get(2)); + } + } + + List matrixIds = new ArrayList<>(); + + for (String address : addresses) { + if (mxidByAddress.containsKey(address)) { + matrixIds.add(mxidByAddress.get(address)); + } else { + matrixIds.add(""); + } + } + + callback.onSuccess(matrixIds); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/UrlPostTask.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/UrlPostTask.java new file mode 100644 index 0000000000..64a2d99a03 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/client/UrlPostTask.java @@ -0,0 +1,140 @@ +/* + * 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.rest.client; + +import android.os.AsyncTask; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import im.vector.matrix.android.internal.legacy.RestClient; +import im.vector.matrix.android.internal.legacy.util.Log; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; + +/** + * UrlPostTask triggers a POST with no param. + */ +public class UrlPostTask extends AsyncTask { + + public interface IPostTaskListener { + /** + * The post succeeds. + * + * @param object the object retrieves in the response. + */ + void onSucceed(JsonObject object); + + /** + * The post failed + * + * @param errorMessage thr error message + */ + void onError(String errorMessage); + } + + private static final String LOG_TAG = "UrlPostTask"; + + // the post listener + private IPostTaskListener mListener; + + @Override + protected String doInBackground(String... params) { + String result = ""; + + try { + URL url = new URL(params[0]); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + if (RestClient.getUserAgent() != null) { + conn.setRequestProperty("User-Agent", RestClient.getUserAgent()); + } + conn.setRequestMethod("POST"); + + InputStream is = new BufferedInputStream(conn.getInputStream()); + + if (is != null) { + result = convertStreamToString(is); + } + } catch (Exception e) { + // Do something about exceptions + result = e.getMessage(); + } + return result; + } + + /** + * Set the post listener + * + * @param listener the listener + */ + public void setListener(IPostTaskListener listener) { + mListener = listener; + } + + /** + * Convert a stream to a string + * + * @param is the input stream to convert + * @return the string + */ + private static String convertStreamToString(InputStream is) { + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + StringBuilder sb = new StringBuilder(); + + String line; + try { + while ((line = reader.readLine()) != null) { + sb.append(line + "\n"); + } + } catch (Exception e) { + Log.e(LOG_TAG, "convertStreamToString " + e.getMessage(), e); + } finally { + try { + is.close(); + } catch (Exception e) { + Log.e(LOG_TAG, "convertStreamToString finally failed " + e.getMessage(), e); + } + } + return sb.toString(); + } + + protected void onPostExecute(String result) { + JsonObject object = null; + + Log.d(LOG_TAG, "onPostExecute " + result); + + try { + JsonParser parser = new JsonParser(); + object = parser.parse(result).getAsJsonObject(); + } catch (Exception e) { + Log.e(LOG_TAG, "## onPostExecute() failed" + e.getMessage(), e); + } + + if (null != mListener) { + if (null != object) { + mListener.onSucceed(object); + } else { + mListener.onError(result); + } + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/json/BooleanDeserializer.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/json/BooleanDeserializer.java new file mode 100644 index 0000000000..cf00509ced --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/json/BooleanDeserializer.java @@ -0,0 +1,96 @@ +/* + * 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.rest.json; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; + +import im.vector.matrix.android.internal.legacy.util.Log; + +import java.lang.reflect.Type; + +/** + * Convenient JsonDeserializer to accept various type of Boolean + */ +public class BooleanDeserializer implements JsonDeserializer { + + private static final String LOG_TAG = BooleanDeserializer.class.getSimpleName(); + + private final boolean mCanReturnNull; + + /** + * Constructor + * + * @param canReturnNull true if the deserializer can return null in case of error + */ + public BooleanDeserializer(boolean canReturnNull) { + mCanReturnNull = canReturnNull; + } + + /** + * @param json The Json data being deserialized + * @param typeOfT The type of the Object to deserialize to + * @param context not used + * @return true if json is: true, 1, "true" or "1". false for other values. null in other cases. + * @throws JsonParseException + */ + @Override + public Boolean deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (json.isJsonPrimitive()) { + JsonPrimitive jsonPrimitive = json.getAsJsonPrimitive(); + + if (jsonPrimitive.isBoolean()) { + // Nominal case + return jsonPrimitive.getAsBoolean(); + } else if (jsonPrimitive.isNumber()) { + Log.w(LOG_TAG, "Boolean detected as a number"); + return jsonPrimitive.getAsInt() == 1; + } else if (jsonPrimitive.isString()) { + Log.w(LOG_TAG, "Boolean detected as a string"); + + String jsonPrimitiveString = jsonPrimitive.getAsString(); + return "1".equals(jsonPrimitiveString) + || "true".equals(jsonPrimitiveString); + } else { + // Should not happen + Log.e(LOG_TAG, "Unknown primitive"); + if (mCanReturnNull) { + return null; + } else { + return false; + } + } + } else if (json.isJsonNull()) { + if (mCanReturnNull) { + return null; + } else { + Log.w(LOG_TAG, "Boolean is null, but not allowed to return null"); + return false; + } + } + + Log.w(LOG_TAG, "Boolean detected as not a primitive type"); + if (mCanReturnNull) { + return null; + } else { + return false; + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/json/ConditionDeserializer.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/json/ConditionDeserializer.java new file mode 100644 index 0000000000..7739d7e5ae --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/json/ConditionDeserializer.java @@ -0,0 +1,74 @@ +/* + * 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.rest.json; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import im.vector.matrix.android.internal.legacy.rest.model.bingrules.Condition; +import im.vector.matrix.android.internal.legacy.rest.model.bingrules.ContainsDisplayNameCondition; +import im.vector.matrix.android.internal.legacy.rest.model.bingrules.DeviceCondition; +import im.vector.matrix.android.internal.legacy.rest.model.bingrules.EventMatchCondition; +import im.vector.matrix.android.internal.legacy.rest.model.bingrules.RoomMemberCountCondition; +import im.vector.matrix.android.internal.legacy.rest.model.bingrules.SenderNotificationPermissionCondition; +import im.vector.matrix.android.internal.legacy.rest.model.bingrules.UnknownCondition; +import im.vector.matrix.android.internal.legacy.util.Log; + +import java.lang.reflect.Type; + +public class ConditionDeserializer implements JsonDeserializer { + private static final String LOG_TAG = ConditionDeserializer.class.getSimpleName(); + + @Override + public Condition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + Condition condition = null; + + JsonObject jsonObject = json.getAsJsonObject(); + JsonElement kindElement = jsonObject.get("kind"); + + if (null != kindElement) { + String kind = kindElement.getAsString(); + + if (null != kind) { + switch (kind) { + case Condition.KIND_EVENT_MATCH: + condition = context.deserialize(json, EventMatchCondition.class); + break; + case Condition.KIND_DEVICE: + condition = context.deserialize(json, DeviceCondition.class); + break; + case Condition.KIND_CONTAINS_DISPLAY_NAME: + condition = context.deserialize(json, ContainsDisplayNameCondition.class); + break; + case Condition.KIND_ROOM_MEMBER_COUNT: + condition = context.deserialize(json, RoomMemberCountCondition.class); + break; + case Condition.KIND_SENDER_NOTIFICATION_PERMISSION: + condition = context.deserialize(json, SenderNotificationPermissionCondition.class); + break; + default: + Log.e(LOG_TAG, "## deserialize() : unsupported kind " + kind + " with value " + json); + condition = context.deserialize(json, UnknownCondition.class); + break; + } + } + } + return condition; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/json/MatrixFieldNamingStrategy.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/json/MatrixFieldNamingStrategy.java new file mode 100644 index 0000000000..c6c5db2df9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/json/MatrixFieldNamingStrategy.java @@ -0,0 +1,58 @@ +/* + * 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.rest.json; + +import com.google.gson.FieldNamingStrategy; + +import java.lang.reflect.Field; +import java.util.Locale; + +/** + * Based on FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES. + * toLowerCase() is replaced by toLowerCase(Locale.ENGLISH). + * In some languages like turkish, toLowerCase does not provide the expected string. + * e.g _I is not converted to _i. + */ +public class MatrixFieldNamingStrategy implements FieldNamingStrategy { + + /** + * Converts the field name that uses camel-case define word separation into + * separate words that are separated by the provided {@code separatorString}. + */ + private static String separateCamelCase(String name, String separator) { + StringBuilder translation = new StringBuilder(); + for (int i = 0; i < name.length(); i++) { + char character = name.charAt(i); + if (Character.isUpperCase(character) && translation.length() != 0) { + translation.append(separator); + } + translation.append(character); + } + return translation.toString(); + } + + /** + * Translates the field name into its JSON field name representation. + * + * @param f the field object that we are translating + * @return the translated field name. + * @since 1.3 + */ + public String translateName(Field f) { + return separateCamelCase(f.getName(), "_").toLowerCase(Locale.ENGLISH); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/AuthParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/AuthParams.java new file mode 100644 index 0000000000..086424fc74 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/AuthParams.java @@ -0,0 +1,33 @@ +/* + * 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.rest.model; + +import java.util.Map; + +/** + * Class to define the authentication parameters + */ +public class AuthParams { + // + public String type; + + // update password (type = m.login.password) + public String user; + public String password; + + // forget password parameters (type = m.login.email.identity) + public Map threepid_creds; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/BannedUser.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/BannedUser.java new file mode 100644 index 0000000000..cb970ad9a6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/BannedUser.java @@ -0,0 +1,24 @@ +/* + * 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.rest.model; + +/** + * Class to contain a banned user and the reason they were banned. + */ +public class BannedUser { + public String userId; + public String reason; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/BulkLookupParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/BulkLookupParams.java new file mode 100644 index 0000000000..d39d1ae2fe --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/BulkLookupParams.java @@ -0,0 +1,26 @@ +/* + * 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.rest.model; + +import java.util.List; + +/** + * 3 pids request param + */ +public class BulkLookupParams { + // List of [medium, value] + public List> threepids; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/BulkLookupResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/BulkLookupResponse.java new file mode 100644 index 0000000000..27fb21f096 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/BulkLookupResponse.java @@ -0,0 +1,26 @@ +/* + * 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.rest.model; + +import java.util.List; + +/** + * 3 pids request response + */ +public class BulkLookupResponse { + // List of [medium, value, mxid] + public List> threepids; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ChangePasswordParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ChangePasswordParams.java new file mode 100644 index 0000000000..12e7258c49 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ChangePasswordParams.java @@ -0,0 +1,26 @@ +/* + * 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.rest.model; + +/** + * Class to update the password + */ +public class ChangePasswordParams { + // current account information + public AuthParams auth; + // the new password + public String new_password; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ChunkEvents.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ChunkEvents.java new file mode 100644 index 0000000000..f6a02357b2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ChunkEvents.java @@ -0,0 +1,20 @@ +/* + * 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.rest.model; + +public class ChunkEvents extends ChunkResponse { +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ChunkResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ChunkResponse.java new file mode 100644 index 0000000000..97163f85aa --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ChunkResponse.java @@ -0,0 +1,25 @@ +/* + * 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.rest.model; + +import java.util.List; + +/** + * Class representing an API response with start and end tokens and a generically-typed chunk. + */ +public class ChunkResponse { + public List chunk; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ContentResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ContentResponse.java new file mode 100644 index 0000000000..b4c132fffd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ContentResponse.java @@ -0,0 +1,23 @@ +/* + * 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.rest.model; + +public class ContentResponse { + + public String contentUri; + public int w; + public int h; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/CreateRoomParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/CreateRoomParams.java new file mode 100644 index 0000000000..70cb803b2c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/CreateRoomParams.java @@ -0,0 +1,254 @@ +/* + * 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.rest.model; + +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import com.google.gson.annotations.SerializedName; + +import im.vector.matrix.android.internal.legacy.HomeServerConnectionConfig; +import im.vector.matrix.android.internal.legacy.MXPatterns; +import im.vector.matrix.android.internal.legacy.data.RoomState; +import im.vector.matrix.android.internal.legacy.rest.model.pid.Invite3Pid; +import im.vector.matrix.android.internal.legacy.rest.model.pid.ThreePid; +import im.vector.matrix.android.internal.legacy.util.JsonUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class CreateRoomParams { + + public static final String PRESET_PRIVATE_CHAT = "private_chat"; + public static final String PRESET_PUBLIC_CHAT = "public_chat"; + public static final String PRESET_TRUSTED_PRIVATE_CHAT = "trusted_private_chat"; + + /** + * A public visibility indicates that the room will be shown in the published room list. + * A private visibility will hide the room from the published room list. + * Rooms default to private visibility if this key is not included. + * NB: This should not be confused with join_rules which also uses the word public. One of: ["public", "private"] + */ + public String visibility; + + /** + * The desired room alias local part. If this is included, a room alias will be created and mapped to the newly created room. + * The alias will belong on the same homeserver which created the room. + * For example, if this was set to "foo" and sent to the homeserver "example.com" the complete room alias would be #foo:example.com. + */ + @SerializedName("room_alias_name") + public String roomAliasName; + + /** + * If this is included, an m.room.name event will be sent into the room to indicate the name of the room. + * See Room Events for more information on m.room.name. + */ + public String name; + + /** + * If this is included, an m.room.topic event will be sent into the room to indicate the topic for the room. + * See Room Events for more information on m.room.topic. + */ + public String topic; + + /** + * A list of user IDs to invite to the room. + * This will tell the server to invite everyone in the list to the newly created room. + */ + @SerializedName("invite") + public List invitedUserIds; + + /** + * A list of objects representing third party IDs to invite into the room. + */ + @SerializedName("invite_3pid") + public List invite3pids; + + /** + * Extra keys to be added to the content of the m.room.create. + * The server will clobber the following keys: creator. + * Future versions of the specification may allow the server to clobber other keys. + */ + public Object creation_content; + + /** + * A list of state events to set in the new room. + * This allows the user to override the default state events set in the new room. + * The expected format of the state events are an object with type, state_key and content keys set. + * Takes precedence over events set by presets, but gets overriden by name and topic keys. + */ + @SerializedName("initial_state") + public List initialStates; + + /** + * Convenience parameter for setting various default state events based on a preset. Must be either: + * private_chat => join_rules is set to invite. history_visibility is set to shared. + * trusted_private_chat => join_rules is set to invite. history_visibility is set to shared. All invitees are given the same power level as the + * room creator. + * public_chat: => join_rules is set to public. history_visibility is set to shared. One of: ["private_chat", "public_chat", "trusted_private_chat"] + */ + public String preset; + + /** + * This flag makes the server set the is_direct flag on the m.room.member events sent to the users in invite and invite_3pid. + * See Direct Messaging for more information. + */ + @SerializedName("is_direct") + public Boolean isDirect; + + /** + * Add the crypto algorithm to the room creation parameters. + * + * @param algorithm the algorithm + */ + public void addCryptoAlgorithm(String algorithm) { + if (!TextUtils.isEmpty(algorithm)) { + Event algoEvent = new Event(); + algoEvent.type = Event.EVENT_TYPE_MESSAGE_ENCRYPTION; + + Map contentMap = new HashMap<>(); + contentMap.put("algorithm", algorithm); + algoEvent.content = JsonUtils.getGson(false).toJsonTree(contentMap); + + if (null == initialStates) { + initialStates = Arrays.asList(algoEvent); + } else { + initialStates.add(algoEvent); + } + } + } + + /** + * Force the history visibility in the room creation parameters. + * + * @param historyVisibility the expected history visibility, set null to remove any existing value. + * see {@link RoomState#HISTORY_VISIBILITY_INVITED}, + * {@link RoomState#HISTORY_VISIBILITY_JOINED}, + * {@link RoomState#HISTORY_VISIBILITY_SHARED}, + * {@link RoomState#HISTORY_VISIBILITY_WORLD_READABLE} + */ + public void setHistoryVisibility(@Nullable String historyVisibility) { + if (!TextUtils.isEmpty(historyVisibility)) { + Event historyVisibilityEvent = new Event(); + historyVisibilityEvent.type = Event.EVENT_TYPE_STATE_HISTORY_VISIBILITY; + + Map contentMap = new HashMap<>(); + contentMap.put("history_visibility", historyVisibility); + historyVisibilityEvent.content = JsonUtils.getGson(false).toJsonTree(contentMap); + + if (null == initialStates) { + initialStates = Arrays.asList(historyVisibilityEvent); + } else { + initialStates.add(historyVisibilityEvent); + } + } else if (!initialStates.isEmpty()) { + final List newInitialStates = new ArrayList<>(); + for (Event event : initialStates) { + if (!event.type.equals(Event.EVENT_TYPE_STATE_HISTORY_VISIBILITY)) { + newInitialStates.add(event); + } + } + initialStates = newInitialStates; + } + } + + /** + * Mark as a direct message room. + */ + public void setDirectMessage() { + preset = CreateRoomParams.PRESET_TRUSTED_PRIVATE_CHAT; + isDirect = true; + } + + /** + * @return the invite count + */ + private int getInviteCount() { + return (null == invitedUserIds) ? 0 : invitedUserIds.size(); + } + + /** + * @return the pid invite count + */ + private int getInvite3PidCount() { + return (null == invite3pids) ? 0 : invite3pids.size(); + } + + /** + * Tells if the created room can be a direct chat one. + * + * @return true if it is a direct chat + */ + public boolean isDirect() { + return TextUtils.equals(preset, CreateRoomParams.PRESET_TRUSTED_PRIVATE_CHAT) + && (null != isDirect) + && isDirect + && (1 == getInviteCount() || (1 == getInvite3PidCount())); + } + + /** + * @return the first invited user id + */ + public String getFirstInvitedUserId() { + if (0 != getInviteCount()) { + return invitedUserIds.get(0); + } + + if (0 != getInvite3PidCount()) { + return invite3pids.get(0).address; + } + + return null; + } + + /** + * Add some ids to the room creation + * ids might be a matrix id or an email address. + * + * @param ids the participant ids to add. + */ + public void addParticipantIds(HomeServerConnectionConfig hsConfig, List ids) { + for (String id : ids) { + if (android.util.Patterns.EMAIL_ADDRESS.matcher(id).matches()) { + if (null == invite3pids) { + invite3pids = new ArrayList<>(); + } + + Invite3Pid pid = new Invite3Pid(); + pid.id_server = hsConfig.getIdentityServerUri().getHost(); + pid.medium = ThreePid.MEDIUM_EMAIL; + pid.address = id; + + invite3pids.add(pid); + } else if (MXPatterns.isUserId(id)) { + // do not invite oneself + if (!TextUtils.equals(hsConfig.getCredentials().userId, id)) { + if (null == invitedUserIds) { + invitedUserIds = new ArrayList<>(); + } + + invitedUserIds.add(id); + } + + } // TODO add phonenumbers when it will be available + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/CreateRoomResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/CreateRoomResponse.java new file mode 100644 index 0000000000..0f8516c0ac --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/CreateRoomResponse.java @@ -0,0 +1,24 @@ +/* + * 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.rest.model; + +import com.google.gson.annotations.SerializedName; + +public class CreateRoomResponse { + @SerializedName("room_id") + public String roomId; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/CreatedEvent.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/CreatedEvent.java new file mode 100644 index 0000000000..7b979dabaf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/CreatedEvent.java @@ -0,0 +1,24 @@ +/* + * 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.rest.model; + +import com.google.gson.annotations.SerializedName; + +public class CreatedEvent { + @SerializedName("event_id") + public String eventId; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/DeactivateAccountParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/DeactivateAccountParams.java new file mode 100644 index 0000000000..0bf1454c4a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/DeactivateAccountParams.java @@ -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.rest.model; + +public class DeactivateAccountParams { + + // Auth params + public AuthParams auth; + + // Set to true to erase all data of the account + public boolean erase; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/EncryptedMediaScanBody.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/EncryptedMediaScanBody.java new file mode 100644 index 0000000000..1bb60c0dcc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/EncryptedMediaScanBody.java @@ -0,0 +1,29 @@ +/* + * 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.rest.model; + +import com.google.gson.annotations.SerializedName; + +import im.vector.matrix.android.internal.legacy.rest.model.crypto.EncryptedFileInfo; + +/** + * Class to prepare the request body used to scan an encrypted content. + */ +public class EncryptedMediaScanBody { + // The encryption information used to decrypt the content before scanning it + @SerializedName("file") + public EncryptedFileInfo encryptedFileInfo; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/EncryptedMediaScanEncryptedBody.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/EncryptedMediaScanEncryptedBody.java new file mode 100644 index 0000000000..2897e40256 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/EncryptedMediaScanEncryptedBody.java @@ -0,0 +1,29 @@ +/* + * 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.rest.model; + +import com.google.gson.annotations.SerializedName; + +import im.vector.matrix.android.internal.legacy.rest.model.crypto.EncryptedBodyFileInfo; + +/** + * Class to prepare the request body used to scan an encrypted content. + */ +public class EncryptedMediaScanEncryptedBody { + // The encrypted encryption information used to decrypt the content before scanning it + @SerializedName("encrypted_body") + public EncryptedBodyFileInfo encryptedBodyFileInfo; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/Event.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/Event.java new file mode 100644 index 0000000000..71d9deef20 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/Event.java @@ -0,0 +1,1339 @@ +/* + * 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.rest.model; + +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.annotations.SerializedName; + +import im.vector.matrix.android.internal.legacy.crypto.MXCryptoError; +import im.vector.matrix.android.internal.legacy.crypto.MXEventDecryptionResult; +import im.vector.matrix.android.internal.legacy.db.MXMediasCache; +import im.vector.matrix.android.internal.legacy.rest.model.crypto.EncryptedFileInfo; +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.LocationMessage; +import im.vector.matrix.android.internal.legacy.rest.model.message.Message; +import im.vector.matrix.android.internal.legacy.rest.model.message.StickerMessage; +import im.vector.matrix.android.internal.legacy.rest.model.message.VideoMessage; +import im.vector.matrix.android.internal.legacy.util.JsonUtils; +import im.vector.matrix.android.internal.legacy.util.Log; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; + +/** + * Generic event class with all possible fields for events. + */ +public class Event implements Externalizable { + private static final String LOG_TAG = Event.class.getSimpleName(); + + private static final long serialVersionUID = -1431845331022808337L; + + public enum SentState { + // the event has not been sent + UNSENT, + // the event is encrypting + ENCRYPTING, + // the event is currently sending + SENDING, + // the event is going to be resent asap + WAITING_RETRY, + // the event has been sent + SENT, + // The event failed to be sent + UNDELIVERED, + // the event failed to be sent because some unknown devices have been found while encrypting it + FAILED_UNKNOWN_DEVICES + } + + // when there is no more message to be paginated in a room + // the server returns a null token. + // defines by a non null one to be able to store it. + public static final String PAGINATE_BACK_TOKEN_END = "PAGINATE_BACK_TOKEN_END"; + + public static final String EVENT_TYPE_PRESENCE = "m.presence"; + public static final String EVENT_TYPE_MESSAGE = "m.room.message"; + public static final String EVENT_TYPE_STICKER = "m.sticker"; + public static final String EVENT_TYPE_MESSAGE_ENCRYPTED = "m.room.encrypted"; + public static final String EVENT_TYPE_MESSAGE_ENCRYPTION = "m.room.encryption"; + public static final String EVENT_TYPE_FEEDBACK = "m.room.message.feedback"; + public static final String EVENT_TYPE_TYPING = "m.typing"; + public static final String EVENT_TYPE_REDACTION = "m.room.redaction"; + public static final String EVENT_TYPE_RECEIPT = "m.receipt"; + public static final String EVENT_TYPE_TAGS = "m.tag"; + public static final String EVENT_TYPE_ROOM_KEY = "m.room_key"; + public static final String EVENT_TYPE_READ_MARKER = "m.fully_read"; + public static final String EVENT_TYPE_ROOM_PLUMBING = "m.room.plumbing"; + public static final String EVENT_TYPE_ROOM_BOT_OPTIONS = "m.room.bot.options"; + public static final String EVENT_TYPE_ROOM_KEY_REQUEST = "m.room_key_request"; + public static final String EVENT_TYPE_FORWARDED_ROOM_KEY = "m.forwarded_room_key"; + public static final String EVENT_TYPE_URL_PREVIEW = "org.matrix.room.preview_urls"; + + // State events + public static final String EVENT_TYPE_STATE_ROOM_NAME = "m.room.name"; + public static final String EVENT_TYPE_STATE_ROOM_TOPIC = "m.room.topic"; + public static final String EVENT_TYPE_STATE_ROOM_AVATAR = "m.room.avatar"; + public static final String EVENT_TYPE_STATE_ROOM_MEMBER = "m.room.member"; + public static final String EVENT_TYPE_STATE_ROOM_THIRD_PARTY_INVITE = "m.room.third_party_invite"; + public static final String EVENT_TYPE_STATE_ROOM_CREATE = "m.room.create"; + public static final String EVENT_TYPE_STATE_ROOM_JOIN_RULES = "m.room.join_rules"; + public static final String EVENT_TYPE_STATE_ROOM_GUEST_ACCESS = "m.room.guest_access"; + public static final String EVENT_TYPE_STATE_ROOM_POWER_LEVELS = "m.room.power_levels"; + public static final String EVENT_TYPE_STATE_ROOM_ALIASES = "m.room.aliases"; + public static final String EVENT_TYPE_STATE_ROOM_TOMBSTONE = "m.room.tombstone"; + public static final String EVENT_TYPE_STATE_CANONICAL_ALIAS = "m.room.canonical_alias"; + public static final String EVENT_TYPE_STATE_HISTORY_VISIBILITY = "m.room.history_visibility"; + public static final String EVENT_TYPE_STATE_RELATED_GROUPS = "m.room.related_groups"; + public static final String EVENT_TYPE_STATE_PINNED_EVENT = "m.room.pinned_events"; + + // call events + public static final String EVENT_TYPE_CALL_INVITE = "m.call.invite"; + public static final String EVENT_TYPE_CALL_CANDIDATES = "m.call.candidates"; + public static final String EVENT_TYPE_CALL_ANSWER = "m.call.answer"; + public static final String EVENT_TYPE_CALL_HANGUP = "m.call.hangup"; + + public static final long DUMMY_EVENT_AGE = Long.MAX_VALUE - 1; + + /** + * Type of the event + * Warning, consider using {@link #getType()} to get the type of the unencrypted event + */ + public String type; + + public transient JsonElement content = null; + private String contentAsString = null; + + public transient JsonElement prev_content = null; + private String prev_content_as_string = null; + + public String eventId; + public String roomId; + // former Sync V1 sender name + public String userId; + // Sync V2 sender name + public String sender; + public long originServerTs; + public Long age; + + // Specific to state events + @SerializedName("state_key") + public String stateKey; + + // Contains optional extra information about the event. + public UnsignedData unsigned; + + // Specific to redaction + public String redacts; + + // A subset of the state of the room at the time of the invite, if membership is invite + public List invite_room_state; + + // store the exception triggered when unsent + public Exception unsentException = null; + public MatrixError unsentMatrixError = null; + + // sent state + public SentState mSentState; + + // save the token to back paginate + // the room history could have been reduced to save memory. + // so store the token from each event. + public String mToken; + + // The file cache uses the token as a pagination marker. + // When the user paginates, the file cache paginate until to find X events or an event with a token. + // This token must be used to perform a server catchup. + public boolean mIsInternalPaginationToken; + + // store the linked matrix id + private String mMatrixId; + + // the time raw offset (time zone management) + private long mTimeZoneRawOffset = 0; + + private long getTimeZoneOffset() { + return TimeZone.getDefault().getRawOffset(); + } + + /** + * Default constructor + */ + public Event() { + type = null; + content = null; + prev_content = null; + mIsInternalPaginationToken = false; + + userId = roomId = eventId = null; + originServerTs = 0; + age = null; + + mTimeZoneRawOffset = getTimeZoneOffset(); + + stateKey = null; + redacts = null; + + unsentMatrixError = null; + unsentException = null; + + mMatrixId = null; + + mSentState = SentState.SENT; + } + + /** + * @return the sender + */ + public String getSender() { + return (null == sender) ? userId : sender; + } + + /** + * Update the sender + * + * @param aSender the new sender + */ + public void setSender(String aSender) { + sender = userId = aSender; + } + + /** + * Update the matrix Id. + * + * @param aMatrixId the new matrix id. + */ + public void setMatrixId(String aMatrixId) { + mMatrixId = aMatrixId; + } + + /** + * @return the matrix id. + */ + public String getMatrixId() { + return mMatrixId; + } + + static final long MAX_ORIGIN_SERVER_TS = 1L << 50L; + + /** + * @return true if originServerTs is valid. + */ + public boolean isValidOriginServerTs() { + return originServerTs < MAX_ORIGIN_SERVER_TS; + } + + /** + * @return the originServerTs. + */ + public long getOriginServerTs() { + return originServerTs; + } + + /** + * Update the event content. + * + * @param newContent the new content. + */ + public void updateContent(JsonElement newContent) { + content = newContent; + contentAsString = null; + } + + /** + * @return true if content has some entries + */ + public boolean hasContentFields() { + boolean res = false; + JsonObject json = getContentAsJsonObject(); + + if (null != json) { + Set> entries = getContentAsJsonObject().entrySet(); + + res = (null != entries) && (0 != entries.size()); + } + return res; + } + + /** + * @return true if this event was redacted + */ + public boolean isRedacted() { + return (null != unsigned) && (null != unsigned.redacted_because); + } + + static DateFormat mDateFormat = null; + static long mFormatterRawOffset = 1234; + + /** + * @return a formatted timestamp. + */ + public String formattedOriginServerTs() { + // avoid displaying weird origin ts + if (!isValidOriginServerTs()) { + return " "; + } else { + // the formatter must be updated if the timezone has been updated + // else the formatted string are wrong (does not use the current timezone) + if ((null == mDateFormat) || (mFormatterRawOffset != getTimeZoneOffset())) { + mDateFormat = new SimpleDateFormat("MMM d HH:mm", Locale.getDefault()); + mFormatterRawOffset = getTimeZoneOffset(); + } + + return mDateFormat.format(new Date(getOriginServerTs())); + } + } + + /** + * Update the originServerTs. + * + * @param anOriginServer the new originServerTs. + */ + public void setOriginServerTs(long anOriginServer) { + originServerTs = anOriginServer; + } + + /** + * @return the event type + */ + public String getType() { + if (null != mClearEvent) { + return mClearEvent.type; + } else { + return type; + } + } + + /** + * Update the event type + * + * @param aType the new type + */ + public void setType(String aType) { + // TODO manage encryption + type = aType; + } + + /** + * @return the wire event type + */ + public String getWireType() { + return type; + } + + /** + * @return the event content + */ + public JsonElement getContent() { + if (null != mClearEvent) { + return mClearEvent.getWireContent(); + } else { + return getWireContent(); + } + } + + /** + * @return the wired event content + */ + public JsonElement getWireContent() { + finalizeDeserialization(); + return content; + } + + /** + * @return a Json representation of the event + */ + public JsonObject toJsonObject() { + finalizeDeserialization(); + return JsonUtils.toJson(this); + } + + /** + * @return the content casted as JsonObject. + */ + @Nullable + public JsonObject getContentAsJsonObject() { + JsonElement cont = getContent(); + + if (null != cont && cont.isJsonObject()) { + return cont.getAsJsonObject(); + } + return null; + } + + /** + * @return the prev_content casted as JsonObject. + */ + public JsonObject getPrevContentAsJsonObject() { + finalizeDeserialization(); + + if ((null != unsigned) && (null != unsigned.prev_content)) { + // avoid getting two value for the same thing + if (null == prev_content) { + prev_content = unsigned.prev_content; + } + unsigned.prev_content = null; + } + + if ((null != prev_content) && prev_content.isJsonObject()) { + return prev_content.getAsJsonObject(); + } + return null; + } + + /** + * @return the content formatted as EventContent. + */ + public EventContent getEventContent() { + if (null != getContent()) { + return JsonUtils.toEventContent(getContent()); + } + return null; + } + + /** + * @return the content formatted as EventContent. + */ + public EventContent getWireEventContent() { + if (null != getWireContent()) { + return JsonUtils.toEventContent(getWireContent()); + } + return null; + } + + /** + * @return the content formatted as EventContent. + */ + public EventContent getPrevContent() { + if (null != getPrevContentAsJsonObject()) { + return JsonUtils.toEventContent(getPrevContentAsJsonObject()); + } + return null; + } + + /** + * @return the event age. + */ + public long getAge() { + if (null != age) { + return age; + } else if ((null != unsigned) && (null != unsigned.age)) { + age = unsigned.age; + return age; + } + + return Long.MAX_VALUE; + } + + /** + * @return the redacted event id. + */ + @Nullable + public String getRedactedEventId() { + if (null != redacts) { + return redacts; + } else if (isRedacted()) { + redacts = unsigned.redacted_because.redacts; + return redacts; + } + + return null; + } + + /** + * Create an event from a message. + * + * @param message the event content + * @param anUserId the event user Id + * @param aRoomId the vent room Id + */ + public Event(Message message, String anUserId, String aRoomId) { + type = Event.EVENT_TYPE_MESSAGE; + content = JsonUtils.toJson(message); + originServerTs = System.currentTimeMillis(); + sender = userId = anUserId; + roomId = aRoomId; + mSentState = Event.SentState.UNSENT; + createDummyEventId(); + } + + /** + * Create an event from a content and a type. + * + * @param aType the event type + * @param aContent the event content + * @param anUserId the event user Id + * @param aRoomId the vent room Id + */ + public Event(String aType, JsonObject aContent, String anUserId, String aRoomId) { + type = aType; + content = aContent; + originServerTs = System.currentTimeMillis(); + sender = userId = anUserId; + roomId = aRoomId; + mSentState = Event.SentState.UNSENT; + createDummyEventId(); + } + + /** + * Some events are not sent by the server. + * They are temporary stored until to get the server response. + */ + public void createDummyEventId() { + eventId = roomId + "-" + originServerTs; + age = DUMMY_EVENT_AGE; + } + + /** + * @return true if the event is a dummy id i.e this event has been created with createDummyEventId. + */ + public boolean isDummyEvent() { + return (roomId + "-" + originServerTs).equals(eventId); + } + + /** + * Update the pagination token. + * + * @param token the new token. + */ + public void setInternalPaginationToken(String token) { + mToken = token; + mIsInternalPaginationToken = true; + } + + /** + * @return true if the token has been set by setInternalPaginationToken. + */ + public boolean isInternalPaginationToken() { + return mIsInternalPaginationToken; + } + + /** + * @return true if the event has a token. + */ + public boolean hasToken() { + return (null != mToken) && !mIsInternalPaginationToken; + } + + /** + * @return true if the event if a call event. + */ + public boolean isCallEvent() { + return EVENT_TYPE_CALL_INVITE.equals(getType()) + || EVENT_TYPE_CALL_CANDIDATES.equals(getType()) + || EVENT_TYPE_CALL_ANSWER.equals(getType()) + || EVENT_TYPE_CALL_HANGUP.equals(getType()); + } + + /** + * Make a deep copy of this room state object. + * + * @return the copy + */ + public Event deepCopy() { + finalizeDeserialization(); + + Event copy = new Event(); + copy.type = type; + copy.content = content; + copy.contentAsString = contentAsString; + + copy.eventId = eventId; + copy.roomId = roomId; + copy.userId = userId; + copy.sender = sender; + copy.originServerTs = originServerTs; + copy.mTimeZoneRawOffset = mTimeZoneRawOffset; + copy.age = age; + + copy.stateKey = stateKey; + copy.prev_content = prev_content; + copy.prev_content_as_string = prev_content_as_string; + + copy.unsigned = unsigned; + copy.invite_room_state = invite_room_state; + copy.redacts = redacts; + + copy.mSentState = mSentState; + + copy.unsentException = unsentException; + copy.unsentMatrixError = unsentMatrixError; + + copy.mMatrixId = mMatrixId; + copy.mToken = mToken; + copy.mIsInternalPaginationToken = mIsInternalPaginationToken; + + return copy; + } + + /** + * Check if the current event can resent. + * + * @return true if it can be resent. + */ + public boolean canBeResent() { + return (mSentState == SentState.WAITING_RETRY) || (mSentState == SentState.UNDELIVERED) || (mSentState == SentState.FAILED_UNKNOWN_DEVICES); + } + + /** + * Check if the current event is encrypting. + * + * @return true if the message encryption is in progress. + */ + public boolean isEncrypting() { + return (mSentState == SentState.ENCRYPTING); + } + + /** + * Check if the current event is unsent. + * + * @return true if it is unsent. + */ + public boolean isUnsent() { + return (mSentState == SentState.UNSENT); + } + + /** + * Check if the current event is sending. + * + * @return true if it is sending. + */ + public boolean isSending() { + return (mSentState == SentState.SENDING) || (mSentState == SentState.WAITING_RETRY); + } + + /** + * Tell if the message sending failed + * + * @return true if the event has not been sent because of a failure + */ + public boolean isUndelivered() { + return (mSentState == SentState.UNDELIVERED); + } + + /** + * Tells if the message sending failed because some unknown devices have been detected. + * + * @return true if some unknown devices have been detected. + */ + public boolean isUnknownDevice() { + return (mSentState == SentState.FAILED_UNKNOWN_DEVICES); + } + + /** + * Check if the current event is sent. + * + * @return true if it is sent. + */ + public boolean isSent() { + return (mSentState == SentState.SENT); + } + + /** + * @return the media URLs defined in the event. + */ + public List getMediaUrls() { + List urls = new ArrayList<>(); + + if (Event.EVENT_TYPE_MESSAGE.equals(getType())) { + String msgType = JsonUtils.getMessageMsgType(getContent()); + + if (Message.MSGTYPE_IMAGE.equals(msgType)) { + ImageMessage imageMessage = JsonUtils.toImageMessage(getContent()); + + if (null != imageMessage.getUrl()) { + urls.add(imageMessage.getUrl()); + } + if (null != imageMessage.getThumbnailUrl()) { + urls.add(imageMessage.getThumbnailUrl()); + } + } else if (Message.MSGTYPE_FILE.equals(msgType) || Message.MSGTYPE_AUDIO.equals(msgType)) { + FileMessage fileMessage = JsonUtils.toFileMessage(getContent()); + + if (null != fileMessage.getUrl()) { + urls.add(fileMessage.getUrl()); + } + } else if (Message.MSGTYPE_VIDEO.equals(msgType)) { + VideoMessage videoMessage = JsonUtils.toVideoMessage(getContent()); + + if (null != videoMessage.getUrl()) { + urls.add(videoMessage.getUrl()); + } + if (null != videoMessage.getThumbnailUrl()) { + urls.add(videoMessage.getThumbnailUrl()); + } + } else if (Message.MSGTYPE_LOCATION.equals(msgType)) { + LocationMessage locationMessage = JsonUtils.toLocationMessage(getContent()); + + if (null != locationMessage.thumbnail_url) { + urls.add(locationMessage.thumbnail_url); + } + } + } else if (Event.EVENT_TYPE_STICKER.equals(getType())) { + StickerMessage stickerMessage = JsonUtils.toStickerMessage(getContent()); + + if (null != stickerMessage.getUrl()) { + urls.add(stickerMessage.getUrl()); + } + + if (null != stickerMessage.getThumbnailUrl()) { + urls.add(stickerMessage.getThumbnailUrl()); + } + } + + return urls; + } + + /** + * @return all the encrypted file infos defined in the event. + */ + public List getEncryptedFileInfos() { + List encryptedFileInfos = new ArrayList<>(); + + if (!isEncrypted()) { + // return empty array + return encryptedFileInfos; + } + + if (Event.EVENT_TYPE_MESSAGE.equals(getType())) { + String msgType = JsonUtils.getMessageMsgType(getContent()); + + if (Message.MSGTYPE_IMAGE.equals(msgType)) { + ImageMessage imageMessage = JsonUtils.toImageMessage(getContent()); + + if (null != imageMessage.file) { + encryptedFileInfos.add(imageMessage.file); + } + if (null != imageMessage.info && null != imageMessage.info.thumbnail_file) { + encryptedFileInfos.add(imageMessage.info.thumbnail_file); + } + } else if (Message.MSGTYPE_FILE.equals(msgType) || Message.MSGTYPE_AUDIO.equals(msgType)) { + FileMessage fileMessage = JsonUtils.toFileMessage(getContent()); + + if (null != fileMessage.file) { + encryptedFileInfos.add(fileMessage.file); + } + } else if (Message.MSGTYPE_VIDEO.equals(msgType)) { + VideoMessage videoMessage = JsonUtils.toVideoMessage(getContent()); + + if (null != videoMessage.file) { + encryptedFileInfos.add(videoMessage.file); + } + if (null != videoMessage.info && null != videoMessage.info.thumbnail_file) { + encryptedFileInfos.add(videoMessage.info.thumbnail_file); + } + } + } else if (Event.EVENT_TYPE_STICKER.equals(getType())) { + StickerMessage stickerMessage = JsonUtils.toStickerMessage(getContent()); + + if (null != stickerMessage.file) { + encryptedFileInfos.add(stickerMessage.file); + } + if (null != stickerMessage.info && null != stickerMessage.info.thumbnail_file) { + encryptedFileInfos.add(stickerMessage.info.thumbnail_file); + } + } + + return encryptedFileInfos; + } + + /** + * Tells if the current event is uploading a media. + * + * @param mediasCache the media cache + * @return true if the event is uploading a media. + */ + public boolean isUploadingMedias(MXMediasCache mediasCache) { + List urls = getMediaUrls(); + + for (String url : urls) { + if (mediasCache.getProgressValueForUploadId(url) >= 0) { + return true; + } + } + + return false; + } + + /** + * Tells if the current event is downloading a media. + * + * @param mediasCache the media cache + * @return true if the event is downloading a media. + */ + public boolean isDownloadingMedias(MXMediasCache mediasCache) { + List urls = getMediaUrls(); + + for (String url : urls) { + if (mediasCache.getProgressValueForDownloadId(mediasCache.downloadIdFromUrl(url)) >= 0) { + return true; + } + } + + return false; + } + + @Override + public String toString() { + // build the string by hand + String text = "{\n"; + + text += " \"age\" : " + age + ",\n"; + + text += " \"content\": {\n"; + + if (null != getWireContent()) { + if (getWireContent().isJsonArray()) { + for (JsonElement e : getWireContent().getAsJsonArray()) { + text += " " + e.toString() + ",\n"; + } + } else if (getWireContent().isJsonObject()) { + for (Map.Entry e : getWireContent().getAsJsonObject().entrySet()) { + text += " \"" + e.getKey() + "\": " + e.getValue().toString() + ",\n"; + } + } else { + text += getWireContent().toString(); + } + } + + text += " },\n"; + + text += " \"eventId\": \"" + eventId + "\",\n"; + text += " \"originServerTs\": " + originServerTs + ",\n"; + text += " \"roomId\": \"" + roomId + "\",\n"; + text += " \"type\": \"" + type + "\",\n"; + text += " \"userId\": \"" + userId + "\",\n"; + text += " \"sender\": \"" + sender + "\",\n"; + + text += "}"; + + text += "\n\n Sent state : "; + + if (mSentState == SentState.UNSENT) { + text += "UNSENT"; + } else if (mSentState == SentState.SENDING) { + text += "SENDING"; + } else if (mSentState == SentState.WAITING_RETRY) { + text += "WAITING_RETRY"; + } else if (mSentState == SentState.SENT) { + text += "SENT"; + } else if (mSentState == SentState.UNDELIVERED) { + text += "UNDELIVERED"; + } else if (mSentState == SentState.FAILED_UNKNOWN_DEVICES) { + text += "FAILED UNKNOWN DEVICES"; + } + + if (null != unsentException) { + text += "\n\n Exception reason: " + unsentException.getMessage() + "\n"; + } + + if (null != unsentMatrixError) { + text += "\n\n Matrix reason: " + unsentMatrixError.getLocalizedMessage() + "\n"; + } + + return text; + } + + @Override + public void readExternal(ObjectInput input) throws IOException, ClassNotFoundException { + if (input.readBoolean()) { + type = input.readUTF(); + } + + if (input.readBoolean()) { + contentAsString = input.readUTF(); + } + + if (input.readBoolean()) { + prev_content_as_string = input.readUTF(); + } + + if (input.readBoolean()) { + eventId = input.readUTF(); + } + + if (input.readBoolean()) { + roomId = input.readUTF(); + } + + if (input.readBoolean()) { + userId = input.readUTF(); + } + + if (input.readBoolean()) { + sender = input.readUTF(); + } + + originServerTs = input.readLong(); + + if (input.readBoolean()) { + age = input.readLong(); + } + + if (input.readBoolean()) { + stateKey = input.readUTF(); + } + + if (input.readBoolean()) { + unsigned = (UnsignedData) input.readObject(); + } + + if (input.readBoolean()) { + redacts = input.readUTF(); + } + + if (input.readBoolean()) { + invite_room_state = (List) input.readObject(); + } + + if (input.readBoolean()) { + unsentException = (Exception) input.readObject(); + } + + if (input.readBoolean()) { + unsentMatrixError = (MatrixError) input.readObject(); + } + + mSentState = (SentState) input.readObject(); + + if (input.readBoolean()) { + mToken = input.readUTF(); + } + + mIsInternalPaginationToken = input.readBoolean(); + + if (input.readBoolean()) { + mMatrixId = input.readUTF(); + } + + mTimeZoneRawOffset = input.readLong(); + } + + @Override + public void writeExternal(ObjectOutput output) throws IOException { + prepareSerialization(); + + output.writeBoolean(null != type); + if (null != type) { + output.writeUTF(type); + } + + output.writeBoolean(null != contentAsString); + if (null != contentAsString) { + output.writeUTF(contentAsString); + } + + output.writeBoolean(null != prev_content_as_string); + if (null != prev_content_as_string) { + output.writeUTF(prev_content_as_string); + } + + output.writeBoolean(null != eventId); + if (null != eventId) { + output.writeUTF(eventId); + } + + output.writeBoolean(null != roomId); + if (null != roomId) { + output.writeUTF(roomId); + } + + output.writeBoolean(null != userId); + if (null != userId) { + output.writeUTF(userId); + } + + output.writeBoolean(null != sender); + if (null != sender) { + output.writeUTF(sender); + } + + output.writeLong(originServerTs); + + output.writeBoolean(null != age); + if (null != age) { + output.writeLong(age); + } + + output.writeBoolean(null != stateKey); + if (null != stateKey) { + output.writeUTF(stateKey); + } + + output.writeBoolean(null != unsigned); + if (null != unsigned) { + output.writeObject(unsigned); + } + + output.writeBoolean(null != redacts); + if (null != redacts) { + output.writeUTF(redacts); + } + + output.writeBoolean(null != invite_room_state); + if (null != invite_room_state) { + output.writeObject(invite_room_state); + } + + output.writeBoolean(null != unsentException); + if (null != unsentException) { + output.writeObject(unsentException); + } + + output.writeBoolean(null != unsentMatrixError); + if (null != unsentMatrixError) { + output.writeObject(unsentMatrixError); + } + + output.writeObject(mSentState); + + output.writeBoolean(null != mToken); + if (null != mToken) { + output.writeUTF(mToken); + } + + output.writeBoolean(mIsInternalPaginationToken); + + output.writeBoolean(null != mMatrixId); + if (null != mMatrixId) { + output.writeUTF(mMatrixId); + } + + output.writeLong(mTimeZoneRawOffset); + } + + /** + * Init some internal fields to serialize the event. + */ + private void prepareSerialization() { + if ((null != content) && (null == contentAsString)) { + contentAsString = content.toString(); + } + + if ((null != getPrevContentAsJsonObject()) && (null == prev_content_as_string)) { + prev_content_as_string = getPrevContentAsJsonObject().toString(); + } + + if ((null != unsigned) && (null != unsigned.prev_content)) { + unsigned.prev_content = null; + } + } + + /** + * Deserialize the event. + */ + private void finalizeDeserialization() { + if ((null != contentAsString) && (null == content)) { + try { + content = new JsonParser().parse(contentAsString).getAsJsonObject(); + } catch (Exception e) { + Log.e(LOG_TAG, "finalizeDeserialization : contentAsString deserialization " + e.getMessage(), e); + contentAsString = null; + } + } + + if ((null != prev_content_as_string) && (null == prev_content)) { + try { + prev_content = new JsonParser().parse(prev_content_as_string).getAsJsonObject(); + } catch (Exception e) { + Log.e(LOG_TAG, "finalizeDeserialization : prev_content_as_string deserialization " + e.getMessage(), e); + prev_content_as_string = null; + } + } + } + + /** + * Filter a JsonObject to keep only the allowed keys. + * + * @param aContent the JsonObject to filter. + * @param allowedKeys the allowed keys list. + * @return the filtered JsonObject + */ + private static JsonObject filterInContentWithKeys(JsonObject aContent, List allowedKeys) { + // sanity check + if (null == aContent) { + return null; + } + + JsonObject filteredContent = new JsonObject(); + + // remove any key + if ((null == allowedKeys) || (0 == allowedKeys.size())) { + return new JsonObject(); + } + + Set> entries = aContent.entrySet(); + + if (null != entries) { + for (Map.Entry entry : entries) { + if (allowedKeys.indexOf(entry.getKey()) >= 0) { + filteredContent.add(entry.getKey(), entry.getValue()); + } + } + } + + return filteredContent; + } + + /** + * Prune the event which removes all keys we don't know about or think could potentially be dodgy. + * This is used when we "redact" an event. We want to remove all fields that the user has specified, + * but we do want to keep necessary information like type, state_key etc. + * + * @param redactionEvent the event which triggers this redaction + */ + public void prune(Event redactionEvent) { + // Filter in event by keeping only the following keys + List allowedKeys; + + // Add filtered content, allowed keys in content depends on the event type + if (TextUtils.equals(Event.EVENT_TYPE_STATE_ROOM_MEMBER, type)) { + allowedKeys = new ArrayList<>(Arrays.asList("membership")); + } else if (TextUtils.equals(Event.EVENT_TYPE_STATE_ROOM_CREATE, type)) { + allowedKeys = new ArrayList<>(Arrays.asList("creator")); + } else if (TextUtils.equals(Event.EVENT_TYPE_STATE_ROOM_JOIN_RULES, type)) { + allowedKeys = new ArrayList<>(Arrays.asList("join_rule")); + } else if (TextUtils.equals(Event.EVENT_TYPE_STATE_ROOM_POWER_LEVELS, type)) { + allowedKeys = new ArrayList<>(Arrays.asList("users", + "users_default", + "events", + "events_default", + "state_default", + "ban", + "kick", + "redact", + "invite")); + } else if (TextUtils.equals(Event.EVENT_TYPE_STATE_ROOM_ALIASES, type)) { + allowedKeys = new ArrayList<>(Arrays.asList("aliases")); + } else if (TextUtils.equals(Event.EVENT_TYPE_STATE_CANONICAL_ALIAS, type)) { + allowedKeys = new ArrayList<>(Arrays.asList("alias")); + } else if (TextUtils.equals(Event.EVENT_TYPE_FEEDBACK, type)) { + allowedKeys = new ArrayList<>(Arrays.asList("type", "target_event_id")); + } else if (TextUtils.equals(Event.EVENT_TYPE_MESSAGE_ENCRYPTED, type)) { + mClearEvent = null; + allowedKeys = null; + } else { + allowedKeys = null; + } + + content = filterInContentWithKeys(getContentAsJsonObject(), allowedKeys); + prev_content = filterInContentWithKeys(getPrevContentAsJsonObject(), allowedKeys); + + prev_content_as_string = null; + contentAsString = null; + + if (null != redactionEvent) { + if (null == unsigned) { + unsigned = new UnsignedData(); + } + + unsigned.redacted_because = new RedactedBecause(); + unsigned.redacted_because.type = redactionEvent.type; + unsigned.redacted_because.origin_server_ts = redactionEvent.originServerTs; + unsigned.redacted_because.sender = redactionEvent.sender; + unsigned.redacted_because.event_id = redactionEvent.eventId; + unsigned.redacted_because.unsigned = redactionEvent.unsigned; + unsigned.redacted_because.redacts = redactionEvent.redacts; + + unsigned.redacted_because.content = new RedactedContent(); + + JsonObject contentAsJson = getContentAsJsonObject(); + if ((null != contentAsJson) && contentAsJson.has("reason")) { + try { + unsigned.redacted_because.content.reason = contentAsJson.get("reason").getAsString(); + } catch (Exception e) { + Log.e(LOG_TAG, "unsigned.redacted_because.content.reason failed " + e.getMessage(), e); + } + + } + } + } + + //============================================================================================================== + // Crypto + //============================================================================================================== + + /** + * For encrypted events, the plaintext payload for the event. + * This is a small MXEvent instance with typically value for `type` and 'content' fields. + */ + private transient Event mClearEvent; + + /** + * Curve25519 key which we believe belongs to the sender of the event. + * See `senderKey` property. + */ + private transient String mSenderCurve25519Key; + + /** + * Ed25519 key which the sender of this event (for olm) or the creator of the megolm session (for megolm) claims to own. + * See `claimedEd25519Key` property. + */ + private transient String mClaimedEd25519Key; + + /** + * Curve25519 keys of devices involved in telling us about the senderCurve25519Key and claimedEd25519Key. + * See `forwardingCurve25519KeyChain` property. + */ + private transient List mForwardingCurve25519KeyChain = new ArrayList<>(); + + /** + * Decryption error + */ + private MXCryptoError mCryptoError; + + /** + * @return true if this event is encrypted. + */ + public boolean isEncrypted() { + return TextUtils.equals(getWireType(), EVENT_TYPE_MESSAGE_ENCRYPTED); + } + + /** + * Update the clear data on this event. + * This is used after decrypting an event; it should not be used by applications. + * It fires kMXEventDidDecryptNotification. + * + * @param decryptionResult the decryption result, including the plaintext and some key info. + */ + public void setClearData(@Nullable MXEventDecryptionResult decryptionResult) { + mClearEvent = null; + + if (null != decryptionResult) { + if (null != decryptionResult.mClearEvent) { + mClearEvent = JsonUtils.toEvent(decryptionResult.mClearEvent); + } + + if (null != mClearEvent) { + mClearEvent.mSenderCurve25519Key = decryptionResult.mSenderCurve25519Key; + mClearEvent.mClaimedEd25519Key = decryptionResult.mClaimedEd25519Key; + + if (null != decryptionResult.mForwardingCurve25519KeyChain) { + mClearEvent.mForwardingCurve25519KeyChain = decryptionResult.mForwardingCurve25519KeyChain; + } else { + mClearEvent.mForwardingCurve25519KeyChain = new ArrayList<>(); + } + + try { + // Add "m.relates_to" data from e2e event to the unencrypted event + if (getWireContent().getAsJsonObject().has("m.relates_to")) { + mClearEvent.getContentAsJsonObject() + .add("m.relates_to", getWireContent().getAsJsonObject().get("m.relates_to")); + } + } catch (Exception e) { + Log.e(LOG_TAG, "Unable to restore 'm.relates_to' the clear event", e); + } + } + + mCryptoError = null; + } + } + + /** + * @return The curve25519 key that sent this event. + */ + public String senderKey() { + if (null != mClearEvent) { + return mClearEvent.mSenderCurve25519Key; + } else { + return mSenderCurve25519Key; + } + } + + /** + * @return The additional keys the sender of this encrypted event claims to possess. + */ + public Map getKeysClaimed() { + Map res = new HashMap<>(); + + String claimedEd25519Key = (null != getClearEvent()) ? getClearEvent().mClaimedEd25519Key : mClaimedEd25519Key; + + if (null != claimedEd25519Key) { + res.put("ed25519", claimedEd25519Key); + } + + return res; + } + + /** + * @return the claimed Ed25519 key + */ + /*public String getClaimedEd25519Key() { + if (null != mClearEvent) { + return mClearEvent.mClaimedEd25519Key; + } else { + return mClaimedEd25519Key; + } + }*/ + + /** + * @return Get the curve25519 keys of the devices which were involved in telling us about the claimedEd25519Key and sender curve25519 key. + */ + /*public List getForwardingCurve25519KeyChain() { + List res = (null != mClearEvent) ? mClearEvent.mForwardingCurve25519KeyChain : mForwardingCurve25519KeyChain; + + if (null == res) { + res = new ArrayList<>(); + } + + return res; + }*/ + + /** + * @return the linked crypto error + */ + public MXCryptoError getCryptoError() { + return mCryptoError; + } + + /** + * Update the linked crypto error + * + * @param error the new crypto error. + */ + public void setCryptoError(MXCryptoError error) { + mCryptoError = error; + if (null != error) { + mClearEvent = null; + } + } + + /** + * @return the clear event + */ + public Event getClearEvent() { + return mClearEvent; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/EventContent.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/EventContent.java new file mode 100644 index 0000000000..ae0540d560 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/EventContent.java @@ -0,0 +1,49 @@ +/* + * 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.rest.model; + +import im.vector.matrix.android.internal.legacy.rest.model.pid.RoomThirdPartyInvite; + +/** + * Class representing an event content + */ +public class EventContent implements java.io.Serializable { + /** + * The display name for this user, if any. + */ + public String displayname; + + /** + * The avatar URL for this user, if any. + */ + public String avatar_url; + + /** + * The membership state of the user. One of: ["invite", "join", "knock", "leave", "ban"] + */ + public String membership; + + /** + * the third party invite + */ + public RoomThirdPartyInvite third_party_invite; + + /* + * e2e encryption format + */ + public String algorithm; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/EventContext.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/EventContext.java new file mode 100644 index 0000000000..8e8a4680cb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/EventContext.java @@ -0,0 +1,56 @@ +/* + * 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.rest.model; + +import java.util.List; + +/** + * represents the response to the /context request. + */ +public class EventContext { + + /** + * The event on which /context has been requested. + */ + public Event event; + + /** + * A token that can be used to paginate backwards with. + */ + public String start; + + /** + * A list of room events that happened just before the requested event. + * The order is anti-chronological. + */ + public List eventsBefore; + + /** + * A list of room events that happened just after the requested event. + * The order is chronological. + */ + public List eventsAfter; + + /** + * A token that can be used to paginate forwards with. + */ + public String end; + + /** + * The state of the room at the last event returned. + */ + public List state; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ForgetPasswordParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ForgetPasswordParams.java new file mode 100644 index 0000000000..f340681abc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ForgetPasswordParams.java @@ -0,0 +1,42 @@ +/* + * 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.rest.model; + +/** + * The forget password params + */ +public class ForgetPasswordParams { + + /** + * The email address + **/ + public String email; + + /** + * Client-generated secret string used to protect this session + **/ + public String client_secret; + + /** + * Used to distinguish protocol level retries from requests to re-send the email. + **/ + public Integer send_attempt; + + /** + * The ID server to send the onward request to as a hostname with an appended colon and port number if the port is not the default. + **/ + public String id_server; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ForgetPasswordResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ForgetPasswordResponse.java new file mode 100644 index 0000000000..46570b3cfb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ForgetPasswordResponse.java @@ -0,0 +1,27 @@ +/* + * 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.rest.model; + +/** + * The forget password response + */ +public class ForgetPasswordResponse { + + /** + * The session id + **/ + public String sid; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/HttpError.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/HttpError.java new file mode 100644 index 0000000000..baeec219c9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/HttpError.java @@ -0,0 +1,44 @@ +package im.vector.matrix.android.internal.legacy.rest.model; + +public final class HttpError { + private final String errorBody; + private final int httpCode; + + public HttpError(String errorBody, int httpCode) { + this.errorBody = errorBody; + this.httpCode = httpCode; + } + + public String getErrorBody() { + return errorBody; + } + + public int getHttpCode() { + return httpCode; + } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + HttpError httpError = (HttpError) o; + + if (httpCode != httpError.httpCode) return false; + return errorBody != null ? + errorBody.equals(httpError.errorBody) : + httpError.errorBody == null; + } + + @Override public int hashCode() { + int result = errorBody != null ? errorBody.hashCode() : 0; + result = 31 * result + httpCode; + return result; + } + + @Override public String toString() { + return "HttpError{" + + "errorBody='" + errorBody + '\'' + + ", httpCode=" + httpCode + + '}'; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/HttpException.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/HttpException.java new file mode 100644 index 0000000000..957d49a932 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/HttpException.java @@ -0,0 +1,14 @@ +package im.vector.matrix.android.internal.legacy.rest.model; + +public class HttpException extends Exception { + + private final HttpError httpError; + + public HttpException(HttpError httpError) { + this.httpError = httpError; + } + + public HttpError getHttpError() { + return httpError; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/Invite.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/Invite.java new file mode 100644 index 0000000000..9105ca7f83 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/Invite.java @@ -0,0 +1,31 @@ +/* + * 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.rest.model; + +/** + * subclass representing a search API response + */ +public class Invite implements java.io.Serializable { + /** + * A name which can be displayed to represent the user instead of their third party identifier. + */ + public String display_name; + + /** + * A block of content which has been signed, which servers can use to verify the event. Clients should ignore this. + */ + public Signed signed; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/MatrixError.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/MatrixError.java new file mode 100644 index 0000000000..ae7fd3e1d7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/MatrixError.java @@ -0,0 +1,160 @@ +/* + * 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.rest.model; + +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import com.google.gson.annotations.SerializedName; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import okhttp3.MediaType; +import okhttp3.ResponseBody; + +/** + * Represents a standard error response. + */ +public class MatrixError implements java.io.Serializable { + public static final String FORBIDDEN = "M_FORBIDDEN"; + public static final String UNKNOWN = "M_UNKNOWN"; + public static final String UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"; + public static final String BAD_JSON = "M_BAD_JSON"; + public static final String NOT_JSON = "M_NOT_JSON"; + public static final String NOT_FOUND = "M_NOT_FOUND"; + public static final String LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"; + public static final String USER_IN_USE = "M_USER_IN_USE"; + public static final String ROOM_IN_USE = "M_ROOM_IN_USE"; + public static final String BAD_PAGINATION = "M_BAD_PAGINATION"; + public static final String UNAUTHORIZED = "M_UNAUTHORIZED"; + public static final String OLD_VERSION = "M_OLD_VERSION"; + public static final String UNRECOGNIZED = "M_UNRECOGNIZED"; + + public static final String LOGIN_EMAIL_URL_NOT_YET = "M_LOGIN_EMAIL_URL_NOT_YET"; + public static final String THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED"; + // Error code returned by the server when no account matches the given 3pid + public static final String THREEPID_NOT_FOUND = "M_THREEPID_NOT_FOUND"; + public static final String THREEPID_IN_USE = "M_THREEPID_IN_USE"; + public static final String SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED"; + public static final String TOO_LARGE = "M_TOO_LARGE"; + public static final String M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN"; + public static final String RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"; + + // custom ones + public static final String NOT_SUPPORTED = "M_NOT_SUPPORTED"; + + // Possible value for "limit_type" + public static final String LIMIT_TYPE_MAU = "monthly_active_user"; + + // Define the configuration error codes. + // The others matrix errors are requests dedicated + // UNKNOWN_TOKEN : the access token is no more valid + // OLD_VERSION : the current SDK / application versions are too old and might trigger some unexpected errors. + public static final Set mConfigurationErrorCodes = new HashSet<>(Arrays.asList(UNKNOWN_TOKEN, OLD_VERSION)); + + public String errcode; + public String error; + public Integer retry_after_ms; + + @SerializedName("consent_uri") + public String consentUri; + + // RESOURCE_LIMIT_EXCEEDED data + @SerializedName("limit_type") + public String limitType; + @Nullable + @SerializedName("admin_contact") + public String adminUri; + + + // extracted from the error response + public Integer mStatus; + public String mReason; + public ResponseBody mErrorBody; + public String mErrorBodyAsString; + public MediaType mErrorBodyMimeType; + + /** + * Default creator + */ + public MatrixError() { + } + + /** + * Creator with error description + * + * @param anErrcode the error code. + * @param anError the error message. + */ + public MatrixError(String anErrcode, String anError) { + errcode = anErrcode; + error = anError; + } + + /** + * @return a localized error message. + */ + public String getLocalizedMessage() { + String localizedMessage = ""; + + if (!TextUtils.isEmpty(error)) { + localizedMessage = error; + } else if (!TextUtils.isEmpty(errcode)) { + localizedMessage = errcode; + } + + return localizedMessage; + } + + /** + * @return a error message. + */ + public String getMessage() { + return getLocalizedMessage(); + } + + /** + * @return true if the error code is a supported one + */ + public boolean isSupportedErrorCode() { + return MatrixError.FORBIDDEN.equals(errcode) + || MatrixError.UNKNOWN_TOKEN.equals(errcode) + || MatrixError.BAD_JSON.equals(errcode) + || MatrixError.NOT_JSON.equals(errcode) + || MatrixError.NOT_FOUND.equals(errcode) + || MatrixError.LIMIT_EXCEEDED.equals(errcode) + || MatrixError.USER_IN_USE.equals(errcode) + || MatrixError.ROOM_IN_USE.equals(errcode) + || MatrixError.TOO_LARGE.equals(errcode) + || MatrixError.BAD_PAGINATION.equals(errcode) + || MatrixError.OLD_VERSION.equals(errcode) + || MatrixError.UNRECOGNIZED.equals(errcode) + || MatrixError.RESOURCE_LIMIT_EXCEEDED.equals(errcode); + } + + /** + * Tells if a matrix error code is a configuration error code. + * + * @param matrixErrorCode the matrix error code + * @return true if it is one + */ + public static boolean isConfigurationErrorCode(String matrixErrorCode) { + return (null != matrixErrorCode) && mConfigurationErrorCodes.contains(matrixErrorCode); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/MediaScanPublicKeyResult.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/MediaScanPublicKeyResult.java new file mode 100644 index 0000000000..1459939985 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/MediaScanPublicKeyResult.java @@ -0,0 +1,27 @@ +/* + * 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.rest.model; + +import com.google.gson.annotations.SerializedName; + +/** + * Class to contain the public key of the media scan server. + */ +public class MediaScanPublicKeyResult { + + @SerializedName("public_key") + public String mCurve25519PublicKey; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/MediaScanResult.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/MediaScanResult.java new file mode 100644 index 0000000000..138e156c58 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/MediaScanResult.java @@ -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.rest.model; + +/** + * Class to contain the anti-virus scan result of a matrix content. + */ +public class MediaScanResult { + // If true, the script ran with an exit code of 0. Otherwise it ran with a non-zero exit code. + public boolean clean; + // Human-readable information about the result. + public String info; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/PowerLevels.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/PowerLevels.java new file mode 100644 index 0000000000..a2be2ee864 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/PowerLevels.java @@ -0,0 +1,169 @@ +/* + * 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.rest.model; + +import android.text.TextUtils; + +import java.util.HashMap; +import java.util.Map; + +public class PowerLevels implements java.io.Serializable { + public int ban = 50; + public int kick = 50; + public int invite = 50; + public int redact = 50; + + public int events_default = 0; + public Map events = new HashMap<>(); + + public int users_default = 0; + public Map users = new HashMap<>(); + + public int state_default = 50; + + public Map notifications = new HashMap<>(); + + public PowerLevels deepCopy() { + PowerLevels copy = new PowerLevels(); + copy.ban = ban; + copy.kick = kick; + copy.invite = invite; + copy.redact = redact; + + copy.events_default = events_default; + copy.events = new HashMap<>(); + copy.events.putAll(events); + + copy.users_default = users_default; + copy.users = new HashMap<>(); + copy.users.putAll(users); + + copy.state_default = state_default; + + copy.notifications = new HashMap<>(notifications); + + return copy; + } + + /** + * Returns the user power level of a dedicated user Id + * + * @param userId the user id + * @return the power level + */ + public int getUserPowerLevel(String userId) { + // sanity check + if (!TextUtils.isEmpty(userId)) { + Integer powerLevel = users.get(userId); + return (powerLevel != null) ? powerLevel : users_default; + } + + return users_default; + } + + /** + * Updates the user power levels of a dedicated user id + * + * @param userId the user + * @param powerLevel the new power level + */ + public void setUserPowerLevel(String userId, int powerLevel) { + if (null != userId) { + users.put(userId, Integer.valueOf(powerLevel)); + } + } + + /** + * Tell if an user can send an event of type 'eventTypeString'. + * + * @param eventTypeString the event type (in Event.EVENT_TYPE_XXX values) + * @param userId the user id + * @return true if the user can send the event + */ + public boolean maySendEventOfType(String eventTypeString, String userId) { + if (!TextUtils.isEmpty(eventTypeString) && !TextUtils.isEmpty(userId)) { + return getUserPowerLevel(userId) >= minimumPowerLevelForSendingEventAsMessage(eventTypeString); + } + + return false; + } + + /** + * Tells if an user can send a room message. + * + * @param userId the user id + * @return true if the user can send a room message + */ + public boolean maySendMessage(String userId) { + return maySendEventOfType(Event.EVENT_TYPE_MESSAGE, userId); + } + + /** + * Helper to get the minimum power level the user must have to send an event of the given type + * as a message. + * + * @param eventTypeString the type of event (in Event.EVENT_TYPE_XXX values) + * @return the required minimum power level. + */ + public int minimumPowerLevelForSendingEventAsMessage(String eventTypeString) { + int minimumPowerLevel = events_default; + + if ((null != eventTypeString) && events.containsKey(eventTypeString)) { + minimumPowerLevel = events.get(eventTypeString); + } + + return minimumPowerLevel; + } + + /** + * Helper to get the minimum power level the user must have to send an event of the given type + * as a state event. + * + * @param eventTypeString the type of event (in Event.EVENT_TYPE_STATE_ values). + * @return the required minimum power level. + */ + public int minimumPowerLevelForSendingEventAsStateEvent(String eventTypeString) { + int minimumPowerLevel = state_default; + + if ((null != eventTypeString) && events.containsKey(eventTypeString)) { + minimumPowerLevel = events.get(eventTypeString); + } + + return minimumPowerLevel; + } + + + /** + * Get the notification level for a dedicated key. + * + * @param key the notification key + * @return the level + */ + public int notificationLevel(String key) { + if ((null != key) && notifications.containsKey(key)) { + Object valAsVoid = notifications.get(key); + + // the first implementation was a string value + if (valAsVoid instanceof String) { + return Integer.parseInt((String) valAsVoid); + } else { + return (int) valAsVoid; + } + } + + return 50; + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/PushersResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/PushersResponse.java new file mode 100644 index 0000000000..a3a9d28fa2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/PushersResponse.java @@ -0,0 +1,27 @@ +/* + * 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.rest.model; + +import im.vector.matrix.android.internal.legacy.data.Pusher; + +import java.util.List; + +/** + * Class representing the pushers GET response + */ +public class PushersResponse { + public List pushers; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ReceiptData.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ReceiptData.java new file mode 100644 index 0000000000..893723478e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ReceiptData.java @@ -0,0 +1,42 @@ +/* + * 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.rest.model; + +import im.vector.matrix.android.internal.legacy.interfaces.DatedObject; + +public class ReceiptData implements java.io.Serializable, DatedObject { + + // the user id + public String userId; + + // The event id. + public String eventId; + + // The timestamp in ms since Epoch generated by the origin homeserver when it receives the event from the client. + public long originServerTs; + + public ReceiptData(String anUserId, String anEventId, long aTs) { + userId = anUserId; + eventId = anEventId; + originServerTs = aTs; + } + + @Override + public long getDate() { + return originServerTs; + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RedactedBecause.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RedactedBecause.java new file mode 100644 index 0000000000..c81881da65 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RedactedBecause.java @@ -0,0 +1,43 @@ +/* + * 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.rest.model; + +/** + * Redacted information + */ +public class RedactedBecause implements java.io.Serializable { + + // should be m.room.redaction" + public String type; + + // + public long origin_server_ts; + + // the redacted sender + public String sender; + + // the events Id + public String event_id; + + // unsigned + public UnsignedData unsigned; + + // + public String redacts; + + // should defined the reason + public RedactedContent content; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RedactedContent.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RedactedContent.java new file mode 100644 index 0000000000..06636683ed --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RedactedContent.java @@ -0,0 +1,25 @@ +/* + * 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.rest.model; + +/** + * Redacted information + */ +public class RedactedContent implements java.io.Serializable { + + // the redaction reason + public String reason; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ReportContentParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ReportContentParams.java new file mode 100644 index 0000000000..fc6ecc7803 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ReportContentParams.java @@ -0,0 +1,31 @@ +/* + * 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.rest.model; + +import java.util.List; + +/** + * Parameters to report an event content + */ +public class ReportContentParams { + + // The event range from -100 “most offensive” to 0 “inoffensive”. + public int score; + + // the report reason + public String reason; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RequestEmailValidationParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RequestEmailValidationParams.java new file mode 100755 index 0000000000..8c4c68a4b3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RequestEmailValidationParams.java @@ -0,0 +1,38 @@ +/* + * 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.rest.model; + +/** + * Parameters to request a validation token for an email + */ +public class RequestEmailValidationParams { + + // the email address + public String email; + + // the client secret key + public String clientSecret; + + // the attempt count + public Integer sendAttempt; + + // the server id + public String id_server; + + // the nextlink (given if it is a registration process) + public String next_link; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RequestEmailValidationResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RequestEmailValidationResponse.java new file mode 100755 index 0000000000..8fd3b9ece8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RequestEmailValidationResponse.java @@ -0,0 +1,34 @@ +/* + * 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.rest.model; + +/** + * Response to a request an email validation post + */ +public class RequestEmailValidationResponse { + + // the client secret key + public String clientSecret; + + // the email address + public String email; + + // the attempt count + public Integer sendAttempt; + + // the email sid + public String sid; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RequestPhoneNumberValidationParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RequestPhoneNumberValidationParams.java new file mode 100755 index 0000000000..b62fbaa632 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RequestPhoneNumberValidationParams.java @@ -0,0 +1,38 @@ +/* + * 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.rest.model; + +/** + * Parameters to request a validation token for a phone number + */ +public class RequestPhoneNumberValidationParams { + + // the country + public String country; + + // the phone number + public String phone_number; + + // the client secret key + public String clientSecret; + + // the attempt count + public Integer sendAttempt; + + // the server id + public String id_server; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RequestPhoneNumberValidationResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RequestPhoneNumberValidationResponse.java new file mode 100755 index 0000000000..c11813e570 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RequestPhoneNumberValidationResponse.java @@ -0,0 +1,37 @@ +/* + * 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.rest.model; + +/** + * Response to a request an phone number validation request + */ +public class RequestPhoneNumberValidationResponse { + + // the client secret key + public String clientSecret; + + // the attempt count + public Integer sendAttempt; + + // the sid + public String sid; + + // the msisdn + public String msisdn; + + // phone number international format + public String intl_fmt; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RoomAliasDescription.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RoomAliasDescription.java new file mode 100644 index 0000000000..f034cb2ec2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RoomAliasDescription.java @@ -0,0 +1,33 @@ +/* + * 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.rest.model; + +import java.util.List; + +/** + * Class representing a room alias + */ +public class RoomAliasDescription { + /** + * The room ID for this alias. + */ + public String room_id; + + /** + * A list of servers that are aware of this room ID. + */ + public List servers; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RoomCreateContent.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RoomCreateContent.java new file mode 100644 index 0000000000..3d4e2fe68a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RoomCreateContent.java @@ -0,0 +1,62 @@ +/* + * 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.rest.model; + + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +/** + * Content of a m.room.create type event + */ +public class RoomCreateContent implements Serializable { + + public String creator; + public Predecessor predecessor; + + public RoomCreateContent deepCopy() { + final RoomCreateContent copy = new RoomCreateContent(); + copy.creator = creator; + copy.predecessor = predecessor != null ? predecessor.deepCopy() : null; + return copy; + } + + public boolean hasPredecessor() { + return predecessor != null; + } + + /** + * A link to an old room in case of room versioning + */ + public static class Predecessor implements Serializable { + + @SerializedName("room_id") + public String roomId; + + @SerializedName("event_id") + public String eventId; + + public Predecessor deepCopy() { + final Predecessor copy = new Predecessor(); + copy.roomId = roomId; + copy.eventId = eventId; + return copy; + } + } + + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RoomDirectoryVisibility.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RoomDirectoryVisibility.java new file mode 100644 index 0000000000..24d7606796 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RoomDirectoryVisibility.java @@ -0,0 +1,24 @@ +/* + * 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.rest.model; + +public class RoomDirectoryVisibility { + public static final String DIRECTORY_VISIBILITY_PRIVATE = "private"; + public static final String DIRECTORY_VISIBILITY_PUBLIC = "public"; + + public String visibility; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RoomMember.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RoomMember.java new file mode 100644 index 0000000000..e797b8315f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RoomMember.java @@ -0,0 +1,349 @@ +/* + * 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.rest.model; + +import android.text.TextUtils; + +import com.google.gson.annotations.SerializedName; + +import im.vector.matrix.android.internal.legacy.util.ContentManager; +import im.vector.matrix.android.internal.legacy.util.Log; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.util.Comparator; + +/** + * Class representing a room member: a user with membership information. + */ +public class RoomMember implements Externalizable { + private static final String LOG_TAG = RoomMember.class.getSimpleName(); + + public static final String MEMBERSHIP_JOIN = "join"; + public static final String MEMBERSHIP_INVITE = "invite"; + public static final String MEMBERSHIP_LEAVE = "leave"; + public static final String MEMBERSHIP_BAN = "ban"; + + // not supported by the server sync response by computed from the room state events + public static final String MEMBERSHIP_KICK = "kick"; + + public String displayname; + public String avatarUrl; + public String membership; + public Invite thirdPartyInvite; + + // tells that the inviter starts a direct chat room + @SerializedName("is_direct") + public Boolean isDirect; + + private String userId = null; + // timestamp of the event which has created this member + private long mOriginServerTs = -1; + + // the event used to build the room member + private String mOriginalEventId = null; + + // kick / ban reason + public String reason; + // user which banned or kicked this member + public String mSender; + + @Override + public void readExternal(ObjectInput input) throws IOException, ClassNotFoundException { + if (input.readBoolean()) { + displayname = input.readUTF(); + } + + if (input.readBoolean()) { + avatarUrl = input.readUTF(); + } + + if (input.readBoolean()) { + membership = input.readUTF(); + } + + if (input.readBoolean()) { + thirdPartyInvite = (Invite) input.readObject(); + } + + if (input.readBoolean()) { + isDirect = input.readBoolean(); + } + + if (input.readBoolean()) { + userId = input.readUTF(); + } + + mOriginServerTs = input.readLong(); + + if (input.readBoolean()) { + mOriginalEventId = input.readUTF(); + } + + if (input.readBoolean()) { + reason = input.readUTF(); + } + + if (input.readBoolean()) { + mSender = input.readUTF(); + } + } + + @Override + public void writeExternal(ObjectOutput output) throws IOException { + output.writeBoolean(null != displayname); + if (null != displayname) { + output.writeUTF(displayname); + } + + output.writeBoolean(null != avatarUrl); + if (null != avatarUrl) { + output.writeUTF(avatarUrl); + } + + output.writeBoolean(null != membership); + if (null != membership) { + output.writeUTF(membership); + } + + output.writeBoolean(null != thirdPartyInvite); + if (null != thirdPartyInvite) { + output.writeObject(thirdPartyInvite); + } + + output.writeBoolean(null != isDirect); + if (null != isDirect) { + output.writeBoolean(isDirect); + } + + output.writeBoolean(null != userId); + if (null != userId) { + output.writeUTF(userId); + } + + output.writeLong(mOriginServerTs); + + output.writeBoolean(null != mOriginalEventId); + if (null != mOriginalEventId) { + output.writeUTF(mOriginalEventId); + } + + output.writeBoolean(null != reason); + if (null != reason) { + output.writeUTF(reason); + } + + output.writeBoolean(null != mSender); + if (null != mSender) { + output.writeUTF(mSender); + } + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public void setOriginServerTs(long aTs) { + mOriginServerTs = aTs; + } + + public long getOriginServerTs() { + return mOriginServerTs; + } + + public void setOriginalEventId(String eventId) { + mOriginalEventId = eventId; + } + + public String getOriginalEventId() { + return mOriginalEventId; + } + + public String getAvatarUrl() { + // allow only url which starts with mxc:// + if ((null != avatarUrl) && !avatarUrl.toLowerCase().startsWith(ContentManager.MATRIX_CONTENT_URI_SCHEME)) { + Log.e(LOG_TAG, "## getAvatarUrl() : the member " + userId + " has an invalid avatar url " + avatarUrl); + return null; + } + + return avatarUrl; + } + + public void setAvatarUrl(String anAvatarUrl) { + avatarUrl = anAvatarUrl; + } + + public String getThirdPartyInviteToken() { + if ((null != thirdPartyInvite) && (null != thirdPartyInvite.signed)) { + return thirdPartyInvite.signed.token; + } + + return null; + } + + // Comparator to order members alphabetically + public static Comparator alphaComparator = new Comparator() { + @Override + public int compare(RoomMember member1, RoomMember member2) { + String lhs = member1.getName(); + String rhs = member2.getName(); + + if (lhs == null) { + return -1; + } else if (rhs == null) { + return 1; + } + if (lhs.startsWith("@")) { + lhs = lhs.substring(1); + } + if (rhs.startsWith("@")) { + rhs = rhs.substring(1); + } + return String.CASE_INSENSITIVE_ORDER.compare(lhs, rhs); + } + }; + + /** + * Test if a room member fields matches with a pattern. + * The check is done with the displayname and the userId. + * + * @param aPattern the pattern to search. + * @return true if it matches. + */ + public boolean matchWithPattern(String aPattern) { + if (TextUtils.isEmpty(aPattern) || TextUtils.isEmpty(aPattern.trim())) { + return false; + } + + boolean res = false; + + if (!TextUtils.isEmpty(displayname)) { + res = (displayname.toLowerCase().indexOf(aPattern) >= 0); + } + + if (!res && !TextUtils.isEmpty(userId)) { + res = (userId.toLowerCase().indexOf(aPattern) >= 0); + } + + return res; + } + + /** + * Test if a room member matches with a reg ex. + * The check is done with the displayname and the userId. + * + * @param aRegEx the reg ex + * @return true if it matches. + */ + public boolean matchWithRegEx(String aRegEx) { + if (TextUtils.isEmpty(aRegEx)) { + return false; + } + + boolean res = false; + + if (!TextUtils.isEmpty(displayname)) { + res = displayname.matches(aRegEx); + } + + if (!res && !TextUtils.isEmpty(userId)) { + res = userId.matches(aRegEx); + } + + return res; + } + + /** + * Compare two members. + * The members are equals if each field have the same value. + * + * @param otherMember the member to compare. + * @return true if they define the same member. + */ + public boolean equals(RoomMember otherMember) { + // compare to null + if (null == otherMember) { + return false; + } + + // compare display name + boolean isEqual = TextUtils.equals(displayname, otherMember.displayname); + + if (isEqual) { + isEqual = TextUtils.equals(avatarUrl, otherMember.avatarUrl); + } + + if (isEqual) { + isEqual = TextUtils.equals(membership, otherMember.membership); + } + + if (isEqual) { + isEqual = TextUtils.equals(userId, otherMember.userId); + } + + return isEqual; + } + + public String getName() { + if (displayname != null) { + return displayname; + } + if (userId != null) { + return userId; + } + return null; + } + + /** + * Prune the room member data as we would have done with its original state event. + */ + public void prune() { + // Redact redactable data + displayname = null; + avatarUrl = null; + reason = null; + + // Note: if we had access to the original event content, we should store + // the `redacted_because` of the redaction event in it. + } + + public RoomMember deepCopy() { + RoomMember copy = new RoomMember(); + copy.displayname = displayname; + copy.avatarUrl = avatarUrl; + copy.membership = membership; + copy.userId = userId; + copy.mOriginalEventId = mOriginalEventId; + copy.mSender = mSender; + copy.reason = reason; + return copy; + } + + /** + * @return true if the user has been banned or kicked + */ + public boolean kickedOrBanned() { + return TextUtils.equals(membership, MEMBERSHIP_KICK) || TextUtils.equals(membership, MEMBERSHIP_BAN); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RoomPinnedEventsContent.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RoomPinnedEventsContent.java new file mode 100644 index 0000000000..f081408d92 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RoomPinnedEventsContent.java @@ -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.rest.model; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * Content of a m.room.pinned_events type event + */ +public class RoomPinnedEventsContent implements Serializable { + + // List of eventIds of pinned events + public List pinned; + + public RoomPinnedEventsContent deepCopy() { + final RoomPinnedEventsContent copy = new RoomPinnedEventsContent(); + if (pinned == null) { + copy.pinned = null; + } else { + copy.pinned = new ArrayList<>(pinned); + } + return copy; + } + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RoomTags.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RoomTags.java new file mode 100644 index 0000000000..f3774cee60 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RoomTags.java @@ -0,0 +1,24 @@ +/* + * 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.rest.model; + + +import java.util.Map; + +public class RoomTags implements java.io.Serializable { + public Map> tags; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RoomTombstoneContent.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RoomTombstoneContent.java new file mode 100644 index 0000000000..84c720f176 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/RoomTombstoneContent.java @@ -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.rest.model; + + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; + +/** + * Class to contains Tombstone information + */ +public class RoomTombstoneContent implements Serializable { + + public String body; + + @SerializedName("replacement_room") + public String replacementRoom; + + public RoomTombstoneContent deepCopy() { + final RoomTombstoneContent copy = new RoomTombstoneContent(); + copy.body = body; + copy.replacementRoom = replacementRoom; + return copy; + } + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ServerNoticeUsageLimitContent.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ServerNoticeUsageLimitContent.java new file mode 100644 index 0000000000..5ad436fd9c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ServerNoticeUsageLimitContent.java @@ -0,0 +1,39 @@ +/* + * 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.rest.model; + +import com.google.gson.annotations.SerializedName; + +/** + * Content of a m.server_notice.usage_limit_reached type event + */ +public class ServerNoticeUsageLimitContent { + + private static final String EVENT_TYPE_SERVER_NOTICE_USAGE_LIMIT = "m.server_notice.usage_limit_reached"; + + // The kind of user limit, generally is monthly_active_user + public String limit; + @SerializedName("admin_contact") + public String adminUri; + @SerializedName("server_notice_type") + public String type; + + public boolean isServerNoticeUsageLimit() { + return EVENT_TYPE_SERVER_NOTICE_USAGE_LIMIT.equals(type); + } + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/Signed.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/Signed.java new file mode 100644 index 0000000000..c4faf8d726 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/Signed.java @@ -0,0 +1,36 @@ +/* + * 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.rest.model; + +/** + * subclass representing a search API response + */ +public class Signed implements java.io.Serializable { + /** + * The token property of the containing third_party_invite object. + */ + public String token; + + /** + * A single signature from the verifying server, in the format specified by the Signing Events section of the server-server API. + */ + public Object signatures; + + /** + * The invited matrix user ID. Must be equal to the user_id property of the event. + */ + public String mxid; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/StateEvent.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/StateEvent.java new file mode 100644 index 0000000000..71b6d0e759 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/StateEvent.java @@ -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.rest.model; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +/** + * Class representing a state event content + * Usually, only one field is not null, depending of the type of the Event. + */ +public class StateEvent { + public String name; + + public String topic; + + @SerializedName("join_rule") + public String joinRule; + + @SerializedName("guest_access") + public String guestAccess; + + @SerializedName("alias") + public String canonicalAlias; + + public List aliases; + + public String algorithm; + + @SerializedName("history_visibility") + public String historyVisibility; + + public String url; + + public List groups; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ThreePidCreds.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ThreePidCreds.java new file mode 100755 index 0000000000..e2b64742df --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/ThreePidCreds.java @@ -0,0 +1,31 @@ +/* + * 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.rest.model; + +/** + * 3 pid credentials + */ +public class ThreePidCreds { + + // the identity server URL (without the http://) + public String id_server; + + // the 3 pids sid + public String sid; + + // a secret key + public String client_secret; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/TokensChunkEvents.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/TokensChunkEvents.java new file mode 100644 index 0000000000..a65f579a1a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/TokensChunkEvents.java @@ -0,0 +1,28 @@ +/* + * 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.rest.model; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class TokensChunkEvents extends TokensChunkResponse { + + // With LazyLoading, we can have state events here + @SerializedName("state") + public List stateEvents; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/TokensChunkResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/TokensChunkResponse.java new file mode 100644 index 0000000000..849dab73b3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/TokensChunkResponse.java @@ -0,0 +1,25 @@ +/* + * 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.rest.model; + +/** + * Class representing an API response with start and end tokens and a generically-typed chunk. + */ +public class TokensChunkResponse extends ChunkResponse { + public String start; + public String end; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/Typing.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/Typing.java new file mode 100644 index 0000000000..5dd7b970c5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/Typing.java @@ -0,0 +1,24 @@ +/* + * 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.rest.model; + +/** + * Class to contain a banned user and the reason they were banned. + */ +public class Typing { + public boolean typing; + public int timeout; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/URLPreview.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/URLPreview.java new file mode 100644 index 0000000000..a4686c9055 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/URLPreview.java @@ -0,0 +1,124 @@ +/* + * 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.rest.model; + +import android.text.TextUtils; + +import im.vector.matrix.android.internal.legacy.util.JsonUtils; + +import java.util.Map; + +/** + * Class representing an URL preview. + */ +public class URLPreview implements java.io.Serializable { + + private static final String OG_DESCRIPTION = "og:description"; + private static final String OG_TITLE = "og:title"; + private static final String OG_TYPE = "og:type"; + + private static final String OG_SITE_NAME = "og:site_name"; + private static final String OG_URL = "og:url"; + + private static final String OG_IMAGE = "og:image"; + private static final String OG_IMAGE_SIZE = "matrix:image:size"; + private static final String OG_IMAGE_TYPE = "og:image:type"; + private static final String OG_IMAGE_WIDTH = "og:image:width"; + private static final String OG_IMAGE_HEIGHT = "og:image:height"; + + /** + * Global information + */ + private final String mDescription; + private final String mTitle; + private final String mType; + + private final String mSiteName; + private final String mRequestedURL; + + /** + * Image information + */ + private final String mThumbnailURL; + private final String mThumbnailMimeType; + + private boolean mIsDismissed; + + /** + * Constructor + * + * @param map the constructor parameters + * @param url the original url, will be used if the map does not contain OG_URL field + */ + public URLPreview(Map map, String url) { + mDescription = JsonUtils.getAsString(map, OG_DESCRIPTION); + mTitle = JsonUtils.getAsString(map, OG_TITLE); + mType = JsonUtils.getAsString(map, OG_TYPE); + + mSiteName = JsonUtils.getAsString(map, OG_SITE_NAME); + + String requestedUrl = JsonUtils.getAsString(map, OG_URL); + + if (TextUtils.isEmpty(requestedUrl)) { + // Fallback: use url + mRequestedURL = url; + } else { + mRequestedURL = requestedUrl; + } + + mThumbnailURL = JsonUtils.getAsString(map, OG_IMAGE); + mThumbnailMimeType = JsonUtils.getAsString(map, OG_IMAGE_TYPE); + } + + + public String getDescription() { + return mDescription; + } + + public String getTitle() { + return mTitle; + } + + public String getType() { + return mType; + } + + public String getSiteName() { + return mSiteName; + } + + public String getRequestedURL() { + return mRequestedURL; + } + + public String getThumbnailURL() { + return mThumbnailURL; + } + + public String getThumbnailMimeType() { + return mThumbnailMimeType; + } + + public boolean IsDismissed() { + return mIsDismissed; + } + + public void setIsDismissed(boolean isDismissed) { + mIsDismissed = isDismissed; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/UnsignedData.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/UnsignedData.java new file mode 100644 index 0000000000..7ebcc43967 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/UnsignedData.java @@ -0,0 +1,44 @@ +/* + * 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.rest.model; + +import com.google.gson.JsonElement; + +/** + * Contains optional extra information about the event. + */ +public class UnsignedData implements java.io.Serializable { + + /** + * The time in milliseconds that has elapsed since the event was sent + */ + public Long age; + + /** + * The reason this event was redacted, if it was redacted + */ + public RedactedBecause redacted_because; + + /** + * The client-supplied transaction ID, if the client being given the event is the same one which sent it. + */ + public String transaction_id; + + /** + * The previous event content (room member information only) + */ + public transient JsonElement prev_content; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/User.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/User.java new file mode 100644 index 0000000000..c1ee93e27b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/User.java @@ -0,0 +1,279 @@ +/* + * 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.rest.model; + +import android.text.TextUtils; + +import im.vector.matrix.android.internal.legacy.MXDataHandler; +import im.vector.matrix.android.internal.legacy.listeners.IMXEventListener; +import im.vector.matrix.android.internal.legacy.listeners.MXEventListener; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Class representing a user. + */ +public class User implements java.io.Serializable { + private static final long serialVersionUID = 5234056937639712713L; + + // the user presence values + public static final String PRESENCE_ONLINE = "online"; + public static final String PRESENCE_UNAVAILABLE = "unavailable"; + public static final String PRESENCE_OFFLINE = "offline"; + public static final String PRESENCE_FREE_FOR_CHAT = "free_for_chat"; + public static final String PRESENCE_HIDDEN = "hidden"; + + // user fields provided by the server + public String user_id; + public String displayname; + public String avatar_url; + public String presence; + public Boolean currently_active; + public Long lastActiveAgo; + public String statusMsg; + + // tell if the information has been refreshed + private transient boolean mIsPresenceRefreshed; + + // Used to provide a more realistic last active time: + // the last active ago time provided by the server + the time that has gone by since + private long mLastPresenceTs; + + // Map to keep track of the listeners the client adds vs. the ones we actually register to the global data handler. + // This is needed to find the right one when removing the listener. + private transient Map mEventListeners = new HashMap<>(); + + // data handler + protected transient MXDataHandler mDataHandler; + + // events listeners list + private transient List mPendingListeners = new ArrayList<>(); + + // hash key to store the user in the file system; + private Integer mStorageHashKey = null; + + // The user data can have been retrieved by a room member + // The data can be partially invalid until a presence is received + private boolean mIsRetrievedFromRoomMember = false; + + // avatar URLs setter / getter + public String getAvatarUrl() { + return avatar_url; + } + + public void setAvatarUrl(String newAvatarUrl) { + avatar_url = newAvatarUrl; + } + + /** + * Tells if this user has been created from a room member event + * + * @return true if this user has been created from a room member event + */ + public boolean isRetrievedFromRoomMember() { + return mIsRetrievedFromRoomMember; + } + + /** + * Set that this user has been created from a room member. + */ + public void setRetrievedFromRoomMember() { + mIsRetrievedFromRoomMember = true; + } + + /** + * Check if mEventListeners has been initialized before providing it. + * The users are now serialized and the transient fields are not initialized. + * + * @return the events listener + */ + private Map getEventListeners() { + if (null == mEventListeners) { + mEventListeners = new HashMap<>(); + } + + return mEventListeners; + } + + /** + * Check if mPendingListeners has been initialized before providing it. + * The users are now serialized and the transient fields are not initialized. + * + * @return the pending listener + */ + private List getPendingListeners() { + if (null == mPendingListeners) { + mPendingListeners = new ArrayList<>(); + } + + return mPendingListeners; + } + + /** + * @return the user hash key + */ + public int getStorageHashKey() { + if (null == mStorageHashKey) { + mStorageHashKey = Math.abs(user_id.hashCode() % 100); + } + + return mStorageHashKey; + } + + /** + * @return true if the presence should be refreshed + */ + public boolean isPresenceObsolete() { + return !mIsPresenceRefreshed || (null == presence); + } + + /** + * Clone an user into this instance + * + * @param user the user to clone. + */ + protected void clone(User user) { + if (user != null) { + user_id = user.user_id; + displayname = user.displayname; + avatar_url = user.avatar_url; + presence = user.presence; + currently_active = user.currently_active; + lastActiveAgo = user.lastActiveAgo; + statusMsg = user.statusMsg; + + mIsPresenceRefreshed = user.mIsPresenceRefreshed; + mLastPresenceTs = user.mLastPresenceTs; + + mEventListeners = new HashMap<>(user.getEventListeners()); + mDataHandler = user.mDataHandler; + + mPendingListeners = user.getPendingListeners(); + } + } + + /** + * Create a deep copy of the current user. + * + * @return a deep copy of the current object + */ + public User deepCopy() { + User copy = new User(); + copy.clone(this); + return copy; + } + + /** + * Tells if an user is active + * + * @return true if the user is active + */ + public boolean isActive() { + return TextUtils.equals(presence, PRESENCE_ONLINE) || ((null != currently_active) && currently_active); + } + + /** + * Set the latest presence event time. + * + * @param ts the timestamp. + */ + public void setLatestPresenceTs(long ts) { + mIsPresenceRefreshed = true; + mLastPresenceTs = ts; + } + + /** + * @return the timestamp of the latest presence event. + */ + public long getLatestPresenceTs() { + return mLastPresenceTs; + } + + /** + * Get the user's last active ago time by adding the one given by the server and the time since elapsed. + * + * @return how long ago the user was last active (in ms) + */ + public long getAbsoluteLastActiveAgo() { + // sanity check + if (null == lastActiveAgo) { + return 0; + } else { + return System.currentTimeMillis() - (mLastPresenceTs - lastActiveAgo); + } + } + + /** + * Set the event listener to send back events to. This is typically the DataHandler for dispatching the events to listeners. + * + * @param dataHandler should be the main data handler for dispatching back events to registered listeners. + */ + public void setDataHandler(MXDataHandler dataHandler) { + mDataHandler = dataHandler; + + for (IMXEventListener listener : getPendingListeners()) { + mDataHandler.addListener(listener); + } + } + + /** + * Add an event listener to this room. Only events relative to the room will come down. + * + * @param eventListener the event listener to add + */ + public void addEventListener(final IMXEventListener eventListener) { + // Create a global listener that we'll add to the data handler + IMXEventListener globalListener = new MXEventListener() { + @Override + public void onPresenceUpdate(Event event, User user) { + // Only pass event through for this user + if (user.user_id.equals(user_id)) { + eventListener.onPresenceUpdate(event, user); + } + } + }; + getEventListeners().put(eventListener, globalListener); + + // the handler could be set later + if (null != mDataHandler) { + mDataHandler.addListener(globalListener); + } else { + getPendingListeners().add(globalListener); + } + } + + /** + * Remove an event listener. + * + * @param eventListener the event listener to remove + */ + public void removeEventListener(IMXEventListener eventListener) { + + if (null != mDataHandler) { + mDataHandler.removeListener(getEventListeners().get(eventListener)); + } else { + getPendingListeners().remove(getEventListeners().get(eventListener)); + } + + getEventListeners().remove(eventListener); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/Versions.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/Versions.java new file mode 100644 index 0000000000..8d54ffe3fa --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/Versions.java @@ -0,0 +1,36 @@ +/* + * 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.rest.model; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; +import java.util.Map; + +/** + * Model for https://matrix.org/docs/spec/client_server/r0.3.0.html#get-matrix-client-versions + *

+ * Ex: {"unstable_features": {"m.lazy_load_members": true}, "versions": ["r0.0.1", "r0.1.0", "r0.2.0", "r0.3.0"]} + */ +public class Versions { + + @SerializedName("versions") + public List supportedVersions; + + @SerializedName("unstable_features") + public Map unstableFeatures; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/BingRule.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/BingRule.java new file mode 100644 index 0000000000..a080285cff --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/BingRule.java @@ -0,0 +1,361 @@ +/* + * 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.rest.model.bingrules; + +import android.text.TextUtils; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.annotations.SerializedName; + +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.HashMap; +import java.util.List; +import java.util.Map; + +public class BingRule { + private static final String LOG_TAG = BingRule.class.getSimpleName(); + + public static final String RULE_ID_DISABLE_ALL = ".m.rule.master"; + public static final String RULE_ID_CONTAIN_USER_NAME = ".m.rule.contains_user_name"; + public static final String RULE_ID_CONTAIN_DISPLAY_NAME = ".m.rule.contains_display_name"; + public static final String RULE_ID_ONE_TO_ONE_ROOM = ".m.rule.room_one_to_one"; + public static final String RULE_ID_INVITE_ME = ".m.rule.invite_for_me"; + public static final String RULE_ID_PEOPLE_JOIN_LEAVE = ".m.rule.member_event"; + public static final String RULE_ID_CALL = ".m.rule.call"; + public static final String RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS = ".m.rule.suppress_notices"; + public static final String RULE_ID_ALL_OTHER_MESSAGES_ROOMS = ".m.rule.message"; + public static final String RULE_ID_FALLBACK = ".m.rule.fallback"; + + public static final String ACTION_NOTIFY = "notify"; + public static final String ACTION_DONT_NOTIFY = "dont_notify"; + public static final String ACTION_COALESCE = "coalesce"; + + public static final String ACTION_SET_TWEAK_SOUND_VALUE = "sound"; + public static final String ACTION_SET_TWEAK_HIGHLIGHT_VALUE = "highlight"; + + public static final String ACTION_PARAMETER_SET_TWEAK = "set_tweak"; + public static final String ACTION_PARAMETER_VALUE = "value"; + + public static final String ACTION_VALUE_DEFAULT = "default"; + public static final String ACTION_VALUE_RING = "ring"; + + public static final String KIND_OVERRIDE = "override"; + public static final String KIND_CONTENT = "content"; + public static final String KIND_ROOM = "room"; + public static final String KIND_SENDER = "sender"; + public static final String KIND_UNDERRIDE = "underride"; + + public String ruleId = null; + public List conditions = null; + // Object is either String or Map + public List actions = null; + @SerializedName("default") + public boolean isDefault = false; + + @SerializedName("enabled") + public boolean isEnabled = true; + + public String kind = null; + + public BingRule(boolean isDefaultValue) { + isDefault = isDefaultValue; + } + + public BingRule() { + isDefault = false; + } + + @Override + public String toString() { + return "BingRule{" + + "ruleId='" + ruleId + '\'' + + ", conditions=" + conditions + + ", actions=" + actions + + ", isDefault=" + isDefault + + ", isEnabled=" + isEnabled + + ", kind='" + kind + '\'' + + '}'; + } + + /** + * Convert BingRule to a JsonElement. + * It seems that "conditions" name triggers conversion issues. + * + * @return the JsonElement + */ + public JsonElement toJsonElement() { + JsonObject jsonObject = JsonUtils.getGson(false).toJsonTree(this).getAsJsonObject(); + + if (null != conditions) { + jsonObject.add("conditions", JsonUtils.getGson(false).toJsonTree(conditions)); + } + + return jsonObject; + } + + /** + * Bing rule creator + * + * @param ruleKind the rule kind + * @param aPattern the pattern to check the condition + * @param notify true to notify + * @param highlight true to highlight + * @param sound true to play sound + */ + public BingRule(String ruleKind, String aPattern, Boolean notify, Boolean highlight, boolean sound) { + // + ruleId = aPattern; + isEnabled = true; + isDefault = false; + kind = ruleKind; + conditions = null; + + actions = new ArrayList<>(); + + if (null != notify) { + setNotify(notify); + } + + if (null != highlight) { + setHighlight(highlight); + } + + if (sound) { + setNotificationSound(); + } + } + + /** + * Build a bing rule from another one. + * + * @param otherRule the other rule + */ + public BingRule(BingRule otherRule) { + ruleId = otherRule.ruleId; + + if (null != otherRule.conditions) { + conditions = new ArrayList<>(otherRule.conditions); + } + + if (null != otherRule.actions) { + actions = new ArrayList<>(otherRule.actions); + } + + isDefault = otherRule.isDefault; + isEnabled = otherRule.isEnabled; + kind = otherRule.kind; + } + + /** + * Add a condition to the rule. + * + * @param condition the condition to add. + */ + public void addCondition(Condition condition) { + if (null == conditions) { + conditions = new ArrayList<>(); + } + conditions.add(condition); + } + + /** + * Search an action map from its tweak. + * + * @param tweak the tweak name. + * @return the action map. null if not found. + */ + public Map getActionMap(String tweak) { + if ((null != actions) && !TextUtils.isEmpty(tweak)) { + for (Object action : actions) { + if (action instanceof Map) { + try { + Map actionMap = ((Map) action); + + if (TextUtils.equals((String) actionMap.get(ACTION_PARAMETER_SET_TWEAK), tweak)) { + return actionMap; + } + } catch (Exception e) { + Log.e(LOG_TAG, "## getActionMap() : " + e.getMessage(), e); + } + } + } + } + + return null; + } + + /** + * Check if the sound type is the default notification sound. + * + * @param sound the sound name. + * @return true if the sound is the default notification sound. + */ + public static boolean isDefaultNotificationSound(String sound) { + return ACTION_VALUE_DEFAULT.equals(sound); + } + + /** + * Check if the sound type is the call ring. + * + * @param sound the sound name. + * @return true if the sound is the call ring. + */ + public static boolean isCallRingNotificationSound(String sound) { + return ACTION_VALUE_RING.equals(sound); + } + + /** + * @return the notification sound (null if it is not defined) + */ + public String getNotificationSound() { + String sound = null; + Map actionMap = getActionMap(ACTION_SET_TWEAK_SOUND_VALUE); + + if ((null != actionMap) && actionMap.containsKey(ACTION_PARAMETER_VALUE)) { + sound = (String) actionMap.get(ACTION_PARAMETER_VALUE); + } + + return sound; + } + + /** + * Add the default notification sound. + */ + public void setNotificationSound() { + setNotificationSound(ACTION_VALUE_DEFAULT); + } + + /** + * Set the notification sound + * + * @param sound notification sound + */ + public void setNotificationSound(String sound) { + removeNotificationSound(); + + if (!TextUtils.isEmpty(sound)) { + Map actionMap = new HashMap<>(); + actionMap.put(ACTION_PARAMETER_SET_TWEAK, ACTION_SET_TWEAK_SOUND_VALUE); + actionMap.put(ACTION_PARAMETER_VALUE, sound); + actions.add(actionMap); + } + } + + /** + * Remove the notification sound + */ + public void removeNotificationSound() { + Map actionMap = getActionMap(ACTION_SET_TWEAK_SOUND_VALUE); + + if (null != actionMap) { + actions.remove(actionMap); + } + } + + /** + * Set the highlight status. + * + * @param highlight the highlight status + */ + public void setHighlight(boolean highlight) { + Map actionMap = getActionMap(ACTION_SET_TWEAK_HIGHLIGHT_VALUE); + + if (null == actionMap) { + actionMap = new HashMap<>(); + actionMap.put(ACTION_PARAMETER_SET_TWEAK, ACTION_SET_TWEAK_HIGHLIGHT_VALUE); + actions.add(actionMap); + } + + if (highlight) { + actionMap.remove(ACTION_PARAMETER_VALUE); + } else { + actionMap.put(ACTION_PARAMETER_VALUE, false); + } + } + + /** + * Return true if the rule should highlight the event. + * + * @return true if the rule should play sound + */ + public boolean shouldHighlight() { + boolean shouldHighlight = false; + + Map actionMap = getActionMap(ACTION_SET_TWEAK_HIGHLIGHT_VALUE); + + if (null != actionMap) { + // default behaviour + shouldHighlight = true; + + if (actionMap.containsKey(ACTION_PARAMETER_VALUE)) { + Object valueAsVoid = actionMap.get(ACTION_PARAMETER_VALUE); + + if (valueAsVoid instanceof Boolean) { + shouldHighlight = (boolean) valueAsVoid; + } else if (valueAsVoid instanceof String) { + shouldHighlight = TextUtils.equals((String)valueAsVoid, "true"); + } else { + Log.e(LOG_TAG, "## shouldHighlight() : unexpected type " + valueAsVoid); + } + } + } + + return shouldHighlight; + } + + /** + * Set the notification status. + * + * @param notify true to notify + */ + public void setNotify(boolean notify) { + if (notify) { + actions.remove(ACTION_DONT_NOTIFY); + + if (!actions.contains(ACTION_NOTIFY)) { + actions.add(ACTION_NOTIFY); + } + } else { + actions.remove(ACTION_NOTIFY); + + if (!actions.contains(ACTION_DONT_NOTIFY)) { + actions.add(ACTION_DONT_NOTIFY); + } + } + } + + /** + * Return true if the rule should highlight the event. + * + * @return true if the rule should play sound + */ + public boolean shouldNotify() { + return actions.contains(ACTION_NOTIFY); + } + + /** + * Return true if the rule should not highlight the event. + * + * @return true if the rule should not play sound + */ + public boolean shouldNotNotify() { + return actions.contains(ACTION_DONT_NOTIFY); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/Condition.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/Condition.java new file mode 100644 index 0000000000..51f1c2704b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/Condition.java @@ -0,0 +1,49 @@ +/* + * 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.rest.model.bingrules; + + +public class Condition { + // defined in the push rules spec + // https://matrix.org/docs/spec/client_server/r0.3.0.html#push-rules + + /* 'key': The dot-separated field of the event to match, eg. content.body + 'pattern': The glob-style pattern to match against. Patterns with no special glob characters should be treated as having asterisks prepended + and appended when testing the condition.*/ + public static final String KIND_EVENT_MATCH = "event_match"; + + /* 'profile_tag': The profile_tag to match with.*/ + public static final String KIND_PROFILE_TAG = "profile_tag"; + + /* no parameter */ + public static final String KIND_CONTAINS_DISPLAY_NAME = "contains_display_name"; + + /* 'is': A decimal integer optionally prefixed by one of, '==', '<', '>', '>=' or '<='. + A prefix of '<' matches rooms where the member count is strictly less than the given number and so forth. If no prefix is present, this matches + rooms where the member count is exactly equal to the given number (ie. the same as '=='). + */ + public static final String KIND_ROOM_MEMBER_COUNT = "room_member_count"; + + /* */ + public static final String KIND_DEVICE = "device"; + + public static final String KIND_SENDER_NOTIFICATION_PERMISSION = "sender_notification_permission"; + + public static final String KIND_UNKNOWN = "unknown_condition"; + + public String kind; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/ContainsDisplayNameCondition.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/ContainsDisplayNameCondition.java new file mode 100644 index 0000000000..f641a3db08 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/ContainsDisplayNameCondition.java @@ -0,0 +1,41 @@ +/* + * 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.rest.model.bingrules; + +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.EventUtils; +import im.vector.matrix.android.internal.legacy.util.JsonUtils; + +/** + * Bing rule condition that is satisfied when a message body contains the user's current display name. + */ +public class ContainsDisplayNameCondition extends Condition { + public ContainsDisplayNameCondition() { + kind = Condition.KIND_CONTAINS_DISPLAY_NAME; + } + + public boolean isSatisfied(Event event, String myDisplayName) { + if (Event.EVENT_TYPE_MESSAGE.equals(event.getType())) { + Message msg = JsonUtils.toMessage(event.getContent()); + + if (null != msg) { + return EventUtils.caseInsensitiveFind(myDisplayName, msg.body); + } + } + return false; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/ContentRule.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/ContentRule.java new file mode 100644 index 0000000000..e34ded8dcf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/ContentRule.java @@ -0,0 +1,25 @@ +/* + * 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.rest.model.bingrules; + +public class ContentRule extends BingRule { + public String pattern; + + public ContentRule(String ruleKind, String aPattern, boolean notify, boolean highlight, boolean sound) { + super(ruleKind, aPattern, notify, highlight, sound); + pattern = aPattern; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/DeviceCondition.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/DeviceCondition.java new file mode 100644 index 0000000000..6585e3bc79 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/DeviceCondition.java @@ -0,0 +1,29 @@ +/* + * 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.rest.model.bingrules; + +public class DeviceCondition extends Condition { + public String profileTag; + + public DeviceCondition() { + kind = Condition.KIND_DEVICE; + } + + @Override + public String toString() { + return "DeviceCondition{" + "profileTag='" + profileTag + "'}'"; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/EventMatchCondition.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/EventMatchCondition.java new file mode 100644 index 0000000000..aad3341393 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/EventMatchCondition.java @@ -0,0 +1,113 @@ +/* + * 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.rest.model.bingrules; + +import android.text.TextUtils; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import im.vector.matrix.android.internal.legacy.rest.model.Event; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + +public class EventMatchCondition extends Condition { + + public String key; + public String pattern; + + private static Map mPatternByRule = null; + + public EventMatchCondition() { + kind = Condition.KIND_EVENT_MATCH; + } + + @Override + public String toString() { + return "EventMatchCondition{" + "key='" + key + ", pattern=" + pattern + '}'; + } + + /** + * Returns whether the given event satisfies the condition. + * + * @param event the event + * @return true if the event satisfies the condition + */ + public boolean isSatisfied(Event event) { + String fieldVal = null; + + // some information are in the decrypted event (like type) + if (event.isEncrypted() && (null != event.getClearEvent())) { + JsonObject eventJson = event.getClearEvent().toJsonObject(); + fieldVal = extractField(eventJson, key); + } + + if (TextUtils.isEmpty(fieldVal)) { + JsonObject eventJson = event.toJsonObject(); + fieldVal = extractField(eventJson, key); + } + + if (TextUtils.isEmpty(fieldVal)) { + return false; + } + + if (TextUtils.equals(pattern, fieldVal)) { + return true; + } + + if (null == mPatternByRule) { + mPatternByRule = new HashMap<>(); + } + + Pattern patternEx = mPatternByRule.get(pattern); + + if (null == patternEx) { + patternEx = Pattern.compile(globToRegex(pattern), Pattern.CASE_INSENSITIVE); + mPatternByRule.put(pattern, patternEx); + } + + return patternEx.matcher(fieldVal).matches(); + } + + private String extractField(JsonObject jsonObject, String fieldPath) { + String[] fieldParts = fieldPath.split("\\."); + JsonElement jsonElement = null; + for (String field : fieldParts) { + jsonElement = jsonObject.get(field); + if (jsonElement == null) { + return null; + } + if (jsonElement.isJsonObject()) { + jsonObject = (JsonObject) jsonElement; + } + } + return (jsonElement == null) ? null : jsonElement.getAsString(); + } + + private String globToRegex(String glob) { + String res = glob.replace("*", ".*").replace("?", "."); + + // If no special characters were found (detected here by no replacements having been made), + // add asterisks and boundaries to both sides + if (res.equals(glob)) { + res = "(^|.*\\W)" + res + "($|\\W.*)"; + } + return res; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/PushRuleSet.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/PushRuleSet.java new file mode 100644 index 0000000000..7cdf3459f7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/PushRuleSet.java @@ -0,0 +1,205 @@ +/* + * 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.rest.model.bingrules; + +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.List; + +public class PushRuleSet { + public List override; + public List content; + public List room; + public List sender; + public List underride; + + /** + * Constructor + */ + public PushRuleSet() { + override = new ArrayList<>(); + content = new ArrayList<>(); + room = new ArrayList<>(); + sender = new ArrayList<>(); + underride = new ArrayList<>(); + } + + /** + * Find a rule from its rule ID. + * + * @param rules the rules list. + * @param ruleID the rule ID. + * @return the bing rule if it exists, else null. + */ + private BingRule findRule(List rules, String ruleID) { + for (BingRule rule : rules) { + if (TextUtils.equals(ruleID, rule.ruleId)) { + return rule; + } + } + return null; + } + + private List getBingRulesList(String kind) { + List res = null; + + if (BingRule.KIND_OVERRIDE.equals(kind)) { + res = override; + } else if (BingRule.KIND_ROOM.equals(kind)) { + res = room; + } else if (BingRule.KIND_SENDER.equals(kind)) { + res = sender; + } else if (BingRule.KIND_UNDERRIDE.equals(kind)) { + res = underride; + } + + return res; + } + + /** + * Add a rule from the bingRules + * + * @param rule the rule to add. + */ + public void addAtTop(BingRule rule) { + if (TextUtils.equals(BingRule.KIND_CONTENT, rule.kind)) { + if (null != content) { + if (rule instanceof ContentRule) { + content.add(0, (ContentRule) rule); + } + } + } else { + List rulesList = getBingRulesList(rule.kind); + + if (null != rulesList) { + rulesList.add(0, rule); + } + } + } + + /** + * Remove a rule from the bingRules + * + * @param rule the rule to delete. + * @return true if the rule has been deleted + */ + public boolean remove(BingRule rule) { + boolean res = false; + + if (BingRule.KIND_CONTENT.equals(rule.kind)) { + if (null != content) { + res = content.remove(rule); + } + } else { + List rulesList = getBingRulesList(rule.kind); + + if (null != rulesList) { + res = rulesList.remove(rule); + } + } + + return res; + } + + /** + * Find a rule from its rule ID. + * + * @param rules the rules list. + * @param ruleID the rule ID. + * @return the bing rule if it exists, else null. + */ + private BingRule findContentRule(List rules, String ruleID) { + for (BingRule rule : rules) { + if (TextUtils.equals(ruleID, rule.ruleId)) { + return rule; + } + } + return null; + } + + /** + * Find a rule from its ruleID. + * + * @param ruleId a RULE_ID_XX value + * @return the matched bing rule or null it doesn't exist. + */ + public BingRule findDefaultRule(String ruleId) { + BingRule rule = null; + + // sanity check + if (null != ruleId) { + if (TextUtils.equals(BingRule.RULE_ID_CONTAIN_USER_NAME, ruleId)) { + rule = findContentRule(content, ruleId); + } else { + // assume that the ruleId is unique. + rule = findRule(override, ruleId); + + if (null == rule) { + rule = findRule(underride, ruleId); + } + } + } + + return rule; + } + + /** + * Return the content rules list. + * + * @return the content rules list. + */ + public List getContentRules() { + List res = new ArrayList<>(); + + if (null != content) { + for (BingRule rule : content) { + if (!rule.ruleId.startsWith(".m.")) { + res.add(rule); + } + } + } + + return res; + } + + /** + * Return the room rules list. + * + * @return the room rules list. + */ + public List getRoomRules() { + if (null == room) { + return new ArrayList<>(); + } else { + return room; + } + } + + /** + * Return the room rules list. + * + * @return the sender rules list. + */ + public List getSenderRules() { + if (null == sender) { + return new ArrayList<>(); + } else { + return sender; + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/PushRulesResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/PushRulesResponse.java new file mode 100644 index 0000000000..9e94c00a08 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/PushRulesResponse.java @@ -0,0 +1,21 @@ +/* + * 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.rest.model.bingrules; + +public class PushRulesResponse { + public PushRuleSet device; + public PushRuleSet global; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/RoomMemberCountCondition.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/RoomMemberCountCondition.java new file mode 100644 index 0000000000..56b542df32 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/RoomMemberCountCondition.java @@ -0,0 +1,102 @@ +/* + * 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.rest.model.bingrules; + +import im.vector.matrix.android.internal.legacy.util.Log; +import im.vector.matrix.android.internal.legacy.data.Room; + +public class RoomMemberCountCondition extends Condition { + + private static final String LOG_TAG = RoomMemberCountCondition.class.getSimpleName(); + + // NB: Leave the strings in order of descending length + private static final String[] PREFIX_ARR = new String[]{"==", "<=", ">=", "<", ">", ""}; + + public String is; + private String comparisonPrefix = null; + private int limit; + private boolean parseError = false; + + public RoomMemberCountCondition() { + kind = Condition.KIND_ROOM_MEMBER_COUNT; + } + + @Override + public String toString() { + return "RoomMemberCountCondition{" + "is='" + is + "'}'"; + } + + @SuppressWarnings("SimplifiableIfStatement") + public boolean isSatisfied(Room room) { + // sanity check + if (room == null) return false; + + if (parseError) return false; + + // Parse the is field into prefix and number the first time + if (comparisonPrefix == null) { + parseIsField(); + if (parseError) return false; + } + + int numMembers = room.getNumberOfJoinedMembers(); + + if ("==".equals(comparisonPrefix) || "".equals(comparisonPrefix)) { + return numMembers == limit; + } + if ("<".equals(comparisonPrefix)) { + return numMembers < limit; + } + if (">".equals(comparisonPrefix)) { + return numMembers > limit; + } + if ("<=".equals(comparisonPrefix)) { + return numMembers <= limit; + } + if (">=".equals(comparisonPrefix)) { + return numMembers >= limit; + } + + return false; + } + + /** + * Parse the is field to extract meaningful information. + */ + protected void parseIsField() { + for (String prefix : PREFIX_ARR) { + if (is.startsWith(prefix)) { + comparisonPrefix = prefix; + break; + } + } + + if (comparisonPrefix == null) { + parseError = true; + } else { + try { + limit = Integer.parseInt(is.substring(comparisonPrefix.length())); + } catch (NumberFormatException e) { + parseError = true; + } + } + + if (parseError) { + Log.e(LOG_TAG, "parsing error : " + is); + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/SenderNotificationPermissionCondition.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/SenderNotificationPermissionCondition.java new file mode 100644 index 0000000000..d8ada7b959 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/SenderNotificationPermissionCondition.java @@ -0,0 +1,37 @@ +/* + * 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.rest.model.bingrules; + +import im.vector.matrix.android.internal.legacy.rest.model.PowerLevels; + +public class SenderNotificationPermissionCondition extends Condition { + private static final String LOG_TAG = SenderNotificationPermissionCondition.class.getSimpleName(); + + public String key; + + public SenderNotificationPermissionCondition() { + kind = Condition.KIND_SENDER_NOTIFICATION_PERMISSION; + } + + public boolean isSatisfied(PowerLevels powerLevels, String userId) { + return (null != powerLevels) && (null != userId) && powerLevels.getUserPowerLevel(userId) >= powerLevels.notificationLevel(key); + } + + @Override + public String toString() { + return "SenderNotificationPermissionCondition{" + "key=" + key; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/UnknownCondition.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/UnknownCondition.java new file mode 100644 index 0000000000..c94fcf4e8a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/bingrules/UnknownCondition.java @@ -0,0 +1,36 @@ +/* + * 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.rest.model.bingrules; + +import im.vector.matrix.android.internal.legacy.rest.model.Event; + +public class UnknownCondition extends Condition { + public UnknownCondition() { + kind = Condition.KIND_UNKNOWN; + } + + // unknown conditions: we previously matched all unknown conditions, + // but given that rules can be added to the base rules on a server, + // it's probably better to not match unknown conditions. + public boolean isSatisfied(Event event) { + return false; + } + + @Override + public String toString() { + return "UnknownCondition"; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/EncryptedBodyFileInfo.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/EncryptedBodyFileInfo.java new file mode 100644 index 0000000000..40f5221aa5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/EncryptedBodyFileInfo.java @@ -0,0 +1,35 @@ +/* + * 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.rest.model.crypto; + +import org.matrix.olm.OlmPkMessage; + +public class EncryptedBodyFileInfo { + public String ciphertext; + public String mac; + public String ephemeral; + + /** + * Build from a OlmPkMessage object + * + * @param olmPkMessage OlmPkMessage + */ + public EncryptedBodyFileInfo(OlmPkMessage olmPkMessage) { + ciphertext = olmPkMessage.mCipherText; + mac = olmPkMessage.mMac; + ephemeral = olmPkMessage.mEphemeralKey; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/EncryptedEventContent.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/EncryptedEventContent.java new file mode 100644 index 0000000000..fe340654ef --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/EncryptedEventContent.java @@ -0,0 +1,47 @@ +/* + * 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.rest.model.crypto; + +/** + * Class representing an encrypted event content + */ +public class EncryptedEventContent implements java.io.Serializable { + + /** + * the used algorithm + */ + public String algorithm; + + /** + * The encrypted event + */ + public String ciphertext; + + /** + * The device id + */ + public String device_id; + + /** + * the sender key + */ + public String sender_key; + + /** + * The session id + */ + public String session_id; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/EncryptedFileInfo.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/EncryptedFileInfo.java new file mode 100644 index 0000000000..cb4229ae04 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/EncryptedFileInfo.java @@ -0,0 +1,50 @@ +/* + * 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.rest.model.crypto; + +import java.io.Serializable; +import java.util.Map; + +public class EncryptedFileInfo implements Serializable{ + public String url; + public String mimetype; + public EncryptedFileKey key; + public String iv; + public Map hashes; + public String v; + + /** + * Make a deep copy. + * @return the copy + */ + public EncryptedFileInfo deepCopy() { + EncryptedFileInfo encryptedFile = new EncryptedFileInfo(); + encryptedFile.url = url; + encryptedFile.mimetype = mimetype; + + if (null != key) { + encryptedFile.key = key.deepCopy(); + } + + encryptedFile.iv = iv; + encryptedFile.hashes = hashes; + + return encryptedFile; + } +} + + + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/EncryptedFileKey.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/EncryptedFileKey.java new file mode 100644 index 0000000000..b5ae311c68 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/EncryptedFileKey.java @@ -0,0 +1,47 @@ +/* + * 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.rest.model.crypto; + +import java.io.Serializable; +import java.util.List; + +public class EncryptedFileKey implements Serializable { + public String alg; + public Boolean ext; + public List key_ops; + public String kty; + public String k; + + /** + * Make a deep copy. + * + * @return the copy + */ + public EncryptedFileKey deepCopy() { + EncryptedFileKey encryptedFileKey = new EncryptedFileKey(); + + encryptedFileKey.alg = alg; + encryptedFileKey.ext = ext; + encryptedFileKey.key_ops = key_ops; + encryptedFileKey.kty = kty; + encryptedFileKey.k = k; + + return encryptedFileKey; + } +} + + + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/ForwardedRoomKeyContent.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/ForwardedRoomKeyContent.java new file mode 100644 index 0000000000..a3385aaeb3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/ForwardedRoomKeyContent.java @@ -0,0 +1,37 @@ +/* + * 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.rest.model.crypto; + +import java.util.List; + +/** + * Class representing the forward room key request body content + */ +public class ForwardedRoomKeyContent implements java.io.Serializable { + public String algorithm; + + public String room_id; + + public String sender_key; + + public String session_id; + + public String session_key; + + public List forwarding_curve25519_key_chain; + + public String sender_claimed_ed25519_key; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/KeyChangesResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/KeyChangesResponse.java new file mode 100644 index 0000000000..1865b276cb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/KeyChangesResponse.java @@ -0,0 +1,29 @@ +/* + * 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.rest.model.crypto; + +import java.util.List; + +/** + * This class describes the key changes response + */ +public class KeyChangesResponse { + // list of user ids which have new devices + public List changed; + + // List of user ids who are no more tracked. + public List left; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/KeysClaimResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/KeysClaimResponse.java new file mode 100644 index 0000000000..fa37443871 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/KeysClaimResponse.java @@ -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.rest.model.crypto; + +import java.util.Map; + +/** + * This class represents the response to /keys/query request made by claimOneTimeKeysForUsersDevices. + */ +public class KeysClaimResponse { + /** + * The requested keys ordered by device by user. + */ + public Map>>> oneTimeKeys; +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/KeysQueryResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/KeysQueryResponse.java new file mode 100644 index 0000000000..62e1acba1d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/KeysQueryResponse.java @@ -0,0 +1,37 @@ +/* + * 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.rest.model.crypto; + +import im.vector.matrix.android.internal.legacy.crypto.data.MXDeviceInfo; + +import java.util.Map; + +/** + * This class represents the response to /keys/query request made by downloadKeysForUsers + */ +public class KeysQueryResponse { + /** + * The device keys per devices per users. + */ + public Map> deviceKeys; + + /** + * The failures sorted by homeservers. + */ + public Map> failures; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/KeysUploadResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/KeysUploadResponse.java new file mode 100644 index 0000000000..5b35cb7ccc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/KeysUploadResponse.java @@ -0,0 +1,62 @@ +/* + * 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.rest.model.crypto; + +import android.text.TextUtils; + +import java.util.Map; + +/** + * This class represents the response to /keys/upload request made by uploadKeys. + */ +public class KeysUploadResponse { + + /** + * The count per algorithm as returned by the home server: a map (algorithm to count). + */ + public Map oneTimeKeyCounts; + + /** + * Helper methods to extract information from 'oneTimeKeyCounts' + * + * @param algorithm the expected algorithm + * @return the time key counts + */ + public int oneTimeKeyCountsForAlgorithm(String algorithm) { + int res = 0; + + if ((null != oneTimeKeyCounts) && !TextUtils.isEmpty(algorithm)) { + Integer val = oneTimeKeyCounts.get(algorithm); + + if (null != val) { + res = val.intValue(); + } + } + + return res; + } + + /** + * Tells if there is a oneTimeKeys for a dedicated algorithm. + * + * @param algorithm the algorithm + * @return true if it is found + */ + public boolean hasOneTimeKeyCountsForAlgorithm(String algorithm) { + return (null != oneTimeKeyCounts) && (null != algorithm) && oneTimeKeyCounts.containsKey(algorithm); + } +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/NewDeviceContent.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/NewDeviceContent.java new file mode 100644 index 0000000000..05eb31c256 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/NewDeviceContent.java @@ -0,0 +1,28 @@ +/* + * 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.rest.model.crypto; + +import java.util.List; + +public class NewDeviceContent { + private static final String LOG_TAG = "NewDeviceContent"; + + // the device id + public String deviceId; + + // the room ids list + public List rooms; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/OlmEventContent.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/OlmEventContent.java new file mode 100644 index 0000000000..67def0ce94 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/OlmEventContent.java @@ -0,0 +1,38 @@ +/* + * 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.rest.model.crypto; + +import java.util.Map; + +/** + * Class representing an encrypted event content + */ +public class OlmEventContent implements java.io.Serializable { + /** + * + */ + public Map ciphertext; + + /** + * The device id + */ + //public String device_id; + + /** + * the sender key + */ + public String sender_key; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/OlmPayloadContent.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/OlmPayloadContent.java new file mode 100644 index 0000000000..6ff0eea758 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/OlmPayloadContent.java @@ -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.rest.model.crypto; + +import java.util.Map; + +/** + * Class representing the OLM payload content + */ +public class OlmPayloadContent implements java.io.Serializable { + /** + * The room id + */ + public String room_id; + + /** + * The sender + */ + public String sender; + + /** + * The receipient + */ + public String recipient; + + /** + * the recipient keys + */ + public Map recipient_keys; + + /** + * The keys + */ + public Map keys; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/RoomKeyContent.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/RoomKeyContent.java new file mode 100644 index 0000000000..034a64924a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/RoomKeyContent.java @@ -0,0 +1,33 @@ +/* + * 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.rest.model.crypto; + +/** + * Class representing an sharekey content + */ +public class RoomKeyContent implements java.io.Serializable { + + public String algorithm; + + public String room_id; + + public String session_id; + + public String session_key; + + // should be a Long but it is sometimes a double + public Object chain_index; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/RoomKeyRequest.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/RoomKeyRequest.java new file mode 100644 index 0000000000..d3c60d9d74 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/RoomKeyRequest.java @@ -0,0 +1,32 @@ +/* + * 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.rest.model.crypto; + +/** + * Class representing an room key request content + */ +public class RoomKeyRequest implements java.io.Serializable { + public static final String ACTION_REQUEST = "request"; + public static final String ACTION_REQUEST_CANCELLATION = "request_cancellation"; + + public String action; + + public String requesting_device_id; + + public String request_id; + + public RoomKeyRequestBody body; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/RoomKeyRequestBody.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/RoomKeyRequestBody.java new file mode 100644 index 0000000000..7e6b91561c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/crypto/RoomKeyRequestBody.java @@ -0,0 +1,29 @@ +/* + * 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.rest.model.crypto; + +/** + * Class representing an room key request body content + */ +public class RoomKeyRequestBody implements java.io.Serializable { + public String algorithm; + + public String room_id; + + public String sender_key; + + public String session_id; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/filter/Filter.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/filter/Filter.java new file mode 100644 index 0000000000..bdf480685a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/filter/Filter.java @@ -0,0 +1,55 @@ +/* + * Copyright 2018 Matthias Kesler + * 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.rest.model.filter; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +/** + * Represents "Filter" as mentioned in the SPEC + * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter + */ +public class Filter { + + public Integer limit; + + public List senders; + + @SerializedName("not_senders") + public List notSenders; + + public List types; + + @SerializedName("not_types") + public List notTypes; + + public List rooms; + + @SerializedName("not_rooms") + public List notRooms; + + public boolean hasData() { + return limit != null + || senders != null + || notSenders != null + || types != null + || notTypes != null + || rooms != null + || notRooms != null; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/filter/FilterBody.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/filter/FilterBody.java new file mode 100644 index 0000000000..0806c6e868 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/filter/FilterBody.java @@ -0,0 +1,54 @@ +/* + * Copyright 2018 Matthias Kesler + * 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.rest.model.filter; + +import com.google.gson.annotations.SerializedName; + +import im.vector.matrix.android.internal.legacy.util.JsonUtils; + +import java.util.List; + +/** + * Class which can be parsed to a filter json string. Used for POST and GET + * Have a look here for further information: + * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter + */ +public class FilterBody { + public static final String LOG_TAG = FilterBody.class.getSimpleName(); + + @SerializedName("event_fields") + public List eventFields; + + @SerializedName("event_format") + public String eventFormat; + + public Filter presence; + + @SerializedName("account_data") + public Filter accountData; + + public RoomFilter room; + + @Override + public String toString() { + return LOG_TAG + toJSONString(); + } + + public String toJSONString() { + return JsonUtils.getGson(false).toJson(this); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/filter/FilterResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/filter/FilterResponse.java new file mode 100644 index 0000000000..4fdb005445 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/filter/FilterResponse.java @@ -0,0 +1,29 @@ +/* + * Copyright 2018 Matthias Kesler + * 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.rest.model.filter; + +import com.google.gson.annotations.SerializedName; + +/** + * Represents the body which is the response when creating a filter on the server + * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter + */ +public class FilterResponse { + + @SerializedName("filter_id") + public String filterId; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/filter/RoomEventFilter.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/filter/RoomEventFilter.java new file mode 100644 index 0000000000..54c8c4817f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/filter/RoomEventFilter.java @@ -0,0 +1,69 @@ +/* + * Copyright 2018 Matthias Kesler + * 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.rest.model.filter; + +import com.google.gson.annotations.SerializedName; + +import im.vector.matrix.android.internal.legacy.util.JsonUtils; + +import java.util.List; + +/** + * Represents "RoomEventFilter" as mentioned in the SPEC + * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter + */ +public class RoomEventFilter { + + public Integer limit; + + @SerializedName("not_senders") + public List notSenders; + + @SerializedName("not_types") + public List notTypes; + + public List senders; + + public List types; + + public List rooms; + + @SerializedName("not_rooms") + public List notRooms; + + @SerializedName("contains_url") + public Boolean containsUrl; + + @SerializedName("lazy_load_members") + public Boolean lazyLoadMembers; + + public boolean hasData() { + return limit != null + || notSenders != null + || notTypes != null + || senders != null + || types != null + || rooms != null + || notRooms != null + || containsUrl != null + || lazyLoadMembers != null; + } + + public String toJSONString() { + return JsonUtils.getGson(false).toJson(this); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/filter/RoomFilter.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/filter/RoomFilter.java new file mode 100644 index 0000000000..1861a075f1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/filter/RoomFilter.java @@ -0,0 +1,55 @@ +/* + * Copyright 2018 Matthias Kesler + * 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.rest.model.filter; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +/** + * Represents "RoomFilter" as mentioned in the SPEC + * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter + */ +public class RoomFilter { + + @SerializedName("not_rooms") + public List notRooms; + + public List rooms; + + public RoomEventFilter ephemeral; + + @SerializedName("include_leave") + public Boolean includeLeave; + + public RoomEventFilter state; + + public RoomEventFilter timeline; + + @SerializedName("account_data") + public RoomEventFilter accountData; + + public boolean hasData() { + return notRooms != null + || rooms != null + || ephemeral != null + || includeLeave != null + || state != null + || timeline != null + || accountData != null; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/AcceptGroupInvitationParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/AcceptGroupInvitationParams.java new file mode 100644 index 0000000000..53f3a28984 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/AcceptGroupInvitationParams.java @@ -0,0 +1,22 @@ +/* + * 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.rest.model.group; + +/** + * Accept an invitation in a group + */ +public class AcceptGroupInvitationParams { +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/AddGroupParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/AddGroupParams.java new file mode 100644 index 0000000000..251cb29658 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/AddGroupParams.java @@ -0,0 +1,22 @@ +/* + * 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.rest.model.group; + +/** + * Room addition to a group + */ +public class AddGroupParams { +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/CreateGroupParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/CreateGroupParams.java new file mode 100644 index 0000000000..80cabed899 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/CreateGroupParams.java @@ -0,0 +1,32 @@ +/* + * 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.rest.model.group; + + +/** + * Group creation params + */ +public class CreateGroupParams { + /** + * The group local part + */ + public String localpart; + + /** + * The group profile + */ + public GroupProfile profile; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/CreateGroupResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/CreateGroupResponse.java new file mode 100644 index 0000000000..47caaab401 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/CreateGroupResponse.java @@ -0,0 +1,26 @@ +/* + * 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.rest.model.group; + +/** + * Group creation response + */ +public class CreateGroupResponse { + /** + * The group Id + */ + public String group_id; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GetGroupsResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GetGroupsResponse.java new file mode 100644 index 0000000000..d3058646b3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GetGroupsResponse.java @@ -0,0 +1,28 @@ +/* + * 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.rest.model.group; + +import java.util.List; + +/** + * Get groups list response + */ +public class GetGroupsResponse { + /** + * Group ids list + */ + public List groupIds; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GetPublicisedGroupsResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GetPublicisedGroupsResponse.java new file mode 100644 index 0000000000..258fbf6056 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GetPublicisedGroupsResponse.java @@ -0,0 +1,29 @@ +/* + * 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.rest.model.group; + +import java.util.List; +import java.util.Map; + +/** + * Get groups list response + */ +public class GetPublicisedGroupsResponse { + /** + * Group ids list indexed by userId + */ + public Map> users; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GetUserPublicisedGroupsResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GetUserPublicisedGroupsResponse.java new file mode 100644 index 0000000000..77c542026b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GetUserPublicisedGroupsResponse.java @@ -0,0 +1,28 @@ +/* + * 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.rest.model.group; + +import java.util.List; + +/** + * Get groups list response + */ +public class GetUserPublicisedGroupsResponse { + /** + * Group ids list + */ + public List groups; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/Group.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/Group.java new file mode 100644 index 0000000000..9ccb54f29b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/Group.java @@ -0,0 +1,277 @@ +/* + * 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.rest.model.group; + +import android.text.TextUtils; + +import im.vector.matrix.android.internal.legacy.rest.model.RoomMember; + +import java.io.Serializable; +import java.util.Comparator; + +/** + * This class represents a community in Matrix. + */ +public class Group implements Serializable { + /** + * Sort by group id + */ + public static final Comparator mGroupsComparator = new Comparator() { + public int compare(Group group1, Group group2) { + return group1.getGroupId().compareTo(group2.getGroupId()); + } + }; + + /** + * The group id. + */ + private String mGroupId; + + /** + * The community summary. + */ + private GroupSummary mSummary = new GroupSummary(); + + /** + * The rooms of the community. + */ + private GroupRooms mRooms = new GroupRooms(); + + /** + * The community members. + */ + private GroupUsers mUsers = new GroupUsers(); + + /** + * The community invited members. + */ + private GroupUsers mInvitedUsers = new GroupUsers(); + + /** + * The user membership. + */ + private String mMembership; + + /** + * The identifier of the potential inviter (tells wether an invite is pending for this group). + */ + private String mInviter; + + /** + * Create an instance with a group id. + * + * @param groupId the identifier. + * @return the MXGroup instance. + */ + public Group(String groupId) { + mGroupId = groupId; + } + + /** + * @return the group ID + */ + public String getGroupId() { + return mGroupId; + } + + /** + * Update the group profile. + * + * @param profile the group profile. + */ + public void setGroupProfile(GroupProfile profile) { + if (null == mSummary) { + mSummary = new GroupSummary(); + } + + getGroupSummary().profile = profile; + } + + /** + * @return the group profile + */ + public GroupProfile getGroupProfile() { + if (null != getGroupSummary()) { + return getGroupSummary().profile; + } + + return null; + } + + /** + * @return the group name + */ + public String getDisplayName() { + String name = null; + + if (null != getGroupProfile()) { + name = getGroupProfile().name; + } + + if (TextUtils.isEmpty(name)) { + name = getGroupId(); + } + + return name; + } + + /** + * @return the group long description + */ + public String getLongDescription() { + if (null != getGroupProfile()) { + return getGroupProfile().longDescription; + } + + return null; + } + + /** + * @return the avatar URL + */ + public String getAvatarUrl() { + if (null != getGroupProfile()) { + return getGroupProfile().avatarUrl; + } + + return null; + } + + /** + * @return the short description + */ + public String getShortDescription() { + if (null != getGroupProfile()) { + return getGroupProfile().shortDescription; + } + + return null; + } + + /** + * Tells if the group is public. + * + * @return true if the group is public. + */ + public boolean isPublic() { + return (null != getGroupProfile()) && (null != getGroupProfile().isPublic) && getGroupProfile().isPublic; + } + + /** + * Tells if the user is invited to this group. + * + * @return true if the user is invited + */ + public boolean isInvited() { + return TextUtils.equals(mMembership, RoomMember.MEMBERSHIP_INVITE); + } + + /** + * @return the group summary + */ + public GroupSummary getGroupSummary() { + return mSummary; + } + + /** + * Update the group summary + * + * @param aGroupSummary the new group summary + */ + public void setGroupSummary(GroupSummary aGroupSummary) { + mSummary = aGroupSummary; + } + + /** + * @return the group rooms + */ + public GroupRooms getGroupRooms() { + return mRooms; + } + + /** + * Update the group rooms + * + * @param aGroupRooms the new group rooms + */ + public void setGroupRooms(GroupRooms aGroupRooms) { + mRooms = aGroupRooms; + } + + /** + * @return the group users + */ + public GroupUsers getGroupUsers() { + return mUsers; + } + + /** + * Update the group users + * + * @param aGroupUsers the group users + */ + public void setGroupUsers(GroupUsers aGroupUsers) { + mUsers = aGroupUsers; + } + + /** + * @return the invited group users + */ + public GroupUsers getInvitedGroupUsers() { + return mInvitedUsers; + } + + /** + * Update the invited group users + * + * @param aGroupUsers the group users + */ + public void setInvitedGroupUsers(GroupUsers aGroupUsers) { + mInvitedUsers = aGroupUsers; + } + + /** + * Update the membership + * + * @param membership the new membership + */ + public void setMembership(String membership) { + mMembership = membership; + } + + /** + * @return the membership + */ + public String getMembership() { + return mMembership; + } + + /** + * @return the inviter + */ + public String getInviter() { + return mInviter; + } + + /** + * Update the inviter. + * + * @param inviter the inviter. + */ + public void setInviter(String inviter) { + mInviter = inviter; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupInviteUserParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupInviteUserParams.java new file mode 100644 index 0000000000..b534ff5a66 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupInviteUserParams.java @@ -0,0 +1,22 @@ +/* + * 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.rest.model.group; + +/** + * Group user invitation parameters + */ +public class GroupInviteUserParams { +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupInviteUserResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupInviteUserResponse.java new file mode 100644 index 0000000000..03326d7f9a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupInviteUserResponse.java @@ -0,0 +1,30 @@ +/* + * 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.rest.model.group; + +/** + * Group user invitation response + */ +public class GroupInviteUserResponse { + /** + * The user state + *

+ * join - the invitee’s HS immediately accepted the invite + * invite - the invitee’s HS accepted the invite, and then may relay to invitee’s clients + * reject - the invitee’s HS immediately rejected the invite + */ + public String state; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupKickUserParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupKickUserParams.java new file mode 100644 index 0000000000..0e657a0759 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupKickUserParams.java @@ -0,0 +1,22 @@ +/* + * 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.rest.model.group; + +/** + * Group user kick parameters + */ +public class GroupKickUserParams { +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupProfile.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupProfile.java new file mode 100644 index 0000000000..bc73e71bb7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupProfile.java @@ -0,0 +1,46 @@ +/* + * 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.rest.model.group; + +import java.io.Serializable; + +/** + * This class represents a community profile in the server responses. + */ +public class GroupProfile implements Serializable { + + public String shortDescription; + + /** + * Tell whether the group is public. + */ + public Boolean isPublic; + + /** + * The URL for the group's avatar. May be nil. + */ + public String avatarUrl; + + /** + * The group's name. + */ + public String name; + + /** + * The optional HTML formatted string used to described the group. + */ + public String longDescription; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupRoom.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupRoom.java new file mode 100644 index 0000000000..7b06a60afc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupRoom.java @@ -0,0 +1,41 @@ +/* + * 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.rest.model.group; + +import android.text.TextUtils; + +import im.vector.matrix.android.internal.legacy.rest.model.publicroom.PublicRoom; + +/** + * This class represents a room linked to a community + */ +public class GroupRoom extends PublicRoom { + + /** + * @return the display name + */ + public String getDisplayName() { + if (!TextUtils.isEmpty(name)) { + return name; + } + + if (!TextUtils.isEmpty(canonicalAlias)) { + return canonicalAlias; + } + + return roomId; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupRooms.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupRooms.java new file mode 100644 index 0000000000..a04e121c01 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupRooms.java @@ -0,0 +1,53 @@ +/* + * 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.rest.model.group; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * This class represents the group rooms in the server response. + */ +public class GroupRooms implements Serializable { + // estimated room count + public Integer totalRoomCountEstimate; + + // rooms list + public List chunk; + + /** + * @return the rooms list + */ + public List getRoomsList() { + if (null == chunk) { + chunk = new ArrayList<>(); + } + + return chunk; + } + + /** + * @return the estimated rooms count + */ + public int getEstimatedRoomCount() { + if (null == totalRoomCountEstimate) { + totalRoomCountEstimate = getRoomsList().size(); + } + + return totalRoomCountEstimate; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupSummary.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupSummary.java new file mode 100644 index 0000000000..87b768f544 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupSummary.java @@ -0,0 +1,43 @@ +/* + * 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.rest.model.group; + +import java.io.Serializable; + +/** + * This class represents the summary of a community in the server response. + */ +public class GroupSummary implements Serializable { + /** + * The group profile. + */ + public GroupProfile profile; + + /** + * The group users. + */ + public GroupSummaryUsersSection usersSection; + + /** + * The current user status. + */ + public GroupSummaryUser user; + + /** + * The rooms linked to the community. + */ + public GroupSummaryRoomsSection roomsSection; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupSummaryRoomsSection.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupSummaryRoomsSection.java new file mode 100644 index 0000000000..15f88cbb90 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupSummaryRoomsSection.java @@ -0,0 +1,32 @@ +/* + * 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.rest.model.group; + +import java.io.Serializable; +import java.util.List; + +/** + * This class represents the community rooms in a group summary response. + */ +public class GroupSummaryRoomsSection implements Serializable { + + public Integer totalRoomCountEstimate; + + public List rooms; + + // @TODO: Check the meaning and the usage of these categories. This dictionary is empty FTM. + //public Map categories; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupSummaryUser.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupSummaryUser.java new file mode 100644 index 0000000000..09168beeb8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupSummaryUser.java @@ -0,0 +1,34 @@ +/* + * 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.rest.model.group; + +import java.io.Serializable; + +/** + * This class represents the current user status in a group summary response. + */ +public class GroupSummaryUser implements Serializable { + + /** + * The current user membership in this community. + */ + public String membership; + + /** + * Tell whether the user published this community on his profile. + */ + public Boolean isPublicised; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupSummaryUsersSection.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupSummaryUsersSection.java new file mode 100644 index 0000000000..9b56cade8e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupSummaryUsersSection.java @@ -0,0 +1,32 @@ +/* + * 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.rest.model.group; + +import java.io.Serializable; +import java.util.List; + +/** + * This class represents the community members in a group summary response. + */ +public class GroupSummaryUsersSection implements Serializable { + + public Integer totalUserCountEstimate; + + public List users; + + // @TODO: Check the meaning and the usage of these roles. This dictionary is empty FTM. + //public Map roles; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupSyncProfile.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupSyncProfile.java new file mode 100644 index 0000000000..dbc323f20f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupSyncProfile.java @@ -0,0 +1,34 @@ +/* + * 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.rest.model.group; + +import java.io.Serializable; + +/** + * Group sync profile + */ +public class GroupSyncProfile { + /** + * The name of the group, if any. May be nil. + */ + public String name; + + /** + * The URL for the group's avatar. May be nil. + */ + public String avatarUrl; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupUser.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupUser.java new file mode 100644 index 0000000000..9e0aee70f3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupUser.java @@ -0,0 +1,56 @@ +/* + * 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.rest.model.group; + +import java.io.Serializable; + +/** + * This class represents a community member + */ +public class GroupUser implements Serializable { + /** + * The user display name. + */ + public String displayname; + + /** + * The ID of the user. + */ + public String userId; + + /** + * Tell whether the user has a role in the community. + */ + public Boolean isPrivileged; + + /** + * The URL for the user's avatar. May be null. + */ + public String avatarUrl; + + /** + * Tell whether the user's membership is public. + */ + public Boolean isPublic; + + + /** + * @return the user display name + */ + public String getDisplayname() { + return (null != displayname) ? displayname : userId; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupUsers.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupUsers.java new file mode 100644 index 0000000000..742698ec0e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupUsers.java @@ -0,0 +1,71 @@ +/* + * 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.rest.model.group; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This class represents the group users in the server response. + */ +public class GroupUsers implements Serializable { + + public Integer totalUserCountEstimate; + + public List chunk; + + // the server sends some duplicated entries + private List mFilteredUsers; + + /** + * @return the users list + */ + public List getUsers() { + if (null == chunk) { + mFilteredUsers = chunk = new ArrayList<>(); + } else if (null == mFilteredUsers) { + mFilteredUsers = new ArrayList<>(); + + Map map = new HashMap<>(); + + for (GroupUser user : chunk) { + if (null != user.userId) { + map.put(user.userId, user); + } else { + mFilteredUsers.add(user); + } + } + mFilteredUsers.addAll(map.values()); + } + + return mFilteredUsers; + } + + /** + * @return the estimated users count + */ + public int getEstimatedUsersCount() { + if (null == totalUserCountEstimate) { + totalUserCountEstimate = getUsers().size(); + } + + return totalUserCountEstimate; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupsSyncResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupsSyncResponse.java new file mode 100644 index 0000000000..424aa30fb6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/GroupsSyncResponse.java @@ -0,0 +1,40 @@ +/* + * 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.rest.model.group; + +import java.io.Serializable; +import java.util.Map; + +/** + * Group sync response + */ +public class GroupsSyncResponse { + /** + * Joined groups: An array of groups ids. + */ + public Map join; + + /** + * Invitations. The groups that the user has been invited to: keys are groups ids. + */ + public Map invite; + + /** + * Left groups. An array of groups ids: the groups that the user has left or been banned from. + */ + public Map leave; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/InvitedGroupSync.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/InvitedGroupSync.java new file mode 100644 index 0000000000..925417c648 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/InvitedGroupSync.java @@ -0,0 +1,34 @@ +/* + * 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.rest.model.group; + +import java.io.Serializable; + +/** + * invited group sync + */ +public class InvitedGroupSync { + /** + * The identifier of the inviter. + */ + public String inviter; + + /** + * The group profile. + */ + public GroupSyncProfile profile; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/LeaveGroupParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/LeaveGroupParams.java new file mode 100644 index 0000000000..7f720d1a39 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/LeaveGroupParams.java @@ -0,0 +1,22 @@ +/* + * 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.rest.model.group; + +/** + * The leave group params + */ +public class LeaveGroupParams { +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/UpdatePubliciseParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/UpdatePubliciseParams.java new file mode 100644 index 0000000000..1ce14e5f74 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/group/UpdatePubliciseParams.java @@ -0,0 +1,26 @@ +/* + * 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.rest.model.group; + +/** + * Update the Publicise status + */ +public class UpdatePubliciseParams { + /* + * Whether to show the group on a user’s profile, i.e. this doesn’t affect who gets shown on the group's profile. + */ + public Boolean publicise; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/Credentials.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/Credentials.java new file mode 100644 index 0000000000..77d7f2d6d3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/Credentials.java @@ -0,0 +1,78 @@ +/* + * 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.rest.model.login; + +import android.text.TextUtils; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * The user's credentials. + */ +public class Credentials { + public String userId; + public String homeServer; // This is the server name and not a URI, e.g. "matrix.org" + public String accessToken; + public String refreshToken; + public String deviceId; + + public JSONObject toJson() throws JSONException { + JSONObject json = new JSONObject(); + + json.put("user_id", userId); + json.put("home_server", homeServer); + json.put("access_token", accessToken); + json.put("refresh_token", TextUtils.isEmpty(refreshToken) ? JSONObject.NULL : refreshToken); + json.put("device_id", deviceId); + + return json; + } + + public static Credentials fromJson(JSONObject obj) throws JSONException { + Credentials creds = new Credentials(); + creds.userId = obj.getString("user_id"); + creds.homeServer = obj.getString("home_server"); + creds.accessToken = obj.getString("access_token"); + + if (obj.has("device_id")) { + creds.deviceId = obj.getString("device_id"); + } + + // refresh_token is mandatory + if (obj.has("refresh_token")) { + try { + creds.refreshToken = obj.getString("refresh_token"); + } catch (Exception e) { + creds.refreshToken = null; + } + } else { + throw new RuntimeException("refresh_token is required."); + } + + return creds; + } + + @Override + public String toString() { + return "Credentials{" + + "userId='" + userId + '\'' + + ", homeServer='" + homeServer + '\'' + + ", refreshToken.length='" + (refreshToken != null ? refreshToken.length() : "null") + '\'' + + ", accessToken.length='" + (accessToken != null ? accessToken.length() : "null") + '\'' + + '}'; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/LoginFlow.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/LoginFlow.java new file mode 100644 index 0000000000..aea8fb71af --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/LoginFlow.java @@ -0,0 +1,26 @@ +/* + * 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.rest.model.login; + +import java.util.List; + +/** + * A Login flow. + */ +public class LoginFlow implements java.io.Serializable { + public String type; + public List stages; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/LoginFlowResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/LoginFlowResponse.java new file mode 100644 index 0000000000..da5f9be98e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/LoginFlowResponse.java @@ -0,0 +1,25 @@ +/* + * 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.rest.model.login; + +import java.util.List; + +/** + * Response to a GET /login call with the different login flows. + */ +public class LoginFlowResponse { + public List flows; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/LoginParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/LoginParams.java new file mode 100644 index 0000000000..963b56ab45 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/LoginParams.java @@ -0,0 +1,23 @@ +/* + * 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.rest.model.login; + +/** + * Class to pass parameters to the different login types for /login. + */ +public class LoginParams { + public String type; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/PasswordLoginParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/PasswordLoginParams.java new file mode 100644 index 0000000000..d0f0d2b93e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/PasswordLoginParams.java @@ -0,0 +1,146 @@ +/* + * 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.rest.model.login; + +import android.os.Build; +import android.support.annotation.NonNull; +import android.text.TextUtils; + +import im.vector.matrix.android.internal.legacy.rest.client.LoginRestClient; + +import java.util.HashMap; +import java.util.Map; + +/** + * Object to pass to a /login call of type password. + */ +public class PasswordLoginParams extends LoginParams { + public static final String IDENTIFIER_KEY_TYPE_USER = "m.id.user"; + public static final String IDENTIFIER_KEY_TYPE_THIRD_PARTY = "m.id.thirdparty"; + public static final String IDENTIFIER_KEY_TYPE_PHONE = "m.id.phone"; + + public static final String IDENTIFIER_KEY_TYPE = "type"; + public static final String IDENTIFIER_KEY_MEDIUM = "medium"; + public static final String IDENTIFIER_KEY_ADDRESS = "address"; + public static final String IDENTIFIER_KEY_USER = "user"; + public static final String IDENTIFIER_KEY_COUNTRY = "country"; + public static final String IDENTIFIER_KEY_NUMBER = "number"; + + // identifier parameters + public Map identifier; + + // user name login + public String user; + + // email login + public String address; + public String medium; + + // common + public String password; + + // A display name to assign to the newly-created device + public String initial_device_display_name; + + // The device id, used for e2e encryption + public String device_id; + + /** + * Set login params for username/password + * + * @param username the username + * @param password the password + */ + public void setUserIdentifier(@NonNull final String username, @NonNull final String password) { + identifier = new HashMap<>(); + identifier.put(IDENTIFIER_KEY_TYPE, IDENTIFIER_KEY_TYPE_USER); + identifier.put(IDENTIFIER_KEY_USER, username); + // For backward compatibility + user = username; + + setOtherData(password); + } + + /** + * Set login params for 3pid(except phone number)/password + * + * @param medium 3pid type + * @param address 3pid value + * @param password the password + */ + public void setThirdPartyIdentifier(@NonNull final String medium, @NonNull final String address, @NonNull final String password) { + identifier = new HashMap<>(); + identifier.put(IDENTIFIER_KEY_TYPE, IDENTIFIER_KEY_TYPE_THIRD_PARTY); + identifier.put(IDENTIFIER_KEY_MEDIUM, medium); + identifier.put(IDENTIFIER_KEY_ADDRESS, address); + // For backward compatibility + this.medium = medium; + this.address = address; + + setOtherData(password); + } + + /** + * Set login params for phone number/password + * + * @param phoneNumber the phone number + * @param countryCode the country code + * @param password the password + */ + public void setPhoneIdentifier(@NonNull final String phoneNumber, @NonNull final String countryCode, @NonNull final String password) { + identifier = new HashMap<>(); + identifier.put(IDENTIFIER_KEY_TYPE, IDENTIFIER_KEY_TYPE_PHONE); + identifier.put(IDENTIFIER_KEY_NUMBER, phoneNumber); + identifier.put(IDENTIFIER_KEY_COUNTRY, countryCode); + + setOtherData(password); + } + + /** + * Set basic params + * + * @param password the password + */ + private void setOtherData(@NonNull final String password) { + this.password = password; + type = LoginRestClient.LOGIN_FLOW_TYPE_PASSWORD; + initial_device_display_name = Build.MODEL.trim(); + } + + /** + * Set the device name + * + * @param deviceName the new device name + */ + public void setDeviceName(String deviceName) { + if ((null != deviceName) && !TextUtils.isEmpty(deviceName.trim())) { + initial_device_display_name = deviceName.trim(); + } else { + initial_device_display_name = Build.MODEL.trim(); + } + } + + /** + * Set the device Id + * + * @param deviceId the device id, used for e2e encryption + */ + public void setDeviceId(String deviceId) { + device_id = deviceId; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/RegistrationFlowResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/RegistrationFlowResponse.java new file mode 100644 index 0000000000..8dc490e2f1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/RegistrationFlowResponse.java @@ -0,0 +1,47 @@ +/* + * 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.rest.model.login; + +import java.util.List; +import java.util.Map; + +/** + * Response to a POST /register call with the different flows. + */ +public class RegistrationFlowResponse implements java.io.Serializable { + /** + * The list of stages the client has completed successfully. + */ + public List flows; + + /** + * The list of stages the client has completed successfully. + */ + public List completed; + + /** + * The session identifier that the client must pass back to the home server, if one is provided, + * in subsequent attempts to authenticate in the same API call. + */ + public String session; + + /** + * The information that the client will need to know in order to use a given type of authentication. + * For each login stage type presented, that type may be present as a key in this dictionary. + * For example, the public key of reCAPTCHA stage could be given here. + */ + public Map params; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/RegistrationParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/RegistrationParams.java new file mode 100644 index 0000000000..b48c4df929 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/RegistrationParams.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014 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.rest.model.login; + +import java.util.Map; + +/** + * Class to pass parameters to the different registration types for /register. + */ +public class RegistrationParams { + // authentification parameters + public Map auth; + + // the account username + public String username; + + // the account password + public String password; + + // With email + public Boolean bind_email; + + // With phone_number + public Boolean bind_msisdn; + + // device name + public String initial_device_display_name; + + // Temporary flag to notify the server that we support msisdn flow. Used to prevent old app + // versions to end up in fallback because the HS returns the msisdn flow which they don't support + public Boolean x_show_msisdn; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/TokenLoginParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/TokenLoginParams.java new file mode 100644 index 0000000000..dd6bbb3ad8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/TokenLoginParams.java @@ -0,0 +1,16 @@ +package im.vector.matrix.android.internal.legacy.rest.model.login; + + +public class TokenLoginParams extends LoginParams { + public String user; + public String token; + public String txn_id; + + // A display name to assign to the newly-created device + public String initial_device_display_name; + + + public TokenLoginParams() { + type = "m.login.token"; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/TokenRefreshParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/TokenRefreshParams.java new file mode 100644 index 0000000000..9c6b8ce353 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/TokenRefreshParams.java @@ -0,0 +1,7 @@ +package im.vector.matrix.android.internal.legacy.rest.model.login; + + +public class TokenRefreshParams { + public String refresh_token; +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/TokenRefreshResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/TokenRefreshResponse.java new file mode 100644 index 0000000000..647a55ad91 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/login/TokenRefreshResponse.java @@ -0,0 +1,8 @@ +package im.vector.matrix.android.internal.legacy.rest.model.login; + + +public class TokenRefreshResponse { + public String access_token; + public String refresh_token; +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/AudioMessage.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/AudioMessage.java new file mode 100644 index 0000000000..dbab691370 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/AudioMessage.java @@ -0,0 +1,25 @@ +/* + * 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.rest.model.message; + +public class AudioMessage extends FileMessage { + public AudioMessage() { + msgtype = MSGTYPE_AUDIO; + } +} + + + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/FileInfo.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/FileInfo.java new file mode 100644 index 0000000000..465b6090ed --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/FileInfo.java @@ -0,0 +1,33 @@ +/* + * 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.rest.model.message; + +public class FileInfo { + public String mimetype; + public Long size; + + /** + * Make a deep copy. + * + * @return the copy + */ + public FileInfo deepCopy() { + FileInfo copy = new FileInfo(); + copy.mimetype = mimetype; + copy.size = size; + return copy; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/FileMessage.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/FileMessage.java new file mode 100644 index 0000000000..665e867abb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/FileMessage.java @@ -0,0 +1,111 @@ +/* + * 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.rest.model.message; + +import android.content.ClipDescription; +import android.text.TextUtils; + +import im.vector.matrix.android.internal.legacy.crypto.MXEncryptedAttachments; +import im.vector.matrix.android.internal.legacy.rest.model.crypto.EncryptedFileInfo; +import im.vector.matrix.android.internal.legacy.util.Log; + +import android.webkit.MimeTypeMap; + +public class FileMessage extends MediaMessage { + private static final String LOG_TAG = FileMessage.class.getSimpleName(); + + public FileInfo info; + public String url; + + // encrypted medias + // url and thumbnailUrl are replaced by their dedicated file + public EncryptedFileInfo file; + + public FileMessage() { + msgtype = MSGTYPE_FILE; + } + + @Override + public String getUrl() { + if (null != url) { + return url; + } else if (null != file) { + return file.url; + } else { + return null; + } + } + + @Override + public void setUrl(MXEncryptedAttachments.EncryptionResult encryptionResult, String contentUrl) { + if (null != encryptionResult) { + file = encryptionResult.mEncryptedFileInfo; + file.url = contentUrl; + url = null; + } else { + url = contentUrl; + } + } + + /** + * Make a deep copy of this VideoMessage. + * + * @return the copy + */ + public FileMessage deepCopy() { + FileMessage copy = new FileMessage(); + copy.msgtype = msgtype; + copy.body = body; + copy.url = url; + + if (null != info) { + copy.info = info.deepCopy(); + } + + if (null != file) { + copy.file = file.deepCopy(); + } + + return copy; + } + + @Override + public String getMimeType() { + if (null != info) { + // the mimetype was not provided or it's invalid + // some android application set the mimetype to text/uri-list + // it should be fixed on application side but we need to patch it on client side. + if ((TextUtils.isEmpty(info.mimetype) || ClipDescription.MIMETYPE_TEXT_URILIST.equals(info.mimetype)) && (body.indexOf('.') > 0)) { + // the body should contain the filename so try to extract the mimetype from the extension + String extension = body.substring(body.lastIndexOf('.') + 1, body.length()); + + try { + info.mimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase()); + } catch (Exception e) { + Log.e(LOG_TAG, "## getMimeType() : getMimeTypeFromExtensionfailed " + e.getMessage(), e); + } + } + + if (TextUtils.isEmpty(info.mimetype)) { + info.mimetype = "application/octet-stream"; + } + + return info.mimetype; + } else { + return null; + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/ImageInfo.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/ImageInfo.java new file mode 100644 index 0000000000..15c57ff8ca --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/ImageInfo.java @@ -0,0 +1,63 @@ +/* + * 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.rest.model.message; + +import im.vector.matrix.android.internal.legacy.rest.model.crypto.EncryptedFileInfo; + +public class ImageInfo { + public String mimetype; + public Integer w; + public Integer h; + public Long size; + public Integer rotation; + + // ExifInterface.ORIENTATION_XX values + public Integer orientation; + + public ThumbnailInfo thumbnailInfo; + public String thumbnailUrl; + public EncryptedFileInfo thumbnail_file; + + /** + * Make a deep copy. + * + * @return the copy + */ + public ImageInfo deepCopy() { + ImageInfo copy = new ImageInfo(); + copy.mimetype = mimetype; + copy.w = w; + copy.h = h; + copy.size = size; + copy.rotation = rotation; + copy.orientation = orientation; + + if (null != thumbnail_file) { + copy.thumbnail_file = thumbnail_file.deepCopy(); + } + + copy.thumbnailUrl = thumbnailUrl; + + if (null != thumbnailInfo) { + copy.thumbnailInfo = thumbnailInfo.deepCopy(); + } + + return copy; + } +} + + + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/ImageMessage.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/ImageMessage.java new file mode 100644 index 0000000000..d42145ff70 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/ImageMessage.java @@ -0,0 +1,131 @@ +/* + * 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.rest.model.message; + +import android.media.ExifInterface; + +import im.vector.matrix.android.internal.legacy.crypto.MXEncryptedAttachments; +import im.vector.matrix.android.internal.legacy.rest.model.crypto.EncryptedFileInfo; + +public class ImageMessage extends MediaMessage { + public ImageInfo info; + public String url; + + // encrypted medias + // url and thumbnailUrl are replaced by their dedicated file + public EncryptedFileInfo file; + + public ImageMessage() { + msgtype = MSGTYPE_IMAGE; + } + + /** + * Make a deep copy of this ImageMessage. + * FIXME Remove this? + * @return the copy + */ + public ImageMessage deepCopy() { + ImageMessage copy = new ImageMessage(); + copy.msgtype = msgtype; + copy.body = body; + copy.url = url; + + if (null != file) { + copy.file = file.deepCopy(); + } + + return copy; + } + + @Override + public String getUrl() { + if (null != url) { + return url; + } else if (null != file) { + return file.url; + } else { + return null; + } + } + + @Override + public void setUrl(MXEncryptedAttachments.EncryptionResult encryptionResult, String contentUrl) { + if (null != encryptionResult) { + file = encryptionResult.mEncryptedFileInfo; + file.url = contentUrl; + url = null; + } else { + url = contentUrl; + } + } + + @Override + public String getThumbnailUrl() { + if (null != info) { + if (null != info.thumbnail_file) { + return info.thumbnail_file.url; + } else { + return info.thumbnailUrl; + } + } + return null; + } + + @Override + public void setThumbnailUrl(MXEncryptedAttachments.EncryptionResult encryptionResult, String url) { + if (null != encryptionResult) { + info.thumbnail_file = encryptionResult.mEncryptedFileInfo; + info.thumbnail_file.url = url; + info.thumbnailUrl = null; + } else { + info.thumbnailUrl = url; + } + } + + @Override + public String getMimeType() { + if (null != file) { + return file.mimetype; + } else if (null != info) { + return info.mimetype; + } else { + return null; + } + } + + /** + * @return the rotation angle. Integer.MAX_VALUE if not defined. + */ + public int getRotation() { + if ((null != info) && (null != info.rotation)) { + return info.rotation; + } else { + return Integer.MAX_VALUE; + } + } + + /** + * @return the image orientation. ExifInterface.ORIENTATION_UNDEFINED if not defined. + */ + public int getOrientation() { + if ((null != info) && (null != info.orientation)) { + return info.orientation; + } else { + return ExifInterface.ORIENTATION_UNDEFINED; + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/LocationMessage.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/LocationMessage.java new file mode 100644 index 0000000000..d6b96b03fb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/LocationMessage.java @@ -0,0 +1,74 @@ +/* + * 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.rest.model.message; + +import android.net.Uri; + +import im.vector.matrix.android.internal.legacy.util.Log; + +import java.io.File; + +public class LocationMessage extends Message { + private static final String LOG_TAG = "LocationMessage"; + public ThumbnailInfo thumbnail_info; + public String geo_uri; + public String thumbnail_url; + + public LocationMessage() { + msgtype = MSGTYPE_LOCATION; + } + + /** + * Make a deep copy + * @return the copy + */ + public LocationMessage deepCopy() { + LocationMessage copy = new LocationMessage(); + copy.msgtype = msgtype; + copy.body = body; + copy.geo_uri = geo_uri; + copy.thumbnail_url = thumbnail_url; + + if (null != thumbnail_info) { + copy.thumbnail_info = thumbnail_info.deepCopy(); + } + + return copy; + } + + public boolean isLocalThumbnailContent() { + return (null != thumbnail_url) && (thumbnail_url.startsWith("file://")); + } + + /** + * Checks if the media Urls are still valid. + * The media Urls could define a file path. + * They could have been deleted after a media cache cleaning. + */ + public void checkMediaUrls() { + if ((thumbnail_url != null) && thumbnail_url.startsWith("file://")) { + try { + File file = new File(Uri.parse(thumbnail_url).getPath()); + + if (!file.exists()) { + thumbnail_url = null; + } + } catch (Exception e) { + Log.e(LOG_TAG, "## checkMediaUrls() failed " + e.getMessage(), e); + } + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/MediaMessage.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/MediaMessage.java new file mode 100644 index 0000000000..433674147b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/MediaMessage.java @@ -0,0 +1,106 @@ +/* + * 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.rest.model.message; + +import android.net.Uri; + +import im.vector.matrix.android.internal.legacy.crypto.MXEncryptedAttachments; +import im.vector.matrix.android.internal.legacy.util.Log; + +import java.io.File; + +public class MediaMessage extends Message { + public static final String LOG_TAG = MediaMessage.class.getSimpleName(); + + /** + * @return the media URL + */ + public String getUrl() { + return null; + } + + + public void setUrl(MXEncryptedAttachments.EncryptionResult encryptionResult, String url) { + } + + /** + * @return the thumbnail url + */ + public String getThumbnailUrl() { + return null; + } + + public void setThumbnailUrl(MXEncryptedAttachments.EncryptionResult encryptionResult, String url) { + } + + /** + * @return true if the thumbnail is a file url + */ + public boolean isThumbnailLocalContent() { + String thumbUrl = getThumbnailUrl(); + return (null != thumbUrl) && thumbUrl.startsWith("file://"); + } + + /** + * @return true if the media url is a file one. + */ + public boolean isLocalContent() { + String url = getUrl(); + return (null != url) && url.startsWith("file://"); + } + + /** + * @return The image mimetype. null is not defined. + */ + public String getMimeType() { + return null; + } + + /** + * Checks if the media Urls are still valid. + * The media Urls could define a file path. + * They could have been deleted after a media cache cleaning. + */ + public void checkMediaUrls() { + String thumbUrl = getThumbnailUrl(); + + if ((null != thumbUrl) && thumbUrl.startsWith("file://")) { + try { + File file = new File(Uri.parse(thumbUrl).getPath()); + + if (!file.exists()) { + setThumbnailUrl(null, null); + } + } catch (Exception e) { + Log.e(LOG_TAG, "## checkMediaUrls() failed" + e.getMessage(), e); + } + } + + String url = getUrl(); + + if ((url != null) && url.startsWith("file://")) { + try { + File file = new File(Uri.parse(url).getPath()); + + if (!file.exists()) { + setUrl(null, null); + } + } catch (Exception e) { + Log.e(LOG_TAG, "## checkMediaUrls() failed" + e.getMessage(), e); + } + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/Message.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/Message.java new file mode 100644 index 0000000000..0c517cc0c9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/Message.java @@ -0,0 +1,44 @@ +/* + * 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.rest.model.message; + +import com.google.gson.annotations.SerializedName; + +public class Message { + public static final String MSGTYPE_TEXT = "m.text"; + public static final String MSGTYPE_EMOTE = "m.emote"; + public static final String MSGTYPE_NOTICE = "m.notice"; + public static final String MSGTYPE_IMAGE = "m.image"; + public static final String MSGTYPE_AUDIO = "m.audio"; + public static final String MSGTYPE_VIDEO = "m.video"; + public static final String MSGTYPE_LOCATION = "m.location"; + public static final String MSGTYPE_FILE = "m.file"; + public static final String FORMAT_MATRIX_HTML = "org.matrix.custom.html"; + + // Add, in local, a fake message type in order to StickerMessage can inherit Message class + // Because sticker isn't a message type but a event type without msgtype field + public static final String MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker"; + + public String msgtype; + public String body; + + public String format; + public String formatted_body; + + @SerializedName("m.relates_to") + public RelatesTo relatesTo; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/RelatesTo.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/RelatesTo.java new file mode 100644 index 0000000000..8a05528eac --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/RelatesTo.java @@ -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.rest.model.message; + +import com.google.gson.annotations.SerializedName; + +import java.util.Map; + +public class RelatesTo { + @SerializedName("m.in_reply_to") + public Map dict; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/StickerJsonMessage.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/StickerJsonMessage.java new file mode 100644 index 0000000000..e9dd1c82c1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/StickerJsonMessage.java @@ -0,0 +1,28 @@ +/* + * 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.rest.model.message; + + +// It's just an intermediate object to create a StickerMessage from a m.sticker event type. +public class StickerJsonMessage { + + public final String msgtype = Message.MSGTYPE_STICKER_LOCAL; + public String body; + public String url; + public String format; + public ImageInfo info; + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/StickerMessage.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/StickerMessage.java new file mode 100644 index 0000000000..e52fac6fb4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/StickerMessage.java @@ -0,0 +1,53 @@ +/* + * 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.rest.model.message; + + +public class StickerMessage extends ImageMessage { + + public StickerMessage() { + msgtype = MSGTYPE_STICKER_LOCAL; + } + + public StickerMessage(StickerJsonMessage stickerJsonMessage) { + this(); + info = stickerJsonMessage.info; + url = stickerJsonMessage.url; + body = stickerJsonMessage.body; + format = stickerJsonMessage.format; + } + + /** + * Make a deep copy of this StickerMessage. + * + * @return the copy + */ + public StickerMessage deepCopy() { + StickerMessage copy = new StickerMessage(); + copy.info = info; + copy.url = url; + copy.body = body; + copy.format = format; + + if (null != file) { + copy.file = file.deepCopy(); + } + + return copy; + } +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/ThumbnailInfo.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/ThumbnailInfo.java new file mode 100644 index 0000000000..8fc3c8ad1c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/ThumbnailInfo.java @@ -0,0 +1,39 @@ +/* + * 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.rest.model.message; + +public class ThumbnailInfo { + public Integer w; + public Integer h; + public Long size; + public String mimetype; + + /** + * Make a deep copy of this VideoMessage. + * + * @return the copy + */ + public ThumbnailInfo deepCopy() { + ThumbnailInfo copy = new ThumbnailInfo(); + + copy.w = w; + copy.h = h; + copy.size = size; + copy.mimetype = mimetype; + + return copy; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/VideoInfo.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/VideoInfo.java new file mode 100644 index 0000000000..62770fe135 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/VideoInfo.java @@ -0,0 +1,54 @@ +/* + * 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.rest.model.message; + +import im.vector.matrix.android.internal.legacy.rest.model.crypto.EncryptedFileInfo; + +public class VideoInfo { + public Integer h; + public Integer w; + public String mimetype; + public Long duration; + public Long size; + public String thumbnail_url; + public ThumbnailInfo thumbnail_info; + + public EncryptedFileInfo thumbnail_file; + + /** + * Make a deep copy. + * + * @return the copy + */ + public VideoInfo deepCopy() { + VideoInfo copy = new VideoInfo(); + copy.h = h; + copy.w = w; + copy.mimetype = mimetype; + copy.duration = duration; + copy.thumbnail_url = thumbnail_url; + + if (null != thumbnail_info) { + copy.thumbnail_info = thumbnail_info.deepCopy(); + } + + if (null != thumbnail_file) { + copy.thumbnail_file = thumbnail_file.deepCopy(); + } + + return copy; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/VideoMessage.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/VideoMessage.java new file mode 100644 index 0000000000..f6d3f84176 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/message/VideoMessage.java @@ -0,0 +1,108 @@ +/* + * 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.rest.model.message; + +import im.vector.matrix.android.internal.legacy.crypto.MXEncryptedAttachments; +import im.vector.matrix.android.internal.legacy.rest.model.crypto.EncryptedFileInfo; + +public class VideoMessage extends MediaMessage { + + public VideoInfo info; + public String url; + + // encrypted medias + // url and thumbnailUrl are replaced by their dedicated file + public EncryptedFileInfo file; + + public VideoMessage() { + msgtype = MSGTYPE_VIDEO; + } + + @Override + public String getUrl() { + if (null != url) { + return url; + } else if (null != file) { + return file.url; + } else { + return null; + } + } + + @Override + public void setUrl(MXEncryptedAttachments.EncryptionResult encryptionResult, String contentUrl) { + if (null != encryptionResult) { + file = encryptionResult.mEncryptedFileInfo; + file.url = contentUrl; + url = null; + } else { + url = contentUrl; + } + } + + @Override + public String getThumbnailUrl() { + if ((null != info) && (null != info.thumbnail_url)) { + return info.thumbnail_url; + } else if ((null != info) && (null != info.thumbnail_file)) { + return info.thumbnail_file.url; + } + + return null; + } + + @Override + public void setThumbnailUrl(MXEncryptedAttachments.EncryptionResult encryptionResult, String url) { + if (null != encryptionResult) { + info.thumbnail_file = encryptionResult.mEncryptedFileInfo; + info.thumbnail_file.url = url; + info.thumbnail_url = null; + } else { + info.thumbnail_url = url; + } + } + + /** + * Make a deep copy of this VideoMessage. + * + * @return the copy + */ + public VideoMessage deepCopy() { + VideoMessage copy = new VideoMessage(); + copy.url = url; + copy.msgtype = msgtype; + copy.body = body; + + if (null != info) { + copy.info = info.deepCopy(); + } + + if (null != file) { + copy.file = file.deepCopy(); + } + + return copy; + } + + @Override + public String getMimeType() { + if (null != info) { + return info.mimetype; + } else { + return null; + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/AccountThreePidsResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/AccountThreePidsResponse.java new file mode 100644 index 0000000000..60f8e8798c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/AccountThreePidsResponse.java @@ -0,0 +1,25 @@ +/* + * 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.rest.model.pid; + +import java.util.List; + +/** + * Class representing the ThreePids response + */ +public class AccountThreePidsResponse { + public List threepids; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/AddThreePidsParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/AddThreePidsParams.java new file mode 100644 index 0000000000..698f7b2d3a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/AddThreePidsParams.java @@ -0,0 +1,31 @@ +/* + * 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.rest.model.pid; + +import im.vector.matrix.android.internal.legacy.rest.model.ThreePidCreds; + +/** + * Parameters to add a 3Pids to an user + */ +public class AddThreePidsParams { + + // the 3rd party id credentials + public ThreePidCreds three_pid_creds; + + // true when the email has been binded. + public Boolean bind; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/DeleteDeviceAuth.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/DeleteDeviceAuth.java new file mode 100644 index 0000000000..c62e334f50 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/DeleteDeviceAuth.java @@ -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.rest.model.pid; + +/** + * This class provides the + */ +public class DeleteDeviceAuth { + + // device device session id + public String session; + + // registration information + public String type; + public String user; + public String password; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/DeleteDeviceParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/DeleteDeviceParams.java new file mode 100644 index 0000000000..899aa9e866 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/DeleteDeviceParams.java @@ -0,0 +1,23 @@ +/* + * 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.rest.model.pid; + +/** + * This class provides the parameter to delete a device + */ +public class DeleteDeviceParams { + public DeleteDeviceAuth auth; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/DeleteThreePidParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/DeleteThreePidParams.java new file mode 100644 index 0000000000..d5977dc90a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/DeleteThreePidParams.java @@ -0,0 +1,29 @@ +/* + * 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.rest.model.pid; + +/** + * Parameters to delete a 3Pid of a user + */ +public class DeleteThreePidParams { + + // the 3pid medium (email, phone number, etc.) + public String medium; + + // the msisdn that will be deleted from the account + public String address; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/Invite3Pid.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/Invite3Pid.java new file mode 100755 index 0000000000..e28e71527c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/Invite3Pid.java @@ -0,0 +1,39 @@ +/* + * 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.rest.model.pid; + +/** + * + */ +public class Invite3Pid { + /** + * Required. + * The hostname+port of the identity server which should be used for third party identifier lookups. + */ + public String id_server; + + /** + * Required. + * The kind of address being passed in the address field, for example email. + */ + public String medium; + + /** + * Required. + * The invitee's third party identifier. + */ + public String address; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/PidResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/PidResponse.java new file mode 100755 index 0000000000..c4766323a5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/PidResponse.java @@ -0,0 +1,20 @@ +/* + * 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.rest.model.pid; + +public class PidResponse { + public String mxid; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/RoomThirdPartyInvite.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/RoomThirdPartyInvite.java new file mode 100644 index 0000000000..6e246d10b7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/RoomThirdPartyInvite.java @@ -0,0 +1,65 @@ +/* + * 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.rest.model.pid; + +/** + * Class representing a room member: a user with membership information. + */ +public class RoomThirdPartyInvite implements java.io.Serializable { + + /** + * The user display name as provided by the home sever. + */ + public String display_name; + + /** + * The token generated by the identity server. + */ + public String token; + + // the event used to build this class + private String mOriginalEventId = null; + + /** + * @return a RoomThirdPartyInvite deep copy. + */ + public RoomThirdPartyInvite deepCopy() { + RoomThirdPartyInvite copy = new RoomThirdPartyInvite(); + copy.display_name = display_name; + copy.token = token; + copy.mOriginalEventId = mOriginalEventId; + return copy; + } + + /** + * Set the original used to create this class + * + * @param eventId the event id + */ + public void setOriginalEventid(String eventId) { + mOriginalEventId = eventId; + } + + /** + * Provides the even used to create this class + * + * @return the event uses to create this class + */ + public String getOriginalEventId() { + return mOriginalEventId; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/ThirdPartyIdentifier.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/ThirdPartyIdentifier.java new file mode 100755 index 0000000000..dc3328303d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/ThirdPartyIdentifier.java @@ -0,0 +1,45 @@ +/* + * 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.rest.model.pid; + +import java.io.Serializable; + +public class ThirdPartyIdentifier implements Serializable { + /** + * The medium of the third party identifier (ThreePid.MEDIUM_XXX) + */ + public String medium; + + /** + * The third party identifier address. + */ + public String address; + + /** + * The timestamp in milliseconds when this 3PID has been validated. + * Define as Object because it should be Long and it is a Double. + * So, it might change. + */ + public Object validatedAt; + + /** + * The timestamp in milliseconds when this 3PID has been added to the user account. + * Define as Object because it should be Long and it is a Double. + * So, it might change. + */ + public Object addedAt; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/ThirdPartyProtocol.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/ThirdPartyProtocol.java new file mode 100755 index 0000000000..530d752ca7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/ThirdPartyProtocol.java @@ -0,0 +1,36 @@ +/* + * 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.rest.model.pid; + +import java.util.List; +import java.util.Map; + +/** + * This class describes the third party server protocols. + */ +public class ThirdPartyProtocol { + // the user fields (domain, nick, username...) + public List userFields; + + // the location fields (domain, channels, room...) + public List locationFields; + + // the field types + public Map> fieldTypes; + + // the protocol instance + public List instances; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/ThirdPartyProtocolInstance.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/ThirdPartyProtocolInstance.java new file mode 100755 index 0000000000..a7197098de --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/ThirdPartyProtocolInstance.java @@ -0,0 +1,43 @@ +/* + * 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.rest.model.pid; + +import java.io.Serializable; +import java.util.Map; + +/** + * This class describes a third party protocol instance + */ +public class ThirdPartyProtocolInstance implements Serializable { + + // the network identifier + public String networkId; + + // the fields (domain...) + public Map fields; + + // the instance id + public String instanceId; + + // the description + public String desc; + + // the dedicated bot + public String botUserId; + + // the icon URL + public String icon; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/ThreePid.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/ThreePid.java new file mode 100755 index 0000000000..617ac6f3f7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/pid/ThreePid.java @@ -0,0 +1,293 @@ +/* + * 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.rest.model.pid; + +import android.content.Context; +import android.text.TextUtils; + +import im.vector.matrix.android.R; +import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback; +import im.vector.matrix.android.internal.legacy.rest.client.ProfileRestClient; +import im.vector.matrix.android.internal.legacy.rest.client.ThirdPidRestClient; +import im.vector.matrix.android.internal.legacy.rest.model.MatrixError; +import im.vector.matrix.android.internal.legacy.rest.model.RequestEmailValidationResponse; +import im.vector.matrix.android.internal.legacy.rest.model.RequestPhoneNumberValidationResponse; + +import java.util.UUID; + +/** + * 3 pid + */ +public class ThreePid implements java.io.Serializable { + /** + * Types of third party media. + * The list is not exhaustive and depends on the Identity server capabilities. + */ + public static final String MEDIUM_EMAIL = "email"; + public static final String MEDIUM_MSISDN = "msisdn"; + + // state + public static final int AUTH_STATE_TOKEN_UNKNOWN = 0; + public static final int AUTH_STATE_TOKEN_REQUESTED = 1; + public static final int AUTH_STATE_TOKEN_RECEIVED = 2; + public static final int AUTH_STATE_TOKEN_SUBMITTED = 3; + public static final int AUTH_STATE_TOKEN_AUTHENTIFICATED = 4; + + /** + * Types of third party media. + */ + public String medium; + + /** + * The email of the user + * Used when MEDIUM_EMAIL + */ + public String emailAddress; + + /** + * The phone number of the user + * Used when MEDIUM_MSISDN + */ + public String phoneNumber; + + /** + * The country of the user + * Usedwhen MEDIUM_MSISDN + */ + public String country; + + /** + * The current client secret key used during email validation. + */ + public String clientSecret; + + /** + * The current session identifier during email validation. + */ + public String sid; + + /** + * The number of attempts + */ + public int sendAttempt; + + /** + * Current validation state (AUTH_STATE_XXX) + */ + private int mValidationState; + + /** + * Two params constructors (MEDIUM_EMAIL) + * + * @param emailAddress the email address. + * @param medium the identifier medium, MEDIUM_EMAIL in that case + */ + public ThreePid(String emailAddress, String medium) { + this.medium = medium; + this.emailAddress = emailAddress; + + if (TextUtils.equals(MEDIUM_EMAIL, medium) && !TextUtils.isEmpty(emailAddress)) { + this.emailAddress = emailAddress.toLowerCase(); + } + + clientSecret = UUID.randomUUID().toString(); + } + + /** + * Build a ThreePid with the given phone number and country (MEDIUM_MSISDN) + * + * @param phoneNumber the phone number (national or international format) + * @param country country code of the phone number (can be empty if phone number has international format and starts by "+") + * @param medium the identifier medium, MEDIUM_MSISDN in that case + */ + public ThreePid(String phoneNumber, String country, String medium) { + this.medium = medium; + this.phoneNumber = phoneNumber; + this.country = country == null ? "" : country.toUpperCase(); + + clientSecret = UUID.randomUUID().toString(); + } + + /** + * Clear the validation parameters + */ + private void resetValidationParameters() { + mValidationState = AUTH_STATE_TOKEN_UNKNOWN; + + clientSecret = UUID.randomUUID().toString(); + sendAttempt = 1; + sid = null; + } + + /** + * Request an email validation token. + * + * @param restClient the rest client to use. + * @param nextLink the nextLink + * @param isDuringRegistration true if it is added during a registration + * @param callback the callback when the operation is done + */ + public void requestEmailValidationToken(final ProfileRestClient restClient, + final String nextLink, + final boolean isDuringRegistration, + final ApiCallback callback) { + // sanity check + if (null != restClient && mValidationState != AUTH_STATE_TOKEN_REQUESTED) { + + if (mValidationState != AUTH_STATE_TOKEN_UNKNOWN) { + resetValidationParameters(); + } + + mValidationState = AUTH_STATE_TOKEN_REQUESTED; + restClient.requestEmailValidationToken(emailAddress, clientSecret, sendAttempt, nextLink, isDuringRegistration, + new ApiCallback() { + + @Override + public void onSuccess(RequestEmailValidationResponse requestEmailValidationResponse) { + + if (TextUtils.equals(requestEmailValidationResponse.clientSecret, clientSecret)) { + mValidationState = AUTH_STATE_TOKEN_RECEIVED; + sid = requestEmailValidationResponse.sid; + callback.onSuccess(null); + } + } + + private void commonError() { + sendAttempt++; + mValidationState = AUTH_STATE_TOKEN_UNKNOWN; + } + + @Override + public void onNetworkError(Exception e) { + commonError(); + callback.onNetworkError(e); + } + + @Override + public void onMatrixError(MatrixError e) { + commonError(); + callback.onMatrixError(e); + } + + @Override + public void onUnexpectedError(Exception e) { + commonError(); + callback.onUnexpectedError(e); + } + }); + + } + } + + /** + * Request a phone number validation token. + * + * @param restClient the rest client to use. + * @param isDuringRegistration true if it is added during a registration + * @param callback the callback when the operation is done + */ + public void requestPhoneNumberValidationToken(final ProfileRestClient restClient, final boolean isDuringRegistration, + final ApiCallback callback) { + // sanity check + if ((null != restClient) && (mValidationState != AUTH_STATE_TOKEN_REQUESTED)) { + + if (mValidationState != AUTH_STATE_TOKEN_UNKNOWN) { + resetValidationParameters(); + } + + mValidationState = AUTH_STATE_TOKEN_REQUESTED; + + restClient.requestPhoneNumberValidationToken(phoneNumber, country, clientSecret, sendAttempt, isDuringRegistration, + new ApiCallback() { + + @Override + public void onSuccess(RequestPhoneNumberValidationResponse requestPhoneNumberValidationResponse) { + + if (TextUtils.equals(requestPhoneNumberValidationResponse.clientSecret, clientSecret)) { + mValidationState = AUTH_STATE_TOKEN_RECEIVED; + sid = requestPhoneNumberValidationResponse.sid; + callback.onSuccess(null); + } + } + + private void commonError() { + sendAttempt++; + mValidationState = AUTH_STATE_TOKEN_UNKNOWN; + } + + @Override + public void onNetworkError(Exception e) { + commonError(); + callback.onNetworkError(e); + } + + @Override + public void onMatrixError(MatrixError e) { + commonError(); + callback.onMatrixError(e); + } + + @Override + public void onUnexpectedError(Exception e) { + commonError(); + callback.onUnexpectedError(e); + } + }); + } + } + + /** + * Request the ownership validation of an email address or a phone number previously set + * by {@link #requestEmailValidationToken(ProfileRestClient, String, boolean, ApiCallback)} + * + * @param restClient REST client + * @param token the token generated by the requestEmailValidationToken or requestPhoneNumberValidationToken call + * @param clientSecret the client secret which was supplied in the requestEmailValidationToken or requestPhoneNumberValidationToken call + * @param sid the sid for the session + * @param respCallback asynchronous callback response + */ + public void submitValidationToken(final ThirdPidRestClient restClient, final String token, final String clientSecret, + final String sid, final ApiCallback respCallback) { + // sanity check + if (null != restClient) { + restClient.submitValidationToken(medium, token, clientSecret, sid, respCallback); + } + } + + /** + * Get the friendly name of the medium + * + * @param medium medium of the 3pid + * @param context the context + * @return friendly name of the medium + */ + public static String getMediumFriendlyName(final String medium, final Context context) { + String mediumFriendlyName = ""; + switch (medium) { + case MEDIUM_EMAIL: + mediumFriendlyName = context.getString(R.string.medium_email); + break; + case MEDIUM_MSISDN: + mediumFriendlyName = context.getString(R.string.medium_phone_number); + break; + } + + return mediumFriendlyName; + } + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/publicroom/PublicRoom.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/publicroom/PublicRoom.java new file mode 100644 index 0000000000..29219cbab5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/publicroom/PublicRoom.java @@ -0,0 +1,53 @@ +/* + * 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.rest.model.publicroom; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +/** + * Class representing the objects returned by /publicRooms call. + */ +public class PublicRoom { + + public List aliases; + + @SerializedName("canonical_alias") + public String canonicalAlias; + + public String name; + + // number of members which have joined the room (the members list is not provided) + @SerializedName("num_joined_members") + public int numJoinedMembers; + + @SerializedName("room_id") + public String roomId; + + public String topic; + + // true when the room history is visible (room preview) + @SerializedName("world_readable") + public boolean worldReadable; + + // a guest can join the room + @SerializedName("guest_can_join") + public boolean guestCanJoin; + + @SerializedName("avatar_url") + public String avatarUrl; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/publicroom/PublicRoomsFilter.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/publicroom/PublicRoomsFilter.java new file mode 100644 index 0000000000..65a114bcdf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/publicroom/PublicRoomsFilter.java @@ -0,0 +1,26 @@ +/* + * 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.rest.model.publicroom; + +/** + * Class to define a filter to retrieve public rooms + */ +public class PublicRoomsFilter { + /** + * String to search for + **/ + public String generic_search_term; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/publicroom/PublicRoomsParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/publicroom/PublicRoomsParams.java new file mode 100644 index 0000000000..5f5fe88cc3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/publicroom/PublicRoomsParams.java @@ -0,0 +1,47 @@ +/* + * 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.rest.model.publicroom; + +/** + * Class to pass parameters to get the public rooms list + */ +public class PublicRoomsParams { + /** + * The third party instance id + */ + public String thirdPartyInstanceId; + + /** + * Tell if the query must be done in all the connected networks. + */ + public boolean includeAllNetworks; + + /** + * Maximum number of entries to return + **/ + public Integer limit; + + /** + * token to paginate from + **/ + public String since; + + /** + * Filter parameters + **/ + public PublicRoomsFilter filter; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/publicroom/PublicRoomsResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/publicroom/PublicRoomsResponse.java new file mode 100644 index 0000000000..6f76ae75c0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/publicroom/PublicRoomsResponse.java @@ -0,0 +1,43 @@ +/* + * 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.rest.model.publicroom; + +import java.util.List; + +/** + * Class representing the public rooms request response + */ +public class PublicRoomsResponse { + /** + * token to forward paginate + **/ + public String next_batch; + + /** + * token to back paginate + **/ + public String prev_batch; + + /** + * public rooms list + **/ + public List chunk; + + /** + * number of unfiltered existing rooms + **/ + public Integer total_room_count_estimate; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchCategories.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchCategories.java new file mode 100644 index 0000000000..b344c9e688 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchCategories.java @@ -0,0 +1,28 @@ +/* + * 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.rest.model.search; + +/** + * subclass representing a search API response + */ +public class SearchCategories { + + /** + * Mapping of category name to search criteria. + */ + public SearchRoomEventResults roomEvents; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchEventContext.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchEventContext.java new file mode 100644 index 0000000000..baab968dda --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchEventContext.java @@ -0,0 +1,53 @@ +/* + * 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.rest.model.search; + +import im.vector.matrix.android.internal.legacy.rest.model.Event; + +import java.util.List; +import java.util.Map; + +/** + * subclass representing a search API response + */ +public class SearchEventContext { + /** + * Pagination token for the start of the chunk. + */ + public String start; + + /** + * Pagination token for the end of the chunk. + */ + public String end; + + /** + * Events just before the result. + */ + public List eventsBefore; + + /** + * Events just after the result. + */ + public List eventsAfter; + + /** + * The historic profile information of the users that sent the events returned. + * The key is the user id, the value the user profile. + */ + public Map profileInfo; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchGroup.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchGroup.java new file mode 100644 index 0000000000..5090293582 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchGroup.java @@ -0,0 +1,30 @@ +/* + * 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.rest.model.search; + +import java.util.Map; + +/** + * subclass representing a search API response + */ +public class SearchGroup { + /** + * Total number of results found. + * The key is "room_id" (TODO_SEARCH) , the value the group. + */ + public Map group; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchGroupContent.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchGroupContent.java new file mode 100644 index 0000000000..09bc8b79d8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchGroupContent.java @@ -0,0 +1,38 @@ +/* + * 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.rest.model.search; + +import java.util.List; + +/** + * subclass representing a search API response + */ +public class SearchGroupContent { + /** + * Which results are in this group. + */ + public List results; + + /** + * Key that can be used to order different groups. + */ + public Integer order; + + /** + * Token that can be used to get the next batch of results in the group, if exists. + */ + public String nextBatch; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchParams.java new file mode 100644 index 0000000000..58f0eb6394 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchParams.java @@ -0,0 +1,27 @@ +/* + * 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.rest.model.search; + +import java.util.Map; + +/** + * Class representing a search parameters + */ +public class SearchParams { + // the search categories + public Map search_categories; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchResponse.java new file mode 100644 index 0000000000..a416e10f5e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchResponse.java @@ -0,0 +1,28 @@ +/* + * 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.rest.model.search; + +/** + * subclass representing a search API response + */ +public class SearchResponse { + + /** + * Categories to search in and their criteria.. + */ + public SearchCategories searchCategories; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchResult.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchResult.java new file mode 100644 index 0000000000..db58a9efff --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchResult.java @@ -0,0 +1,40 @@ +/* + * 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.rest.model.search; + +import im.vector.matrix.android.internal.legacy.rest.model.Event; + +/** + * subclass representing a search API response + */ +public class SearchResult { + + /** + * The event that matched. + */ + public Event result; + + /** + * A number that describes how closely this result matches the search. Higher is closer. + */ + public Double rank; + + /** + * Context for result, if requested. + */ + public SearchEventContext context; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchRoomEventCategoryParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchRoomEventCategoryParams.java new file mode 100644 index 0000000000..eb03ab1286 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchRoomEventCategoryParams.java @@ -0,0 +1,37 @@ +/* + * 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.rest.model.search; + +import java.util.Map; + +/** + * Class representing the room events search category parameters + */ +public class SearchRoomEventCategoryParams { + // the searched text + public String search_term; + + // the sort order + public String order_by; + + // the event context + public Map event_context; + + // the search filters + public Map filter; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchRoomEventResults.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchRoomEventResults.java new file mode 100644 index 0000000000..180b547a94 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchRoomEventResults.java @@ -0,0 +1,56 @@ +/* + * 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.rest.model.search; + +import im.vector.matrix.android.internal.legacy.rest.model.Event; + +import java.util.List; +import java.util.Map; + +/** + * Class representing a search API response + */ +public class SearchRoomEventResults { + /** + * Total number of results found. + */ + public Integer count; + + /** + * List of results in the requested order. + */ + public List results; + + /** + * The current state for every room in the results. + * This is included if the request had the include_state key set with a value of true. + * The key is the roomId, the value its state. (TODO_SEARCH: right?) + */ + public Map> state; + + /** + * Any groups that were requested. + * The key is the group id (TODO_SEARCH). + */ + public Map groups; + + /** + * Token that can be used to get the next batch of results in the group, if exists. + */ + public String nextBatch; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchUserProfile.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchUserProfile.java new file mode 100644 index 0000000000..f439e3282b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchUserProfile.java @@ -0,0 +1,31 @@ +/* + * 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.rest.model.search; + +/** + * subclass representing a search API response + */ +public class SearchUserProfile { + /** + * The avatar URL for this user, if any. + */ + public String avatarUrl; + + /** + * The display name for this user, if any. + */ + public String displayName; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchUsersParams.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchUsersParams.java new file mode 100644 index 0000000000..05d76a5248 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchUsersParams.java @@ -0,0 +1,27 @@ +/* + * 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.rest.model.search; + +/** + * Class representing an user search parameters + */ +public class SearchUsersParams { + // the searched term + public String search_term; + + // set a limit to the request response + public Integer limit; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchUsersRequestResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchUsersRequestResponse.java new file mode 100644 index 0000000000..7ff7435a67 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchUsersRequestResponse.java @@ -0,0 +1,38 @@ +/* + * 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.rest.model.search; + +import java.util.List; + +/** + * Class representing an users search response + */ +public class SearchUsersRequestResponse { + + // cannot use org.matrix.androidsdk.rest.model.User + // because the display name does not have the same syntax + public class User { + public String user_id; + public String display_name; + public String avatar_url; + } + + // indicates if the result list has been truncated by the limit. + public Boolean limited; + + // set a limit to the request response + public List results; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchUsersResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchUsersResponse.java new file mode 100644 index 0000000000..94f7af7e3c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/search/SearchUsersResponse.java @@ -0,0 +1,32 @@ +/* + * 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.rest.model.search; + +import im.vector.matrix.android.internal.legacy.rest.model.User; + +import java.util.List; + +/** + * Class representing an users search response + */ +public class SearchUsersResponse { + + // indicates if the result list has been truncated by the limit. + public Boolean limited; + + // set a limit to the request response + public List results; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/DeviceInfo.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/DeviceInfo.java new file mode 100644 index 0000000000..625d804d39 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/DeviceInfo.java @@ -0,0 +1,70 @@ +/* + * 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.rest.model.sync; + +import im.vector.matrix.android.internal.legacy.data.comparator.Comparators; +import im.vector.matrix.android.internal.legacy.interfaces.DatedObject; + +import java.util.Collections; +import java.util.List; + +/** + * This class describes the device information + */ +public class DeviceInfo implements DatedObject { + /** + * The owner user id + */ + public String user_id; + + /** + * The device id + */ + public String device_id; + + /** + * The device display name + */ + public String display_name; + + /** + * The last time this device has been seen. + */ + public long last_seen_ts = 0; + + /** + * The last ip address + */ + public String last_seen_ip; + + @Override + public long getDate() { + return last_seen_ts; + } + + /** + * Sort a devices list by their presences from the most recent to the oldest one. + * + * @param deviceInfos the deviceinfo list + */ + public static void sortByLastSeen(List deviceInfos) { + if (null != deviceInfos) { + Collections.sort(deviceInfos, Comparators.descComparator); + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/DeviceListResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/DeviceListResponse.java new file mode 100644 index 0000000000..8a4b0e68a7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/DeviceListResponse.java @@ -0,0 +1,29 @@ +/* + * 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.rest.model.sync; + +import java.util.List; + +/** + * This class describes the device list response from a sync request + */ +public class DeviceListResponse { + // user ids list which have new crypto devices + public List changed; + + // List of user ids who are no more tracked. + public List left; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/DeviceOneTimeKeysCountSyncResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/DeviceOneTimeKeysCountSyncResponse.java new file mode 100644 index 0000000000..f336148b28 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/DeviceOneTimeKeysCountSyncResponse.java @@ -0,0 +1,23 @@ +/* + * 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.rest.model.sync; + +public class DeviceOneTimeKeysCountSyncResponse { + + public Integer signed_curve25519; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/DevicesListResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/DevicesListResponse.java new file mode 100644 index 0000000000..d0bf1dba89 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/DevicesListResponse.java @@ -0,0 +1,25 @@ +/* + * 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.rest.model.sync; + +import java.util.List; + +/** + * This class describes the + */ +public class DevicesListResponse { + public List devices; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/InvitedRoomSync.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/InvitedRoomSync.java new file mode 100644 index 0000000000..9e66108079 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/InvitedRoomSync.java @@ -0,0 +1,33 @@ +/* + * 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.rest.model.sync; + +import com.google.gson.annotations.SerializedName; + +// InvitedRoomSync represents a room invitation during server sync v2. +public class InvitedRoomSync { + + /** + * The state of a room that the user has been invited to. These state events may only have the 'sender', 'type', 'state_key' + * and 'content' keys present. These events do not replace any state that the client already has for the room, for example if + * the client has archived the room. Instead the client should keep two separate copies of the state: the one from the 'invite_state' + * and one from the archived 'state'. If the client joins the room then the current state will be given as a delta against the + * archived 'state' not the 'invite_state'. + */ + @SerializedName("invite_state") + public RoomInviteState inviteState; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/PresenceSyncResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/PresenceSyncResponse.java new file mode 100644 index 0000000000..caf4d95163 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/PresenceSyncResponse.java @@ -0,0 +1,30 @@ +/* + * 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.rest.model.sync; + +import im.vector.matrix.android.internal.legacy.rest.model.Event; + +import java.util.List; + +// PresenceSyncResponse represents the updates to the presence status of other users during server sync v2. +public class PresenceSyncResponse { + + /** + * List of presence events (array of Event with type m.presence). + */ + public List events; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomInviteState.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomInviteState.java new file mode 100644 index 0000000000..85f3e7cee9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomInviteState.java @@ -0,0 +1,30 @@ +/* + * 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.rest.model.sync; + +import im.vector.matrix.android.internal.legacy.rest.model.Event; + +import java.util.List; + +// RoomInviteState represents the state of a room that the user has been invited to. +public class RoomInviteState { + + /** + * List of state events (array of MXEvent). + */ + public List events; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomResponse.java new file mode 100644 index 0000000000..7b39063fd1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomResponse.java @@ -0,0 +1,57 @@ +/* + * 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.rest.model.sync; + +import im.vector.matrix.android.internal.legacy.rest.model.Event; +import im.vector.matrix.android.internal.legacy.rest.model.TokensChunkEvents; + +import java.util.List; + +/** + * Class representing a room from a JSON response from room or global initial sync. + */ +public class RoomResponse { + // The room identifier. + public String roomId; + + // The last recent messages of the room. + public TokensChunkEvents messages; + + // The state events. + public List state; + + // The private data that this user has attached to this room. + public List accountData; + + // The current user membership in this room. + public String membership; + + // The room visibility (public/private). + public String visibility; + + // The matrix id of the inviter in case of pending invitation. + public String inviter; + + // The invite event if membership is invite. + public Event invite; + + // The presence status of other users (Provided in case of room initial sync @see http://matrix.org/docs/api/client-server/#!/-rooms/get_room_sync_data)). + public List presence; + + // The read receipts (Provided in case of room initial sync). + public List receipts; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomSync.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomSync.java new file mode 100644 index 0000000000..81595074fd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomSync.java @@ -0,0 +1,54 @@ +/* + * 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.rest.model.sync; + +import com.google.gson.annotations.SerializedName; + +// RoomSync represents the response for a room during server sync v2. +public class RoomSync { + /** + * The state updates for the room. + */ + public RoomSyncState state; + + /** + * The timeline of messages and state changes in the room. + */ + public RoomSyncTimeline timeline; + + /** + * The ephemeral events in the room that aren't recorded in the timeline or state of the room (e.g. typing, receipts). + */ + public RoomSyncEphemeral ephemeral; + + /** + * The account data events for the room (e.g. tags). + */ + public RoomSyncAccountData accountData; + + /** + The notification counts for the room. + */ + public RoomSyncUnreadNotifications unreadNotifications; + + /** + * The room summary + */ + @SerializedName("summary") + public RoomSyncSummary roomSyncSummary; + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomSyncAccountData.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomSyncAccountData.java new file mode 100644 index 0000000000..fdc2e1c8a8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomSyncAccountData.java @@ -0,0 +1,29 @@ +/* + * 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.rest.model.sync; + +import im.vector.matrix.android.internal.legacy.rest.model.Event; + +import java.util.List; + +// RoomSyncAccountData represents the account data events for a room. +public class RoomSyncAccountData { + /** + * List of account data events (array of Event). + */ + public List events; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomSyncEphemeral.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomSyncEphemeral.java new file mode 100644 index 0000000000..d57b29dbde --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomSyncEphemeral.java @@ -0,0 +1,29 @@ +/* + * 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.rest.model.sync; + +import im.vector.matrix.android.internal.legacy.rest.model.Event; + +import java.util.List; + +// RoomSyncEphemeral represents the ephemeral events in the room that aren't recorded in the timeline or state of the room (e.g. typing). +public class RoomSyncEphemeral { + /** + * List of ephemeral events. + */ + public List events; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomSyncState.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomSyncState.java new file mode 100644 index 0000000000..a1d65aa546 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomSyncState.java @@ -0,0 +1,31 @@ +/* + * 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.rest.model.sync; + +import im.vector.matrix.android.internal.legacy.rest.model.Event; + +import java.util.List; + +// RoomSyncState represents the state updates for a room during server sync v2. +public class RoomSyncState { + + /** + * List of state events. The resulting state corresponds to the *start* of the timeline. + */ + public List events; + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomSyncSummary.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomSyncSummary.java new file mode 100644 index 0000000000..399a4dea56 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomSyncSummary.java @@ -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.rest.model.sync; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +/** + * See https://docs.google.com/document/d/11i14UI1cUz-OJ0knD5BFu7fmT6Fo327zvMYqfSAR7xs + */ +public class RoomSyncSummary { + + /** + * Present only if the room has no m.room.name or m.room.canonical_alias. + *

+ * Lists the mxids of the first 5 members in the room who are currently joined or invited (ordered by stream ordering as seen on the server, + * to avoid it jumping around if/when topological order changes). As the heroes’ membership status changes, the list changes appropriately + * (sending the whole new list in the next /sync response). This list always excludes the current logged in user. If there are no joined or + * invited users, it lists the parted and banned ones instead. Servers can choose to send more or less than 5 members if they must, but 5 + * seems like a good enough number for most naming purposes. Clients should use all the provided members to name the room, but may truncate + * the list if helpful for UX + */ + @SerializedName("m.heroes") + public List heroes; + + /** + * The number of m.room.members in state 'joined' (including the syncing user) (can be null) + */ + @SerializedName("m.joined_member_count") + public Integer joinedMembersCount; + + /** + * The number of m.room.members in state 'invited' (can be null) + */ + @SerializedName("m.invited_member_count") + public Integer invitedMembersCount; +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomSyncTimeline.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomSyncTimeline.java new file mode 100644 index 0000000000..01c557b232 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomSyncTimeline.java @@ -0,0 +1,43 @@ +/* + * 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.rest.model.sync; + +import im.vector.matrix.android.internal.legacy.rest.model.Event; + +import java.util.List; + +/** + * RoomSyncTimeline represents the timeline of messages and state changes for a room during server sync v2. + */ +public class RoomSyncTimeline { + + /** + * List of events. + */ + public List events; + + /** + * Boolean which tells whether there are more events on the server + * In the case of an incremental sync, if the value is true, it's means that there is a gap between known events and received events + */ + public boolean limited; + + /** + * If the batch was limited then this is a token that can be supplied to the server to retrieve more events + */ + public String prevBatch; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomSyncUnreadNotifications.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomSyncUnreadNotifications.java new file mode 100644 index 0000000000..34a869d7e1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomSyncUnreadNotifications.java @@ -0,0 +1,41 @@ +/* + * 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.rest.model.sync; + +import im.vector.matrix.android.internal.legacy.rest.model.Event; + +import java.util.List; + +/** + `MXRoomSyncUnreadNotifications` represents the unread counts for a room. + */ +public class RoomSyncUnreadNotifications { + /** + * List of account data events (array of Event). + */ + public List events; + + /** + * The number of unread messages that match the push notification rules. + */ + public Integer notificationCount; + + /** + * The number of highlighted unread messages (subset of notifications). + */ + public Integer highlightCount; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomsSyncResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomsSyncResponse.java new file mode 100644 index 0000000000..179e3b2dba --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/RoomsSyncResponse.java @@ -0,0 +1,38 @@ +/* + * 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.rest.model.sync; + +import java.util.Map; + +// RoomsSyncResponse represents the rooms list in server sync v2 response. +public class RoomsSyncResponse { + + /** + * Joined rooms: keys are rooms ids. + */ + public Map join; + + /** + * Invitations. The rooms that the user has been invited to: keys are rooms ids. + */ + public Map invite; + + /** + * Left rooms. The rooms that the user has left or been banned from: keys are rooms ids. + */ + public Map leave; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/SyncResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/SyncResponse.java new file mode 100644 index 0000000000..f13d133b8b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/SyncResponse.java @@ -0,0 +1,67 @@ +/* + * 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.rest.model.sync; + +import im.vector.matrix.android.internal.legacy.rest.model.group.GroupsSyncResponse; + +import java.util.Map; + +// SyncResponse represents the request response for server sync v2. +public class SyncResponse { + + /** + * The user private data. + */ + public Map accountData; + + /** + * The opaque token for the end. + */ + public String nextBatch; + + /** + * The updates to the presence status of other users. + */ + public PresenceSyncResponse presence; + + /* + * Data directly sent to one of user's devices. + */ + public ToDeviceSyncResponse toDevice; + + /** + * List of rooms. + */ + public RoomsSyncResponse rooms; + + /** + * Devices list update + */ + public DeviceListResponse deviceLists; + + /** + * One time keys management + */ + public DeviceOneTimeKeysCountSyncResponse deviceOneTimeKeysCount; + + /** + * List of groups. + */ + public GroupsSyncResponse groups; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/ToDeviceSyncResponse.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/ToDeviceSyncResponse.java new file mode 100644 index 0000000000..5293459ec6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/rest/model/sync/ToDeviceSyncResponse.java @@ -0,0 +1,31 @@ +/* + * 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.rest.model.sync; + +import im.vector.matrix.android.internal.legacy.rest.model.Event; + +import java.util.List; + +// ToDeviceSyncResponse represents the data directly sent to one of user's devices. +public class ToDeviceSyncResponse { + + /** + * List of direct-to-device events. + */ + public List events; +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/ssl/CertUtil.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/ssl/CertUtil.java new file mode 100644 index 0000000000..0e577f81c7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/ssl/CertUtil.java @@ -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.ssl; + +import android.util.Pair; + +import im.vector.matrix.android.internal.legacy.HomeServerConnectionConfig; +import im.vector.matrix.android.internal.legacy.util.Log; + +import java.security.KeyStore; +import java.security.MessageDigest; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import okhttp3.CipherSuite; +import okhttp3.ConnectionSpec; +import okhttp3.TlsVersion; + +/** + * Various utility classes for dealing with X509Certificates + */ +public class CertUtil { + private static final String LOG_TAG = CertUtil.class.getSimpleName(); + + /** + * Generates the SHA-256 fingerprint of the given certificate + * + * @param cert the certificate. + * @return the finger print + * @throws CertificateException the certificate exception + */ + public static byte[] generateSha256Fingerprint(X509Certificate cert) throws CertificateException { + return generateFingerprint(cert, "SHA-256"); + } + + /** + * Generates the SHA-1 fingerprint of the given certificate + * + * @param cert the certificated + * @return the SHA1 fingerprint + * @throws CertificateException the certificate exception + */ + public static byte[] generateSha1Fingerprint(X509Certificate cert) throws CertificateException { + return generateFingerprint(cert, "SHA-1"); + } + + /** + * Generate the fingerprint for a dedicated type. + * + * @param cert the certificate + * @param type the type + * @return the fingerprint + * @throws CertificateException certificate exception + */ + private static byte[] generateFingerprint(X509Certificate cert, String type) throws CertificateException { + final byte[] fingerprint; + final MessageDigest md; + try { + md = MessageDigest.getInstance(type); + } catch (Exception e) { + // This really *really* shouldn't throw, as java should always have a SHA-256 and SHA-1 impl. + throw new CertificateException(e); + } + + fingerprint = md.digest(cert.getEncoded()); + + return fingerprint; + } + + final private static char[] hexArray = "0123456789ABCDEF".toCharArray(); + + /** + * Convert the fingerprint to an hexa string. + * + * @param fingerprint the fingerprint + * @return the hexa string. + */ + public static String fingerprintToHexString(byte[] fingerprint) { + return fingerprintToHexString(fingerprint, ' '); + } + + public static String fingerprintToHexString(byte[] fingerprint, char sep) { + char[] hexChars = new char[fingerprint.length * 3]; + for (int j = 0; j < fingerprint.length; j++) { + int v = fingerprint[j] & 0xFF; + hexChars[j * 3] = hexArray[v >>> 4]; + hexChars[j * 3 + 1] = hexArray[v & 0x0F]; + hexChars[j * 3 + 2] = sep; + } + return new String(hexChars, 0, hexChars.length - 1); + } + + /** + * Recursively checks the exception to see if it was caused by an + * UnrecognizedCertificateException + * + * @param e the throwable. + * @return The UnrecognizedCertificateException if exists, else null. + */ + public static UnrecognizedCertificateException getCertificateException(Throwable e) { + int i = 0; // Just in case there is a getCause loop + while (e != null && i < 10) { + if (e instanceof UnrecognizedCertificateException) { + return (UnrecognizedCertificateException) e; + } + e = e.getCause(); + i++; + } + + return null; + } + + /** + * Create a SSLSocket factory for a HS config. + * + * @param hsConfig the HS config. + * @return SSLSocket factory + */ + public static Pair newPinnedSSLSocketFactory(HomeServerConnectionConfig hsConfig) { + try { + X509TrustManager defaultTrustManager = null; + + // If we haven't specified that we wanted to pin the certs, fallback to standard + // X509 checks if fingerprints don't match. + if (!hsConfig.shouldPin()) { + TrustManagerFactory tf = null; + + // get the PKIX instance + try { + tf = TrustManagerFactory.getInstance("PKIX"); + } catch (Exception e) { + Log.e(LOG_TAG, "## newPinnedSSLSocketFactory() : TrustManagerFactory.getInstance failed " + e.getMessage(), e); + } + + // it doesn't exist, use the default one. + if (null == tf) { + try { + tf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + } catch (Exception e) { + Log.e(LOG_TAG, "## addRule : onBingRuleUpdateFailure failed " + e.getMessage(), e); + } + } + + tf.init((KeyStore) null); + TrustManager[] trustManagers = tf.getTrustManagers(); + + for (int i = 0; i < trustManagers.length; i++) { + if (trustManagers[i] instanceof X509TrustManager) { + defaultTrustManager = (X509TrustManager) trustManagers[i]; + break; + } + } + } + + TrustManager[] trustPinned = new TrustManager[]{ + new PinnedTrustManager(hsConfig.getAllowedFingerprints(), defaultTrustManager) + }; + + SSLSocketFactory sslSocketFactory; + + if (hsConfig.forceUsageOfTlsVersions() && hsConfig.getAcceptedTlsVersions() != null) { + // Force usage of accepted Tls Versions for Android < 20 + sslSocketFactory = new TLSSocketFactory(trustPinned, hsConfig.getAcceptedTlsVersions()); + } else { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustPinned, new java.security.SecureRandom()); + sslSocketFactory = sslContext.getSocketFactory(); + } + + return new Pair<>(sslSocketFactory, defaultTrustManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Create a Host name verifier for a hs config. + * + * @param hsConfig the hs config. + * @return a new HostnameVerifier. + */ + public static HostnameVerifier newHostnameVerifier(HomeServerConnectionConfig hsConfig) { + final HostnameVerifier defaultVerifier = HttpsURLConnection.getDefaultHostnameVerifier(); + final List trusted_fingerprints = hsConfig.getAllowedFingerprints(); + + return new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + if (defaultVerifier.verify(hostname, session)) return true; + if (trusted_fingerprints == null || trusted_fingerprints.size() == 0) return false; + + // If remote cert matches an allowed fingerprint, just accept it. + try { + for (Certificate cert : session.getPeerCertificates()) { + for (Fingerprint allowedFingerprint : trusted_fingerprints) { + if (allowedFingerprint != null && cert instanceof X509Certificate && allowedFingerprint.matchesCert((X509Certificate) cert)) { + return true; + } + } + } + } catch (SSLPeerUnverifiedException e) { + return false; + } catch (CertificateException e) { + return false; + } + + return false; + } + }; + } + + /** + * Create a list of accepted TLS specifications for a hs config. + * + * @param hsConfig the hs config. + * @return a list of accepted TLS specifications. + */ + public static List newConnectionSpecs(HomeServerConnectionConfig hsConfig) { + final ConnectionSpec.Builder builder = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS); + + final List tlsVersions = hsConfig.getAcceptedTlsVersions(); + if (null != tlsVersions) { + builder.tlsVersions(tlsVersions.toArray(new TlsVersion[0])); + } + + final List tlsCipherSuites = hsConfig.getAcceptedTlsCipherSuites(); + if (null != tlsCipherSuites) { + builder.cipherSuites(tlsCipherSuites.toArray(new CipherSuite[0])); + } + + builder.supportsTlsExtensions(hsConfig.shouldAcceptTlsExtensions()); + + List list = new ArrayList<>(); + + list.add(builder.build()); + + if (hsConfig.isHttpConnectionAllowed()) { + list.add(ConnectionSpec.CLEARTEXT); + } + + return list; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/ssl/Fingerprint.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/ssl/Fingerprint.java new file mode 100644 index 0000000000..1c5a720852 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/ssl/Fingerprint.java @@ -0,0 +1,133 @@ +/* + * 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.ssl; + +import android.util.Base64; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +/** + * Represents a X509 Certificate fingerprint. + */ +public class Fingerprint { + public enum HashType {SHA1, SHA256} + + private final byte[] mBytes; + private final HashType mHashType; + private String mDisplayableHexRepr; + + public Fingerprint(byte[] bytes, HashType hashType) { + mBytes = bytes; + mHashType = hashType; + mDisplayableHexRepr = null; + } + + public static Fingerprint newSha256Fingerprint(X509Certificate cert) throws CertificateException { + return new Fingerprint( + CertUtil.generateSha256Fingerprint(cert), + HashType.SHA256 + ); + } + + public static Fingerprint newSha1Fingerprint(X509Certificate cert) throws CertificateException { + return new Fingerprint( + CertUtil.generateSha1Fingerprint(cert), + HashType.SHA1 + ); + } + + public HashType getType() { + return mHashType; + } + + public byte[] getBytes() { + return mBytes; + } + + public String getBytesAsHexString() { + if (mDisplayableHexRepr == null) { + mDisplayableHexRepr = CertUtil.fingerprintToHexString(mBytes); + } + + return mDisplayableHexRepr; + } + + public JSONObject toJson() throws JSONException { + JSONObject obj = new JSONObject(); + obj.put("bytes", Base64.encodeToString(getBytes(), Base64.DEFAULT)); + obj.put("hash_type", mHashType.toString()); + return obj; + } + + public static Fingerprint fromJson(JSONObject obj) throws JSONException { + String hashTypeStr = obj.getString("hash_type"); + byte[] fingerprintBytes = Base64.decode(obj.getString("bytes"), Base64.DEFAULT); + + final HashType hashType; + if ("SHA256".equalsIgnoreCase(hashTypeStr)) { + hashType = HashType.SHA256; + } else if ("SHA1".equalsIgnoreCase(hashTypeStr)) { + hashType = HashType.SHA1; + } else { + throw new JSONException("Unrecognized hash type: " + hashTypeStr); + } + + return new Fingerprint(fingerprintBytes, hashType); + } + + public boolean matchesCert(X509Certificate cert) throws CertificateException { + Fingerprint o = null; + switch (mHashType) { + case SHA256: + o = Fingerprint.newSha256Fingerprint(cert); + break; + case SHA1: + o = Fingerprint.newSha1Fingerprint(cert); + break; + } + + return equals(o); + } + + public String toString() { + return String.format("Fingerprint{type: '%s', fingeprint: '%s'}", mHashType.toString(), getBytesAsHexString()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Fingerprint that = (Fingerprint) o; + + if (!Arrays.equals(mBytes, that.mBytes)) return false; + return mHashType == that.mHashType; + + } + + @Override + public int hashCode() { + int result = mBytes != null ? Arrays.hashCode(mBytes) : 0; + result = 31 * result + (mHashType != null ? mHashType.hashCode() : 0); + return result; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/ssl/PinnedTrustManager.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/ssl/PinnedTrustManager.java new file mode 100644 index 0000000000..15925c7942 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/ssl/PinnedTrustManager.java @@ -0,0 +1,101 @@ +/* + * 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.ssl; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.List; + +import javax.net.ssl.X509TrustManager; + +/** + * Implements a TrustManager that checks Certificates against an explicit list of known + * fingerprints. + */ +public class PinnedTrustManager implements X509TrustManager { + private final List mFingerprints; + private final X509TrustManager mDefaultTrustManager; + + /** + * @param fingerprints An array of SHA256 cert fingerprints + * @param defaultTrustManager Optional trust manager to fall back on if cert does not match + * any of the fingerprints. Can be null. + */ + public PinnedTrustManager(List fingerprints, X509TrustManager defaultTrustManager) { + mFingerprints = fingerprints; + mDefaultTrustManager = defaultTrustManager; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String s) throws CertificateException { + try { + if (mDefaultTrustManager != null) { + mDefaultTrustManager.checkClientTrusted( + chain, s + ); + return; + } + } catch (CertificateException e) { + // If there is an exception we fall back to checking fingerprints + if (mFingerprints == null || mFingerprints.size() == 0) { + throw new UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.getCause()); + } + } + checkTrusted("client", chain); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String s) throws CertificateException { + try { + if (mDefaultTrustManager != null) { + mDefaultTrustManager.checkServerTrusted( + chain, s + ); + return; + } + } catch (CertificateException e) { + // If there is an exception we fall back to checking fingerprints + if (mFingerprints == null || mFingerprints.size() == 0) { + throw new UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.getCause()); + } + } + checkTrusted("server", chain); + } + + private void checkTrusted(String type, X509Certificate[] chain) throws CertificateException { + X509Certificate cert = chain[0]; + + boolean found = false; + if (mFingerprints != null) { + for (Fingerprint allowedFingerprint : mFingerprints) { + if (allowedFingerprint != null && allowedFingerprint.matchesCert(cert)) { + found = true; + break; + } + } + } + + if (!found) { + throw new UnrecognizedCertificateException(cert, Fingerprint.newSha256Fingerprint(cert), null); + } + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/ssl/TLSSocketFactory.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/ssl/TLSSocketFactory.java new file mode 100644 index 0000000000..60e6ecc6ed --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/ssl/TLSSocketFactory.java @@ -0,0 +1,134 @@ +/* + * 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.ssl; + +import im.vector.matrix.android.internal.legacy.util.Log; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; + +import okhttp3.TlsVersion; + +/** + * Force the usage of Tls versions on every created socket + * Inspired from https://blog.dev-area.net/2015/08/13/android-4-1-enable-tls-1-1-and-tls-1-2/ + */ +/*package*/ class TLSSocketFactory extends SSLSocketFactory { + private static final String LOG_TAG = TLSSocketFactory.class.getSimpleName(); + + private SSLSocketFactory internalSSLSocketFactory; + + private String[] enabledProtocols; + + /** + * Constructor + * + * @param trustPinned + * @param acceptedTlsVersions + * @throws KeyManagementException + * @throws NoSuchAlgorithmException + */ + /*package*/ TLSSocketFactory(TrustManager[] trustPinned, List acceptedTlsVersions) throws KeyManagementException, NoSuchAlgorithmException { + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, trustPinned, new SecureRandom()); + internalSSLSocketFactory = context.getSocketFactory(); + + enabledProtocols = new String[acceptedTlsVersions.size()]; + int i = 0; + for (TlsVersion tlsVersion : acceptedTlsVersions) { + enabledProtocols[i] = tlsVersion.javaName(); + i++; + } + } + + @Override + public String[] getDefaultCipherSuites() { + return internalSSLSocketFactory.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return internalSSLSocketFactory.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket() throws IOException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket()); + } + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose)); + } + + @Override + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort)); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort)); + } + + private Socket enableTLSOnSocket(Socket socket) { + if (socket != null && (socket instanceof SSLSocket)) { + SSLSocket sslSocket = (SSLSocket) socket; + + List supportedProtocols = Arrays.asList(sslSocket.getSupportedProtocols()); + List filteredEnabledProtocols = new ArrayList<>(); + + for (String protocol : enabledProtocols) { + if (supportedProtocols.contains(protocol)) { + filteredEnabledProtocols.add(protocol); + } + } + + if (!filteredEnabledProtocols.isEmpty()) { + try { + sslSocket.setEnabledProtocols(filteredEnabledProtocols.toArray(new String[filteredEnabledProtocols.size()])); + } catch (Exception e) { + Log.e(LOG_TAG, "Exception: ", e); + } + } + } + return socket; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/ssl/UnrecognizedCertificateException.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/ssl/UnrecognizedCertificateException.java new file mode 100644 index 0000000000..e85b86e8a0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/ssl/UnrecognizedCertificateException.java @@ -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.ssl; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * Thrown when we are given a certificate that does match the certificate we were told to + * expect. + */ +public class UnrecognizedCertificateException extends CertificateException { + private final X509Certificate mCert; + private final Fingerprint mFingerprint; + + public UnrecognizedCertificateException(X509Certificate cert, Fingerprint fingerprint, Throwable cause) { + super("Unrecognized certificate with unknown fingerprint: " + cert.getSubjectDN(), cause); + mCert = cert; + mFingerprint = fingerprint; + } + + public X509Certificate getCertificate() { + return mCert; + } + + public Fingerprint getFingerprint() { + return mFingerprint; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/sync/DefaultEventsThreadListener.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/sync/DefaultEventsThreadListener.java new file mode 100644 index 0000000000..886fb1d360 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/sync/DefaultEventsThreadListener.java @@ -0,0 +1,50 @@ +/* + * 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.sync; + +import im.vector.matrix.android.internal.legacy.MXDataHandler; +import im.vector.matrix.android.internal.legacy.rest.model.MatrixError; +import im.vector.matrix.android.internal.legacy.rest.model.sync.SyncResponse; + +/** + * Listener for the events thread that sends data back to a data handler. + */ +public class DefaultEventsThreadListener implements EventsThreadListener { + + private final MXDataHandler mDataHandler; + + public DefaultEventsThreadListener(MXDataHandler data) { + mDataHandler = data; + } + + @Override + public void onSyncResponse(SyncResponse syncResponse, String fromToken, boolean isCatchingUp) { + mDataHandler.onSyncResponse(syncResponse, fromToken, isCatchingUp); + } + + @Override + public void onSyncError(MatrixError matrixError) { + mDataHandler.onSyncError(matrixError); + } + + @Override + public void onConfigurationError(String matrixErrorCode) { + mDataHandler.onConfigurationError(matrixErrorCode); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/sync/EventsThread.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/sync/EventsThread.java new file mode 100644 index 0000000000..9c580b14a0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/sync/EventsThread.java @@ -0,0 +1,707 @@ +/* + * 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.sync; + +import android.annotation.SuppressLint; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.PowerManager; +import android.os.SystemClock; + +import im.vector.matrix.android.internal.legacy.data.metrics.MetricsListener; +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.callback.ApiFailureCallback; +import im.vector.matrix.android.internal.legacy.rest.callback.SimpleApiCallback; +import im.vector.matrix.android.internal.legacy.rest.client.EventsRestClient; +import im.vector.matrix.android.internal.legacy.rest.model.MatrixError; +import im.vector.matrix.android.internal.legacy.rest.model.sync.RoomsSyncResponse; +import im.vector.matrix.android.internal.legacy.rest.model.sync.SyncResponse; +import im.vector.matrix.android.internal.legacy.util.Log; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; + + +/** + * Thread that continually watches the event stream and sends events to its listener. + */ +public class EventsThread extends Thread { + private static final String LOG_TAG = EventsThread.class.getSimpleName(); + + private static final int RETRY_WAIT_TIME_MS = 10000; + + private static final int DEFAULT_SERVER_TIMEOUT_MS = 30000; + private static final int DEFAULT_CLIENT_TIMEOUT_MS = 120000; + + private EventsRestClient mEventsRestClient; + private EventsThreadListener mListener; + private String mCurrentToken; + + private MetricsListener mMetricsListener; + + private boolean mPaused = true; + private boolean mIsNetworkSuspended = false; + private boolean mIsCatchingUp = false; + private boolean mIsOnline = false; + + private boolean mKilling = false; + + private int mDefaultServerTimeoutms = DEFAULT_SERVER_TIMEOUT_MS; + private int mNextServerTimeoutms = DEFAULT_SERVER_TIMEOUT_MS; + + // add a delay between two sync requests + private final Context mContext; + private int mRequestDelayMs = 0; + private final AlarmManager mAlarmManager; + private PowerManager mPowerManager; + private PendingIntent mPendingDelayedIntent; + private static final Map mSyncObjectByInstance = new HashMap<>(); + + // avoid sync on "this" because it might differ if there is a timer. + private final Object mSyncObject = new Object(); + + // Custom Retrofit error callback that will convert Retrofit errors into our own error callback + private ApiFailureCallback mFailureCallback; + + // avoid restarting the listener if there is no network. + // wait that there is an available network. + private NetworkConnectivityReceiver mNetworkConnectivityReceiver; + private boolean mbIsConnected = true; + + // use dedicated filter when enable + private String mFilterOrFilterId; + + private final IMXNetworkEventListener mNetworkListener = new IMXNetworkEventListener() { + @Override + public void onNetworkConnectionUpdate(boolean isConnected) { + Log.d(LOG_TAG, "onNetworkConnectionUpdate : before " + mbIsConnected + " now " + isConnected); + + synchronized (mSyncObject) { + mbIsConnected = isConnected; + } + + // the thread has been suspended and there is an available network + if (isConnected && !mKilling) { + Log.d(LOG_TAG, "onNetworkConnectionUpdate : call onNetworkAvailable"); + onNetworkAvailable(); + } + } + }; + + /** + * Default constructor. + * + * @param context the context + * @param apiClient API client to make the events API calls + * @param listener a listener to inform + * @param initialToken the sync initial token. + */ + public EventsThread(Context context, EventsRestClient apiClient, EventsThreadListener listener, String initialToken) { + super("Events thread"); + mContext = context; + mEventsRestClient = apiClient; + mListener = listener; + mCurrentToken = initialToken; + mSyncObjectByInstance.put(toString(), this); + mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + } + + /** + * Update the metrics listener mode + * + * @param metricsListener the metrics listener + */ + + public void setMetricsListener(MetricsListener metricsListener) { + this.mMetricsListener = metricsListener; + } + + /** + * @return the current sync token + */ + public String getCurrentSyncToken() { + return mCurrentToken; + } + + /** + * Set filterOrFilterId used for /sync requests + * + * @param filterOrFilterId + */ + public void setFilterOrFilterId(String filterOrFilterId) { + mFilterOrFilterId = filterOrFilterId; + } + + /** + * Update the long poll timeout. + * + * @param ms the timeout in ms + */ + public void setServerLongPollTimeout(int ms) { + mDefaultServerTimeoutms = Math.max(ms, DEFAULT_SERVER_TIMEOUT_MS); + Log.d(LOG_TAG, "setServerLongPollTimeout : " + mDefaultServerTimeoutms); + + } + + /** + * @return the long poll timeout + */ + public int getServerLongPollTimeout() { + return mDefaultServerTimeoutms; + } + + /** + * Set a delay between two sync requests. + * + * @param ms the delay in ms + */ + public void setSyncDelay(int ms) { + mRequestDelayMs = Math.max(0, ms); + + Log.d(LOG_TAG, "## setSyncDelay() : " + mRequestDelayMs + " with state " + getState()); + + if (State.WAITING == getState() && (!mPaused || (0 == mRequestDelayMs) && mIsCatchingUp)) { + if (!mPaused) { + Log.d(LOG_TAG, "## setSyncDelay() : resume the application"); + } + + if ((0 == mRequestDelayMs) && mIsCatchingUp) { + Log.d(LOG_TAG, "## setSyncDelay() : cancel catchup"); + mIsCatchingUp = false; + } + + // and sync asap + synchronized (mSyncObject) { + mSyncObject.notify(); + } + } + } + + /** + * @return the delay between two sync requests. + */ + public int getSyncDelay() { + return mRequestDelayMs; + } + + /** + * Set the network connectivity listener. + * It is used to avoid restarting the events threads each 10 seconds when there is no available network. + * + * @param networkConnectivityReceiver the network receiver + */ + public void setNetworkConnectivityReceiver(NetworkConnectivityReceiver networkConnectivityReceiver) { + mNetworkConnectivityReceiver = networkConnectivityReceiver; + } + + /** + * Set the failure callback. + * + * @param failureCallback the failure callback. + */ + public void setFailureCallback(ApiFailureCallback failureCallback) { + mFailureCallback = failureCallback; + } + + /** + * Pause the thread. It will resume where it left off when unpause()d. + */ + public void pause() { + Log.d(LOG_TAG, "pause()"); + mPaused = true; + mIsCatchingUp = false; + } + + /** + * A network connection has been retrieved. + */ + private void onNetworkAvailable() { + Log.d(LOG_TAG, "onNetWorkAvailable()"); + if (mIsNetworkSuspended) { + mIsNetworkSuspended = false; + + if (mPaused) { + Log.d(LOG_TAG, "the event thread is still suspended"); + } else { + Log.d(LOG_TAG, "Resume the thread"); + // cancel any catchup process. + mIsCatchingUp = false; + + synchronized (mSyncObject) { + mSyncObject.notify(); + } + } + } else { + Log.d(LOG_TAG, "onNetWorkAvailable() : nothing to do"); + } + } + + /** + * Unpause the thread if it had previously been paused. If not, this does nothing. + */ + public void unpause() { + Log.d(LOG_TAG, "## unpause() : thread state " + getState()); + + if (State.WAITING == getState()) { + Log.d(LOG_TAG, "## unpause() : the thread was paused so resume it."); + + mPaused = false; + synchronized (mSyncObject) { + mSyncObject.notify(); + } + } + + // cancel any catchup process. + mIsCatchingUp = false; + } + + /** + * Catchup until some events are retrieved. + */ + public void catchup() { + Log.d(LOG_TAG, "## catchup() : thread state " + getState()); + + if (State.WAITING == getState()) { + Log.d(LOG_TAG, "## catchup() : the thread was paused so wake it up"); + + mPaused = false; + synchronized (mSyncObject) { + mSyncObject.notify(); + } + } + + mIsCatchingUp = true; + } + + /** + * Allow the thread to finish its current processing, then permanently stop. + */ + public void kill() { + Log.d(LOG_TAG, "killing ..."); + + mKilling = true; + + if (mPaused) { + Log.d(LOG_TAG, "killing : the thread was pause so wake it up"); + + mPaused = false; + synchronized (mSyncObject) { + mSyncObject.notify(); + } + + Log.d(LOG_TAG, "Resume the thread to kill it."); + } + } + + /** + * Cancel the killing process + */ + public void cancelKill() { + if (mKilling) { + Log.d(LOG_TAG, "## cancelKill() : Cancel the pending kill"); + mKilling = false; + } else { + Log.d(LOG_TAG, "## cancelKill() : Nothing to d"); + } + } + + /** + * Update the online status + * + * @param isOnline true if the client must be seen as online + */ + public void setIsOnline(boolean isOnline) { + Log.d(LOG_TAG, "setIsOnline to " + isOnline); + mIsOnline = isOnline; + } + + /** + * Tells if the presence is online. + * + * @return true if the user is seen as online. + */ + public boolean isOnline() { + return mIsOnline; + } + + @Override + public void run() { + try { + Looper.prepare(); + } catch (Exception e) { + Log.e(LOG_TAG, "## run() : prepare failed " + e.getMessage(), e); + } + startSync(); + } + + /** + * Tells if a sync request contains some changed devices. + * + * @param syncResponse the sync response + * @return true if the response contains some changed devices. + */ + private static boolean hasDevicesChanged(SyncResponse syncResponse) { + return (null != syncResponse.deviceLists) + && (null != syncResponse.deviceLists.changed) + && (syncResponse.deviceLists.changed.size() > 0); + } + + + /** + * Use a broadcast receiver because the Timer delay might be inaccurate when the screen is turned off. + * For example, request a 1 min delay and get a 6 mins one. + */ + public static class SyncDelayReceiver extends BroadcastReceiver { + public static final String EXTRA_INSTANCE_ID = "EXTRA_INSTANCE_ID"; + + public void onReceive(Context context, Intent intent) { + String instanceId = intent.getStringExtra(EXTRA_INSTANCE_ID); + + if ((null != instanceId) && mSyncObjectByInstance.containsKey(instanceId)) { + EventsThread eventsThread = mSyncObjectByInstance.get(instanceId); + + eventsThread.mPendingDelayedIntent = null; + + Log.d(LOG_TAG, "start a sync after " + eventsThread.mRequestDelayMs + " ms"); + + synchronized (eventsThread.mSyncObject) { + eventsThread.mSyncObject.notify(); + } + } + } + } + + private void resumeInitialSync() { + Log.d(LOG_TAG, "Resuming initial sync from " + mCurrentToken); + // dummy initial sync + // to hide the splash screen + SyncResponse dummySyncResponse = new SyncResponse(); + dummySyncResponse.nextBatch = mCurrentToken; + mListener.onSyncResponse(dummySyncResponse, null, true); + } + + private void executeInitialSync() { + Log.d(LOG_TAG, "Requesting initial sync..."); + long initialSyncStartTime = System.currentTimeMillis(); + while (!isInitialSyncDone()) { + final CountDownLatch latch = new CountDownLatch(1); + mEventsRestClient.syncFromToken(null, 0, DEFAULT_CLIENT_TIMEOUT_MS, mIsOnline ? null : "offline", mFilterOrFilterId, + new SimpleApiCallback(mFailureCallback) { + @Override + public void onSuccess(SyncResponse syncResponse) { + Log.d(LOG_TAG, "Received initial sync response."); + mNextServerTimeoutms = hasDevicesChanged(syncResponse) ? 0 : mDefaultServerTimeoutms; + mListener.onSyncResponse(syncResponse, null, (0 == mNextServerTimeoutms)); + mCurrentToken = syncResponse.nextBatch; + // unblock the events thread + latch.countDown(); + } + + private void sleepAndUnblock() { + Log.i(LOG_TAG, "Waiting a bit before retrying"); + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + public void run() { + latch.countDown(); + } + }, RETRY_WAIT_TIME_MS); + } + + @Override + public void onNetworkError(Exception e) { + if (isInitialSyncDone()) { + // Ignore error + // FIXME I think this is the source of infinite initial sync if a network error occurs + // FIXME because latch is not counted down. TO BE TESTED + onSuccess(null); + } else { + Log.e(LOG_TAG, "Sync V2 onNetworkError " + e.getMessage(), e); + super.onNetworkError(e); + sleepAndUnblock(); + } + } + + @Override + public void onMatrixError(MatrixError e) { + super.onMatrixError(e); + + if (MatrixError.isConfigurationErrorCode(e.errcode)) { + mListener.onConfigurationError(e.errcode); + } else { + mListener.onSyncError(e); + sleepAndUnblock(); + } + } + + @Override + public void onUnexpectedError(Exception e) { + super.onUnexpectedError(e); + Log.e(LOG_TAG, "Sync V2 onUnexpectedError " + e.getMessage(), e); + sleepAndUnblock(); + } + }); + + // block until the initial sync callback is invoked. + try { + latch.await(); + } catch (InterruptedException e) { + Log.e(LOG_TAG, "Interrupted whilst performing initial sync.", e); + } catch (Exception e) { + // reported by GA + // The thread might have been killed. + Log.e(LOG_TAG, "latch.await() failed " + e.getMessage(), e); + } + } + long initialSyncEndTime = System.currentTimeMillis(); + long initialSyncDuration = initialSyncEndTime - initialSyncStartTime; + if (mMetricsListener != null) { + mMetricsListener.onInitialSyncFinished(initialSyncDuration); + } + } + + + /** + * Start the events sync + */ + @SuppressLint("NewApi") + private void startSync() { + int serverTimeout; + mPaused = false; + if (isInitialSyncDone()) { + resumeInitialSync(); + serverTimeout = 0; + } else { + // Start with initial sync + executeInitialSync(); + serverTimeout = mNextServerTimeoutms; + } + + Log.d(LOG_TAG, "Starting event stream from token " + mCurrentToken); + // sanity check + if (null != mNetworkConnectivityReceiver) { + mNetworkConnectivityReceiver.addEventListener(mNetworkListener); + // + mbIsConnected = mNetworkConnectivityReceiver.isConnected(); + mIsNetworkSuspended = !mbIsConnected; + } + + // Then repeatedly long-poll for events + + while (!mKilling) { + + // test if a delay between two syncs + if ((!mPaused && !mIsNetworkSuspended) && (0 != mRequestDelayMs)) { + Log.d(LOG_TAG, "startSync : start a delay timer "); + + Intent intent = new Intent(mContext, SyncDelayReceiver.class); + intent.putExtra(SyncDelayReceiver.EXTRA_INSTANCE_ID, toString()); + mPendingDelayedIntent = PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + + long futureInMillis = SystemClock.elapsedRealtime() + mRequestDelayMs; + + if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + && mPowerManager.isIgnoringBatteryOptimizations(mContext.getPackageName())) { + mAlarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, futureInMillis, mPendingDelayedIntent); + } else { + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, futureInMillis, mPendingDelayedIntent); + } + } + + if (mPaused || mIsNetworkSuspended || (null != mPendingDelayedIntent)) { + if (null != mPendingDelayedIntent) { + Log.d(LOG_TAG, "Event stream is paused because there is a timer delay."); + } else if (mIsNetworkSuspended) { + Log.d(LOG_TAG, "Event stream is paused because there is no available network."); + } else { + Log.d(LOG_TAG, "Event stream is paused. Waiting."); + } + + try { + Log.d(LOG_TAG, "startSync : wait ..."); + + synchronized (mSyncObject) { + mSyncObject.wait(); + } + + if (null != mPendingDelayedIntent) { + Log.d(LOG_TAG, "startSync : cancel mSyncDelayTimer"); + mAlarmManager.cancel(mPendingDelayedIntent); + mPendingDelayedIntent.cancel(); + mPendingDelayedIntent = null; + } + + Log.d(LOG_TAG, "Event stream woken from pause."); + + // perform a catchup asap + serverTimeout = 0; + } catch (InterruptedException e) { + Log.e(LOG_TAG, "Unexpected interruption while paused: " + e.getMessage(), e); + } + } + + // the service could have been killed while being paused. + if (!mKilling) { + + long incrementalSyncStartTime = System.currentTimeMillis(); + + final CountDownLatch latch = new CountDownLatch(1); + + Log.d(LOG_TAG, "Get events from token " + mCurrentToken + " with filterOrFilterId " + mFilterOrFilterId); + + final int fServerTimeout = serverTimeout; + mNextServerTimeoutms = mDefaultServerTimeoutms; + + mEventsRestClient.syncFromToken(mCurrentToken, serverTimeout, DEFAULT_CLIENT_TIMEOUT_MS, mIsOnline ? null : "offline", mFilterOrFilterId, + new SimpleApiCallback(mFailureCallback) { + @Override + public void onSuccess(SyncResponse syncResponse) { + if (!mKilling) { + // poll /sync with timeout=0 until + // we get no to_device messages back. + if (0 == fServerTimeout) { + if (hasDevicesChanged(syncResponse)) { + if (mIsCatchingUp) { + Log.d(LOG_TAG, "Some devices have changed but do not set mNextServerTimeoutms to 0 to avoid infinite loops"); + } else { + Log.d(LOG_TAG, "mNextServerTimeoutms is set to 0 because of hasDevicesChanged " + + syncResponse.deviceLists.changed); + mNextServerTimeoutms = 0; + } + } + } + + // the catchup request is suspended when there is no need + // to loop again + if (mIsCatchingUp && (0 != mNextServerTimeoutms)) { + // the catchup triggers sync requests until there are some useful events + int eventCounts = 0; + + if (null != syncResponse.rooms) { + RoomsSyncResponse roomsSyncResponse = syncResponse.rooms; + + if (null != roomsSyncResponse.join) { + eventCounts += roomsSyncResponse.join.size(); + } + + if (null != roomsSyncResponse.invite) { + eventCounts += roomsSyncResponse.invite.size(); + } + } + + // stop any catch up + mIsCatchingUp = false; + mPaused = (0 == mRequestDelayMs); + Log.d(LOG_TAG, "Got " + eventCounts + " useful events while catching up : mPaused is set to " + mPaused); + } + Log.d(LOG_TAG, "Got event response"); + mListener.onSyncResponse(syncResponse, mCurrentToken, (0 == mNextServerTimeoutms)); + mCurrentToken = syncResponse.nextBatch; + Log.d(LOG_TAG, "mCurrentToken is now set to " + mCurrentToken); + + } + + // unblock the events thread + latch.countDown(); + } + + private void onError(String description) { + boolean isConnected; + Log.d(LOG_TAG, "Got an error while polling events " + description); + + synchronized (mSyncObject) { + isConnected = mbIsConnected; + } + + // detected if the device is connected before trying again + if (isConnected) { + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + public void run() { + latch.countDown(); + } + }, RETRY_WAIT_TIME_MS); + + } else { + // no network -> wait that a network connection comes back. + mIsNetworkSuspended = true; + latch.countDown(); + } + } + + @Override + public void onNetworkError(Exception e) { + onError(e.getLocalizedMessage()); + } + + @Override + public void onMatrixError(MatrixError e) { + if (MatrixError.isConfigurationErrorCode(e.errcode)) { + mListener.onConfigurationError(e.errcode); + } else { + mListener.onSyncError(e); + onError(e.getLocalizedMessage()); + } + } + + @Override + public void onUnexpectedError(Exception e) { + onError(e.getLocalizedMessage()); + } + }); + + // block until the sync callback is invoked. + try { + latch.await(); + } catch (InterruptedException e) { + Log.e(LOG_TAG, "Interrupted whilst polling message", e); + } catch (Exception e) { + // reported by GA + // The thread might have been killed. + Log.e(LOG_TAG, "latch.await() failed " + e.getMessage(), e); + } + long incrementalSyncEndTime = System.currentTimeMillis(); + long incrementalSyncDuration = incrementalSyncEndTime - incrementalSyncStartTime; + if (mMetricsListener != null) { + mMetricsListener.onIncrementalSyncFinished(incrementalSyncDuration); + } + } + serverTimeout = mNextServerTimeoutms; + } + + if (null != mNetworkConnectivityReceiver) { + mNetworkConnectivityReceiver.removeEventListener(mNetworkListener); + } + Log.d(LOG_TAG, "Event stream terminating."); + } + + /** + * Ask if the initial sync is done. It means we have a sync token + * + * @return + */ + private boolean isInitialSyncDone() { + return mCurrentToken != null; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/sync/EventsThreadListener.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/sync/EventsThreadListener.java new file mode 100644 index 0000000000..d858a7fb7f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/sync/EventsThreadListener.java @@ -0,0 +1,49 @@ +/* + * 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.sync; + +import im.vector.matrix.android.internal.legacy.rest.model.MatrixError; +import im.vector.matrix.android.internal.legacy.rest.model.sync.SyncResponse; + +/** + * Interface to implement to listen to the event thread. + */ +public interface EventsThreadListener { + /** + * Call when a sync request has been performed with the API V2. + * + * @param response the response (can be null) + * @param fromToken the start token + * @param isCatchingUp true if a catchup is on progress + */ + void onSyncResponse(SyncResponse response, String fromToken, boolean isCatchingUp); + + /** + * The sync has encountered an error + * + * @param matrixError the matrix error + */ + void onSyncError(final MatrixError matrixError); + + /** + * A configuration error has been received. + * + * @param matrixErrorCode the matrix error code + */ + void onConfigurationError(String matrixErrorCode); +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/BingRulesManager.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/BingRulesManager.java new file mode 100644 index 0000000000..8c9363cced --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/BingRulesManager.java @@ -0,0 +1,1106 @@ +/* + * 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.util; + +import android.text.TextUtils; + +import im.vector.matrix.android.internal.legacy.MXDataHandler; +import im.vector.matrix.android.internal.legacy.MXSession; +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.listeners.IMXNetworkEventListener; +import im.vector.matrix.android.internal.legacy.network.NetworkConnectivityReceiver; +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.PushRulesRestClient; +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.bingrules.BingRule; +import im.vector.matrix.android.internal.legacy.rest.model.bingrules.Condition; +import im.vector.matrix.android.internal.legacy.rest.model.bingrules.ContainsDisplayNameCondition; +import im.vector.matrix.android.internal.legacy.rest.model.bingrules.ContentRule; +import im.vector.matrix.android.internal.legacy.rest.model.bingrules.EventMatchCondition; +import im.vector.matrix.android.internal.legacy.rest.model.bingrules.PushRuleSet; +import im.vector.matrix.android.internal.legacy.rest.model.bingrules.PushRulesResponse; +import im.vector.matrix.android.internal.legacy.rest.model.bingrules.RoomMemberCountCondition; +import im.vector.matrix.android.internal.legacy.rest.model.bingrules.SenderNotificationPermissionCondition; +import im.vector.matrix.android.internal.legacy.rest.model.message.Message; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * Object that gets and processes bing rules from the server. + */ +public class BingRulesManager { + private static final String LOG_TAG = BingRulesManager.class.getSimpleName(); + + /** + * Bing rule listener + */ + public interface onBingRuleUpdateListener { + /** + * The manager succeeds to update the bingrule enable status. + */ + void onBingRuleUpdateSuccess(); + + /** + * The manager fails to update the bingrule enable status. + * + * @param errorMessage the error message. + */ + void onBingRuleUpdateFailure(String errorMessage); + } + + /** + * Bing rules update + */ + public interface onBingRulesUpdateListener { + /** + * Warn that some bing rules have been updated + */ + void onBingRulesUpdate(); + } + + // general members + private final PushRulesRestClient mApiClient; + private final MXSession mSession; + private final String mMyUserId; + private final MXDataHandler mDataHandler; + + // the rules set to apply + private PushRuleSet mRulesSet = new PushRuleSet(); + + // the rules list + private final List mRules = new ArrayList<>(); + + // the default bing rule + private BingRule mDefaultBingRule = new BingRule(true); + + // tell if the bing rules set is initialized + private boolean mIsInitialized = false; + + // map to check if a room is "mention only" + private final Map mIsMentionOnlyMap = new HashMap<>(); + + // network management + private NetworkConnectivityReceiver mNetworkConnectivityReceiver; + private IMXNetworkEventListener mNetworkListener; + private ApiCallback mLoadRulesCallback; + + // listener + private final Set mBingRulesUpdateListeners = new HashSet<>(); + + /** + * Defines the room notification state + */ + public enum RoomNotificationState { + /** + * All the messages will trigger a noisy notification + */ + ALL_MESSAGES_NOISY, + + /** + * All the messages will trigger a notification + */ + ALL_MESSAGES, + + /** + * Only the messages with user display name / user name will trigger notifications + */ + MENTIONS_ONLY, + + /** + * No notifications + */ + MUTE + } + + private Map mRoomNotificationStateByRoomId = new HashMap<>(); + + /** + * Constructor + * + * @param session the session + * @param networkConnectivityReceiver the network events listener + */ + public BingRulesManager(MXSession session, NetworkConnectivityReceiver networkConnectivityReceiver) { + mSession = session; + mApiClient = session.getBingRulesApiClient(); + mMyUserId = session.getCredentials().userId; + mDataHandler = session.getDataHandler(); + + mNetworkListener = new IMXNetworkEventListener() { + @Override + public void onNetworkConnectionUpdate(boolean isConnected) { + // mLoadRulesCallback is set when a loadRules failed + // so when a network is available, trigger again loadRules + if (isConnected && (null != mLoadRulesCallback)) { + loadRules(mLoadRulesCallback); + } + } + }; + + mNetworkConnectivityReceiver = networkConnectivityReceiver; + networkConnectivityReceiver.addEventListener(mNetworkListener); + } + + /** + * @return true if it is ready to be used (i.e initializedà + */ + public boolean isReady() { + return mIsInitialized; + } + + /** + * Remove the network events listener. + * This listener is only used to initialize the rules at application launch. + */ + private void removeNetworkListener() { + if ((null != mNetworkConnectivityReceiver) && (null != mNetworkListener)) { + mNetworkConnectivityReceiver.removeEventListener(mNetworkListener); + mNetworkConnectivityReceiver = null; + mNetworkListener = null; + } + } + + /** + * Add a listener + * + * @param listener the listener + */ + public void addBingRulesUpdateListener(onBingRulesUpdateListener listener) { + if (null != listener) { + mBingRulesUpdateListeners.add(listener); + } + } + + /** + * remove a listener + * + * @param listener the listener + */ + public void removeBingRulesUpdateListener(onBingRulesUpdateListener listener) { + if (null != listener) { + mBingRulesUpdateListeners.remove(listener); + } + } + + /** + * Some rules have been updated. + */ + private void onBingRulesUpdate() { + // delete cached data + mRoomNotificationStateByRoomId.clear(); + + for (onBingRulesUpdateListener listener : mBingRulesUpdateListeners) { + try { + listener.onBingRulesUpdate(); + } catch (Exception e) { + Log.e(LOG_TAG, "## onBingRulesUpdate() : onBingRulesUpdate failed " + e.getMessage(), e); + } + } + } + + /** + * Load the bing rules from the server. + * + * @param callback an async callback called when the rules are loaded + */ + public void loadRules(final ApiCallback callback) { + mLoadRulesCallback = null; + + Log.d(LOG_TAG, "## loadRules() : refresh the bing rules"); + mApiClient.getAllRules(new ApiCallback() { + @Override + public void onSuccess(PushRulesResponse info) { + Log.d(LOG_TAG, "## loadRules() : succeeds"); + + buildRules(info); + mIsInitialized = true; + + if (callback != null) { + callback.onSuccess(null); + } + + removeNetworkListener(); + } + + private void onError(String errorMessage) { + Log.e(LOG_TAG, "## loadRules() : failed " + errorMessage); + // the callback will be called when the request will succeed + mLoadRulesCallback = callback; + } + + @Override + public void onNetworkError(Exception e) { + onError(e.getMessage()); + + if (null != callback) { + callback.onNetworkError(e); + } + } + + @Override + public void onMatrixError(MatrixError e) { + onError(e.getMessage()); + + if (null != callback) { + callback.onMatrixError(e); + } + } + + @Override + public void onUnexpectedError(Exception e) { + onError(e.getMessage()); + + if (null != callback) { + callback.onUnexpectedError(e); + } + } + }); + } + + /** + * Returns whether a string contains an occurrence of another, as a standalone word, regardless of case. + * + * @param subString the string to search for + * @param longString the string to search in + * @return whether a match was found + */ + private static boolean caseInsensitiveFind(String subString, String longString) { + // sanity check + if (TextUtils.isEmpty(subString) || TextUtils.isEmpty(longString)) { + return false; + } + + boolean found = false; + + try { + Pattern pattern = Pattern.compile("(\\W|^)" + subString + "(\\W|$)", Pattern.CASE_INSENSITIVE); + found = pattern.matcher(longString).find(); + } catch (Exception e) { + Log.e(LOG_TAG, "caseInsensitiveFind : pattern.matcher failed with " + e.getMessage(), e); + } + + return found; + } + + /** + * Returns the first highlighted notifiable bing rule which fulfills its condition with this event. + * + * @param event the event + * @return the first matched bing rule, null if none + */ + public BingRule fulfilledHighlightBingRule(Event event) { + return fulfilledBingRule(event, true); + } + + /** + * Returns the first notifiable bing rule which fulfills its condition with this event. + * + * @param event the event + * @return the first matched bing rule, null if none + */ + public BingRule fulfilledBingRule(Event event) { + return fulfilledBingRule(event, false); + } + + /** + * Returns the first notifiable bing rule which fulfills its condition with this event. + * + * @param event the event + * @param highlightRuleOnly true to only check the highlight rule + * @return the first matched bing rule, null if none + */ + private BingRule fulfilledBingRule(Event event, boolean highlightRuleOnly) { + // sanity check + if (null == event) { + Log.e(LOG_TAG, "## fulfilledBingRule() : null event"); + return null; + } + + if (!mIsInitialized) { + Log.e(LOG_TAG, "## fulfilledBingRule() : not initialized"); + return null; + } + + if (0 == mRules.size()) { + Log.e(LOG_TAG, "## fulfilledBingRule() : no rules"); + return null; + } + + // do not trigger notification for oneself messages + if ((null != event.getSender()) && TextUtils.equals(event.getSender(), mMyUserId)) { + return null; + } + + String eventType = event.getType(); + + // some types are not bingable + if (TextUtils.equals(eventType, Event.EVENT_TYPE_PRESENCE) + || TextUtils.equals(eventType, Event.EVENT_TYPE_TYPING) + || TextUtils.equals(eventType, Event.EVENT_TYPE_REDACTION) + || TextUtils.equals(eventType, Event.EVENT_TYPE_RECEIPT) + || TextUtils.equals(eventType, Event.EVENT_TYPE_TAGS)) { + return null; + } + + // GA issue + final List rules; + + synchronized (this) { + rules = new ArrayList<>(mRules); + } + + // Go down the rule list until we find a match + for (BingRule bingRule : rules) { + if (bingRule.isEnabled && (!highlightRuleOnly || bingRule.shouldHighlight())) { + boolean isFullfilled = false; + + // some rules have no condition + // so their ruleId defines the method + if (BingRule.RULE_ID_CONTAIN_USER_NAME.equals(bingRule.ruleId) || BingRule.RULE_ID_CONTAIN_DISPLAY_NAME.equals(bingRule.ruleId)) { + if (Event.EVENT_TYPE_MESSAGE.equals(event.getType())) { + Message message = JsonUtils.toMessage(event.getContent()); + MyUser myUser = mSession.getMyUser(); + String pattern = null; + + if (BingRule.RULE_ID_CONTAIN_USER_NAME.equals(bingRule.ruleId)) { + if (mMyUserId.indexOf(":") >= 0) { + pattern = mMyUserId.substring(1, mMyUserId.indexOf(":")); + } else { + pattern = mMyUserId; + } + } else if (BingRule.RULE_ID_CONTAIN_DISPLAY_NAME.equals(bingRule.ruleId)) { + pattern = myUser.displayname; + if ((null != mSession.getDataHandler()) && (null != mSession.getDataHandler().getStore())) { + Room room = mSession.getDataHandler().getStore().getRoom(event.roomId); + + if ((null != room) && (null != room.getState())) { + String disambiguousedName = room.getState().getMemberName(mMyUserId); + + if (!TextUtils.equals(disambiguousedName, mMyUserId)) { + pattern = Pattern.quote(disambiguousedName); + } + } + } + } + + if (!TextUtils.isEmpty(pattern)) { + isFullfilled = caseInsensitiveFind(pattern, message.body); + } + } + } else if (BingRule.RULE_ID_FALLBACK.equals(bingRule.ruleId)) { + isFullfilled = true; + } else { + // some default rules define conditions + // so use them instead of doing a custom treatment + // RULE_ID_ONE_TO_ONE_ROOM + // RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS + isFullfilled = eventMatchesConditions(event, bingRule.conditions); + } + + if (isFullfilled) { + return bingRule; + } + } + } + + // no rules are fulfilled + return null; + } + + /** + * Check if an event matches a conditions set + * + * @param event the event to test + * @param conditions the conditions set + * @return true if the event matches all the conditions set. + */ + private boolean eventMatchesConditions(Event event, List conditions) { + try { + if ((conditions != null) && (event != null)) { + for (Condition condition : conditions) { + if (condition instanceof EventMatchCondition) { + if (!((EventMatchCondition) condition).isSatisfied(event)) { + return false; + } + } else if (condition instanceof ContainsDisplayNameCondition) { + String myDisplayName = null; + + if (event.roomId != null) { + Room room = mDataHandler.getRoom(event.roomId, false); + + // sanity checks + if (room != null && room.getMember(mMyUserId) != null) { + // Best way to get your display name for now + myDisplayName = room.getMember(mMyUserId).displayname; + } + } + + if (TextUtils.isEmpty(myDisplayName)) { + // RoomMember is maybe not known due to lazy loading + // Get displayName from the session + myDisplayName = mSession.getMyUser().displayname; + } + + if (!((ContainsDisplayNameCondition) condition).isSatisfied(event, myDisplayName)) { + return false; + } + } else if (condition instanceof RoomMemberCountCondition) { + if (event.roomId != null) { + Room room = mDataHandler.getRoom(event.roomId, false); + + if (!((RoomMemberCountCondition) condition).isSatisfied(room)) { + return false; + } + } + } else if (condition instanceof SenderNotificationPermissionCondition) { + if (event.roomId != null) { + Room room = mDataHandler.getRoom(event.roomId, false); + + if (!((SenderNotificationPermissionCondition) condition).isSatisfied(room.getState().getPowerLevels(), event.sender)) { + return false; + } + } + } else { + // unknown conditions: we previously matched all unknown conditions, + // but given that rules can be added to the base rules on a server, + // it's probably better to not match unknown conditions. + return false; + } + } + } + } catch (Exception e) { + Log.e(LOG_TAG, "## eventMatchesConditions() failed " + e.getMessage(), e); + return false; + } + return true; + } + + /** + * Build the internal push rules + * + * @param pushRulesResponse the server request response. + */ + public void buildRules(PushRulesResponse pushRulesResponse) { + if (null != pushRulesResponse) { + updateRulesSet(pushRulesResponse.global); + onBingRulesUpdate(); + } + } + + /** + * @return the rules set + */ + public PushRuleSet pushRules() { + return mRulesSet; + } + + /** + * Update mRulesSet with the new one. + * + * @param ruleSet the new ruleSet to apply + */ + private void updateRulesSet(PushRuleSet ruleSet) { + synchronized (this) { + // clear the rules list + // it is + mRules.clear(); + + // sanity check + if (null == ruleSet) { + mRulesSet = new PushRuleSet(); + return; + } + + // Replace the list by ArrayList to be able to add/remove rules + // Add the rule kind in each rule + // Ensure that the null pointers are replaced by an empty list + if (ruleSet.override != null) { + ruleSet.override = new ArrayList<>(ruleSet.override); + for (BingRule rule : ruleSet.override) { + rule.kind = BingRule.KIND_OVERRIDE; + } + mRules.addAll(ruleSet.override); + } else { + ruleSet.override = new ArrayList<>(ruleSet.override); + } + + if (ruleSet.content != null) { + ruleSet.content = new ArrayList<>(ruleSet.content); + for (BingRule rule : ruleSet.content) { + rule.kind = BingRule.KIND_CONTENT; + } + addContentRules(ruleSet.content); + } else { + ruleSet.content = new ArrayList<>(); + } + + mIsMentionOnlyMap.clear(); + if (ruleSet.room != null) { + ruleSet.room = new ArrayList<>(ruleSet.room); + + for (BingRule rule : ruleSet.room) { + rule.kind = BingRule.KIND_ROOM; + } + addRoomRules(ruleSet.room); + } else { + ruleSet.room = new ArrayList<>(); + } + + if (ruleSet.sender != null) { + ruleSet.sender = new ArrayList<>(ruleSet.sender); + + for (BingRule rule : ruleSet.sender) { + rule.kind = BingRule.KIND_SENDER; + } + addSenderRules(ruleSet.sender); + } else { + ruleSet.sender = new ArrayList<>(); + } + + if (ruleSet.underride != null) { + ruleSet.underride = new ArrayList<>(ruleSet.underride); + for (BingRule rule : ruleSet.underride) { + rule.kind = BingRule.KIND_UNDERRIDE; + } + mRules.addAll(ruleSet.underride); + } else { + ruleSet.underride = new ArrayList<>(); + } + + mRulesSet = ruleSet; + + Log.d(LOG_TAG, "## updateRules() : has " + mRules.size() + " rules"); + } + } + + /** + * Create a content EventMatchConditions list from a ContentRules list + * + * @param rules the ContentRules list + */ + private void addContentRules(List rules) { + // sanity check + if (null != rules) { + for (ContentRule rule : rules) { + EventMatchCondition condition = new EventMatchCondition(); + condition.kind = Condition.KIND_EVENT_MATCH; + condition.key = "content.body"; + condition.pattern = rule.pattern; + + rule.addCondition(condition); + + mRules.add(rule); + } + } + } + + /** + * Create a room EventMatchConditions list from a BingRule list + * + * @param rules the BingRule list + */ + private void addRoomRules(List rules) { + if (null != rules) { + for (BingRule rule : rules) { + EventMatchCondition condition = new EventMatchCondition(); + condition.kind = Condition.KIND_EVENT_MATCH; + condition.key = "room_id"; + condition.pattern = rule.ruleId; + + rule.addCondition(condition); + + mRules.add(rule); + } + } + } + + /** + * Create a sender EventMatchConditions list from a BingRule list + * + * @param rules the BingRule list + */ + private void addSenderRules(List rules) { + if (null != rules) { + for (BingRule rule : rules) { + EventMatchCondition condition = new EventMatchCondition(); + condition.kind = Condition.KIND_EVENT_MATCH; + condition.key = "user_id"; + condition.pattern = rule.ruleId; + + rule.addCondition(condition); + + mRules.add(rule); + } + } + } + + /** + * Force to refresh the rules. + * The listener is called when the rules are refreshed. + * + * @param errorMsg the error message to dispatch. + * @param listener the asynchronous listener + */ + private void forceRulesRefresh(final String errorMsg, final onBingRuleUpdateListener listener) { + // refresh only there is a listener + if (null != listener) { + // clear cached data + mRoomNotificationStateByRoomId.clear(); + + loadRules(new ApiCallback() { + private void onDone(String error) { + // clear cached data + mRoomNotificationStateByRoomId.clear(); + + try { + if (TextUtils.isEmpty(error) && TextUtils.isEmpty(errorMsg)) { + listener.onBingRuleUpdateSuccess(); + } else { + listener.onBingRuleUpdateFailure(TextUtils.isEmpty(errorMsg) ? error : errorMsg); + } + } catch (Exception e) { + Log.e(LOG_TAG, "## forceRulesRefresh() : failed " + e.getMessage(), e); + } + } + + @Override + public void onSuccess(Void info) { + onDone(null); + } + + @Override + public void onNetworkError(Exception e) { + onDone(e.getLocalizedMessage()); + } + + @Override + public void onMatrixError(MatrixError e) { + onDone(e.getLocalizedMessage()); + } + + @Override + public void onUnexpectedError(Exception e) { + onDone(e.getLocalizedMessage()); + } + }); + } + } + + /** + * Get the rules update callback. + * + * @param listener the listener + * @return the asynchronous callback + */ + private ApiCallback getUpdateCallback(final onBingRuleUpdateListener listener) { + return new ApiCallback() { + @Override + public void onSuccess(Void info) { + forceRulesRefresh(null, listener); + } + + private void onError(String message) { + forceRulesRefresh(message, listener); + } + + /** + * Called if there is a network error. + * + * @param e the exception + */ + @Override + public void onNetworkError(Exception e) { + onError(e.getLocalizedMessage()); + } + + /** + * Called in case of a Matrix error. + * + * @param e the Matrix error + */ + @Override + public void onMatrixError(MatrixError e) { + onError(e.getLocalizedMessage()); + } + + /** + * Called for some other type of error. + * + * @param e the exception + */ + @Override + public void onUnexpectedError(Exception e) { + onError(e.getLocalizedMessage()); + } + }; + } + + /** + * Update the rule enable status. + * The rules lits are refreshed when the listener is called. + * + * @param rule the bing rule to toggle. + * @param listener the rule update listener. + */ + public void updateEnableRuleStatus(final BingRule rule, final boolean isEnabled, final onBingRuleUpdateListener listener) { + if (null != rule) { + mApiClient.updateEnableRuleStatus(rule.kind, rule.ruleId, isEnabled, getUpdateCallback(listener)); + } + } + + /** + * Delete the rule. + * The rules lists are refreshed when the listener is called. + * + * @param rule the rule to delete. + * @param listener the rule update listener. + */ + public void deleteRule(final BingRule rule, final onBingRuleUpdateListener listener) { + // null case + if (null == rule) { + if (listener != null) { + try { + listener.onBingRuleUpdateSuccess(); + } catch (Exception e) { + Log.e(LOG_TAG, "## deleteRule : onBingRuleUpdateSuccess failed " + e.getMessage(), e); + } + } + return; + } + + mApiClient.deleteRule(rule.kind, rule.ruleId, getUpdateCallback(listener)); + } + + /** + * Delete a rules list. + * The rules lists are refreshed when the listener is called. + * + * @param rules the rules to delete + * @param listener the listener when the rules are deleted + */ + public void deleteRules(final List rules, final onBingRuleUpdateListener listener) { + deleteRules(rules, 0, listener); + } + + /** + * Recursive rules deletion method. + * + * @param rules the rules to delete + * @param index the rule index + * @param listener the listener when the rules are deleted + */ + private void deleteRules(final List rules, final int index, final onBingRuleUpdateListener listener) { + // sanity checks + if ((null == rules) || (index >= rules.size())) { + onBingRulesUpdate(); + if (null != listener) { + try { + listener.onBingRuleUpdateSuccess(); + } catch (Exception e) { + Log.e(LOG_TAG, "## deleteRules() : onBingRuleUpdateSuccess failed " + e.getMessage(), e); + } + } + + return; + } + + // delete the rule + deleteRule(rules.get(index), new onBingRuleUpdateListener() { + @Override + public void onBingRuleUpdateSuccess() { + deleteRules(rules, index + 1, listener); + } + + @Override + public void onBingRuleUpdateFailure(String errorMessage) { + if (null != listener) { + try { + listener.onBingRuleUpdateFailure(errorMessage); + } catch (Exception e) { + Log.e(LOG_TAG, "## deleteRules() : onBingRuleUpdateFailure failed " + e.getMessage(), e); + } + } + } + }); + } + + /** + * Add a rule. + * The rules lists are refreshed when the listener is called. + * + * @param rule the rule to delete. + * @param listener the rule update listener. + */ + public void addRule(final BingRule rule, final onBingRuleUpdateListener listener) { + // null case + if (null == rule) { + if (listener != null) { + try { + listener.onBingRuleUpdateSuccess(); + } catch (Exception e) { + Log.e(LOG_TAG, "## addRule : onBingRuleUpdateSuccess failed " + e.getMessage(), e); + } + } + return; + } + + mApiClient.addRule(rule, getUpdateCallback(listener)); + } + + /** + * Update a bing rule. + * The rules list are updated when the callback is called. + * + * @param source the source + * @param target the target + * @param listener the listener + */ + public void updateRule(final BingRule source, final BingRule target, final onBingRuleUpdateListener listener) { + if (null == source) { + addRule(target, listener); + return; + } + + if (null == target) { + deleteRule(source, listener); + return; + } + + if (source.isEnabled != target.isEnabled) { + mApiClient.updateEnableRuleStatus(target.kind, target.ruleId, target.isEnabled, new ApiCallback() { + @Override + public void onSuccess(Void info) { + source.isEnabled = target.isEnabled; + updateRule(source, target, listener); + } + + @Override + public void onNetworkError(Exception e) { + forceRulesRefresh(e.getLocalizedMessage(), listener); + } + + @Override + public void onMatrixError(MatrixError e) { + forceRulesRefresh(e.getLocalizedMessage(), listener); + } + + @Override + public void onUnexpectedError(Exception e) { + forceRulesRefresh(e.getLocalizedMessage(), listener); + } + }); + + return; + } + + if (source.actions != target.actions) { + Map map = new HashMap<>(); + List sortedActions = new ArrayList<>(); + + // the webclient needs to have them sorted + if (null != target.actions) { + if (target.actions.contains(BingRule.ACTION_NOTIFY)) { + sortedActions.add(BingRule.ACTION_NOTIFY); + } + + if (target.actions.contains(BingRule.ACTION_DONT_NOTIFY)) { + sortedActions.add(BingRule.ACTION_DONT_NOTIFY); + } + + if (null != target.getActionMap(BingRule.ACTION_SET_TWEAK_SOUND_VALUE)) { + sortedActions.add(target.getActionMap(BingRule.ACTION_SET_TWEAK_SOUND_VALUE)); + } + + if (null != target.getActionMap(BingRule.ACTION_SET_TWEAK_HIGHLIGHT_VALUE)) { + sortedActions.add(target.getActionMap(BingRule.ACTION_SET_TWEAK_HIGHLIGHT_VALUE)); + } + } + + map.put("actions", sortedActions); + + mApiClient.updateRuleActions(target.kind, target.ruleId, map, new SimpleApiCallback() { + @Override + public void onSuccess(Void info) { + source.actions = target.actions; + updateRule(source, target, listener); + } + + @Override + public void onNetworkError(Exception e) { + forceRulesRefresh(e.getLocalizedMessage(), listener); + } + + @Override + public void onMatrixError(MatrixError e) { + forceRulesRefresh(e.getLocalizedMessage(), listener); + } + + @Override + public void onUnexpectedError(Exception e) { + forceRulesRefresh(e.getLocalizedMessage(), listener); + } + }); + + return; + } + + // the update succeeds + forceRulesRefresh(null, listener); + } + + /** + * Search the push rules for the room id + * + * @param roomId the room id + * @return the room rules list + */ + private List getPushRulesForRoomId(String roomId) { + List rules = new ArrayList<>(); + + // sanity checks + if (!TextUtils.isEmpty(roomId) && (null != mRulesSet)) { + // the webclient defines two ways to set a room rule + // mention only : the user won't have any push for the room except if a content rule is fulfilled + // mute : no notification for this room + + // mute rules are defined in override groups + if (null != mRulesSet.override) { + for (BingRule roomRule : mRulesSet.override) { + if (TextUtils.equals(roomRule.ruleId, roomId)) { + rules.add(roomRule); + } + } + } + + // mention only are defined in room group + if (null != mRulesSet.room) { + for (BingRule roomRule : mRulesSet.room) { + if (TextUtils.equals(roomRule.ruleId, roomId)) { + rules.add(roomRule); + } + } + } + } + + return rules; + } + + /** + * Provide the room notification state + * + * @param roomId the room + * @return the room notification state + */ + public RoomNotificationState getRoomNotificationState(String roomId) { + if (TextUtils.isEmpty(roomId)) { + return RoomNotificationState.ALL_MESSAGES; + } + + if (mRoomNotificationStateByRoomId.containsKey(roomId)) { + return mRoomNotificationStateByRoomId.get(roomId); + } + + RoomNotificationState result = RoomNotificationState.ALL_MESSAGES; + List bingRules = getPushRulesForRoomId(roomId); + + for (BingRule rule : bingRules) { + if (rule.isEnabled) { + if (rule.shouldNotNotify()) { + result = TextUtils.equals(rule.kind, BingRule.KIND_OVERRIDE) ? RoomNotificationState.MUTE : RoomNotificationState.MENTIONS_ONLY; + break; + } else if (rule.shouldNotify()) { + result = (null != rule.getNotificationSound()) ? RoomNotificationState.ALL_MESSAGES_NOISY : RoomNotificationState.ALL_MESSAGES; + } + } + } + + mRoomNotificationStateByRoomId.put(roomId, result); + return result; + } + + /** + * Update the notification state of a dedicated room + * + * @param roomId the room id + * @param state the new state + * @param listener the asynchronous callback + */ + public void updateRoomNotificationState(final String roomId, final RoomNotificationState state, final onBingRuleUpdateListener listener) { + List bingRules = getPushRulesForRoomId(roomId); + + deleteRules(bingRules, new onBingRuleUpdateListener() { + @Override + public void onBingRuleUpdateSuccess() { + if (state == RoomNotificationState.ALL_MESSAGES) { + forceRulesRefresh(null, listener); + } else { + BingRule rule; + + if (state == RoomNotificationState.ALL_MESSAGES_NOISY) { + rule = new BingRule(BingRule.KIND_ROOM, roomId, true, false, true); + } else { + rule = new BingRule((state == RoomNotificationState.MENTIONS_ONLY) ? + BingRule.KIND_ROOM : BingRule.KIND_OVERRIDE, roomId, false, null, false); + + EventMatchCondition condition = new EventMatchCondition(); + condition.key = "room_id"; + condition.pattern = roomId; + rule.addCondition(condition); + + } + + addRule(rule, listener); + } + } + + @Override + public void onBingRuleUpdateFailure(String errorMessage) { + listener.onBingRuleUpdateFailure(errorMessage); + } + }); + } + + /** + * Tell whether the regular notifications are disabled for the room. + * + * @param roomId the room id + * @return true if the regular notifications are disabled (mention only) + */ + public boolean isRoomMentionOnly(String roomId) { + return RoomNotificationState.MENTIONS_ONLY == getRoomNotificationState(roomId); + } + + /** + * Test if the room has a dedicated rule which disables notification. + * + * @param roomId the roomId + * @return true if there is a rule to disable notifications. + */ + public boolean isRoomNotificationsDisabled(String roomId) { + RoomNotificationState state = getRoomNotificationState(roomId); + return (RoomNotificationState.MENTIONS_ONLY == state) || (RoomNotificationState.MUTE == state); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/CompatUtil.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/CompatUtil.java new file mode 100644 index 0000000000..fb52caff94 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/CompatUtil.java @@ -0,0 +1,319 @@ +/* + * 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.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Build; +import android.preference.PreferenceManager; +import android.security.KeyPairGeneratorSpec; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.util.Base64; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.RSAKeyGenParameterSpec; +import java.util.Calendar; +import java.util.zip.GZIPOutputStream; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.security.auth.x500.X500Principal; + +public class CompatUtil { + private static final String TAG = CompatUtil.class.getSimpleName(); + private static final String ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore"; + private static final String AES_GCM_CIPHER_TYPE = "AES/GCM/NoPadding"; + private static final int AES_GCM_KEY_SIZE_IN_BITS = 128; + private static final int AES_GCM_IV_LENGTH = 12; + private static final String AES_LOCAL_PROTECTION_KEY_ALIAS = "aes_local_protection"; + + private static final String RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS = "rsa_wrap_local_protection"; + private static final String RSA_WRAP_CIPHER_TYPE = "RSA/NONE/PKCS1Padding"; + private static final String AES_WRAPPED_PROTECTION_KEY_SHARED_PREFERENCE = "aes_wrapped_local_protection"; + + private static final String SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED = "android_version_when_key_has_been_generated"; + + private static SecretKeyAndVersion sSecretKeyAndVersion; + private static SecureRandom sPrng; + + /** + * Create a GZIPOutputStream instance + * Special treatment on KitKat device, force the syncFlush param to false + * Before Kitkat, this param does not exist and after Kitkat it is set to false by default + * + * @param outputStream the output stream + */ + public static GZIPOutputStream createGzipOutputStream(OutputStream outputStream) throws IOException { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + return new GZIPOutputStream(outputStream, false); + } else { + return new GZIPOutputStream(outputStream); + } + } + + /** + * Returns the AES key used for local storage encryption/decryption with AES/GCM. + * The key is created if it does not exist already in the keystore. + * From Marshmallow, this key is generated and operated directly from the android keystore. + * From KitKat and before Marshmallow, this key is stored in the application shared preferences + * wrapped by a RSA key generated and operated directly from the android keystore. + * + * @param context the context holding the application shared preferences + */ + @RequiresApi(Build.VERSION_CODES.KITKAT) + private static synchronized SecretKeyAndVersion getAesGcmLocalProtectionKey(Context context) + throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException, + NoSuchProviderException, InvalidAlgorithmParameterException, NoSuchPaddingException, + InvalidKeyException, IllegalBlockSizeException, UnrecoverableKeyException { + if (sSecretKeyAndVersion == null) { + final KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER); + keyStore.load(null); + + Log.i(TAG, "Loading local protection key"); + + SecretKey key; + + final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + // Get the version of Android when the key has been generated, default to the current version of the system. In this case, the + // key will be generated + final int androidVersionWhenTheKeyHasBeenGenerated + = sharedPreferences.getInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (keyStore.containsAlias(AES_LOCAL_PROTECTION_KEY_ALIAS)) { + Log.i(TAG, "AES local protection key found in keystore"); + key = (SecretKey) keyStore.getKey(AES_LOCAL_PROTECTION_KEY_ALIAS, null); + } else { + // Check if a key has been created on version < M (in case of OS upgrade) + key = readKeyApiL(sharedPreferences, keyStore); + + if (key == null) { + Log.i(TAG, "Generating AES key with keystore"); + final KeyGenerator generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE_PROVIDER); + generator.init( + new KeyGenParameterSpec.Builder(AES_LOCAL_PROTECTION_KEY_ALIAS, + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setKeySize(AES_GCM_KEY_SIZE_IN_BITS) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build()); + key = generator.generateKey(); + + sharedPreferences.edit() + .putInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT) + .apply(); + } + } + } else { + key = readKeyApiL(sharedPreferences, keyStore); + + if (key == null) { + Log.i(TAG, "Generating RSA key pair with keystore"); + final KeyPairGenerator generator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEY_STORE_PROVIDER); + final Calendar start = Calendar.getInstance(); + final Calendar end = Calendar.getInstance(); + end.add(Calendar.YEAR, 10); + + generator.initialize( + new KeyPairGeneratorSpec.Builder(context) + .setAlgorithmParameterSpec(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4)) + .setAlias(RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS) + .setSubject(new X500Principal("CN=matrix-android-sdk")) + .setStartDate(start.getTime()) + .setEndDate(end.getTime()) + .setSerialNumber(BigInteger.ONE) + .build()); + final KeyPair keyPair = generator.generateKeyPair(); + + Log.i(TAG, "Generating wrapped AES key"); + + final byte[] aesKeyRaw = new byte[AES_GCM_KEY_SIZE_IN_BITS / Byte.SIZE]; + getPrng().nextBytes(aesKeyRaw); + key = new SecretKeySpec(aesKeyRaw, "AES"); + + final Cipher cipher = Cipher.getInstance(RSA_WRAP_CIPHER_TYPE); + cipher.init(Cipher.WRAP_MODE, keyPair.getPublic()); + byte[] wrappedAesKey = cipher.wrap(key); + + sharedPreferences.edit() + .putString(AES_WRAPPED_PROTECTION_KEY_SHARED_PREFERENCE, Base64.encodeToString(wrappedAesKey, 0)) + .putInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT) + .apply(); + } + } + + sSecretKeyAndVersion = new SecretKeyAndVersion(key, androidVersionWhenTheKeyHasBeenGenerated); + } + + return sSecretKeyAndVersion; + } + + /** + * Read the key, which may have been stored when the OS was < M + * + * @param sharedPreferences shared pref + * @param keyStore key store + * @return the key if it exists or null + */ + @Nullable + private static SecretKey readKeyApiL(SharedPreferences sharedPreferences, KeyStore keyStore) + throws KeyStoreException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, UnrecoverableKeyException { + final String wrappedAesKeyString = sharedPreferences.getString(AES_WRAPPED_PROTECTION_KEY_SHARED_PREFERENCE, null); + if (wrappedAesKeyString != null && keyStore.containsAlias(RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS)) { + Log.i(TAG, "RSA + wrapped AES local protection keys found in keystore"); + final PrivateKey privateKey = (PrivateKey) keyStore.getKey(RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS, null); + final byte[] wrappedAesKey = Base64.decode(wrappedAesKeyString, 0); + final Cipher cipher = Cipher.getInstance(RSA_WRAP_CIPHER_TYPE); + cipher.init(Cipher.UNWRAP_MODE, privateKey); + return (SecretKey) cipher.unwrap(wrappedAesKey, "AES", Cipher.SECRET_KEY); + } + + // Key does not exist + return null; + } + + /** + * Returns the unique SecureRandom instance shared for all local storage encryption operations. + */ + private static SecureRandom getPrng() { + if (sPrng == null) { + sPrng = new SecureRandom(); + } + + return sPrng; + } + + /** + * Create a CipherOutputStream instance. + * Before Kitkat, this method will return out as local storage encryption is not implemented for + * devices before KitKat. + * + * @param out the output stream + * @param context the context holding the application shared preferences + */ + @Nullable + public static OutputStream createCipherOutputStream(OutputStream out, Context context) + throws IOException, CertificateException, NoSuchAlgorithmException, + UnrecoverableKeyException, InvalidKeyException, InvalidAlgorithmParameterException, + NoSuchPaddingException, NoSuchProviderException, KeyStoreException, IllegalBlockSizeException { + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return out; + } + + final SecretKeyAndVersion keyAndVersion = getAesGcmLocalProtectionKey(context); + if (keyAndVersion == null || keyAndVersion.getSecretKey() == null) { + throw new KeyStoreException(); + } + + final Cipher cipher = Cipher.getInstance(AES_GCM_CIPHER_TYPE); + byte[] iv; + + if (keyAndVersion.getAndroidVersionWhenTheKeyHasBeenGenerated() >= Build.VERSION_CODES.M) { + cipher.init(Cipher.ENCRYPT_MODE, keyAndVersion.getSecretKey()); + iv = cipher.getIV(); + } else { + iv = new byte[AES_GCM_IV_LENGTH]; + getPrng().nextBytes(iv); + cipher.init(Cipher.ENCRYPT_MODE, keyAndVersion.getSecretKey(), new IvParameterSpec(iv)); + } + + if (iv.length != AES_GCM_IV_LENGTH) { + Log.e(TAG, "Invalid IV length " + iv.length); + return null; + } + + out.write(iv.length); + out.write(iv); + + return new CipherOutputStream(out, cipher); + } + + /** + * Create a CipherInputStream instance. + * Before Kitkat, this method will return `in` because local storage encryption is not implemented for devices before KitKat. + * Warning, if `in` is not an encrypted stream, it's up to the caller to close and reopen `in`, because the stream has been read. + * + * @param in the input stream + * @param context the context holding the application shared preferences + * @return in, or the created InputStream, or null if the InputStream `in` does not contain encrypted data + */ + @Nullable + public static InputStream createCipherInputStream(InputStream in, Context context) + throws NoSuchPaddingException, NoSuchAlgorithmException, CertificateException, + InvalidKeyException, KeyStoreException, UnrecoverableKeyException, IllegalBlockSizeException, + NoSuchProviderException, InvalidAlgorithmParameterException, IOException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return in; + } + + final int iv_len = in.read(); + if (iv_len != AES_GCM_IV_LENGTH) { + Log.e(TAG, "Invalid IV length " + iv_len); + return null; + } + + final byte[] iv = new byte[AES_GCM_IV_LENGTH]; + in.read(iv); + + final Cipher cipher = Cipher.getInstance(AES_GCM_CIPHER_TYPE); + + final SecretKeyAndVersion keyAndVersion = getAesGcmLocalProtectionKey(context); + if (keyAndVersion == null || keyAndVersion.getSecretKey() == null) { + throw new KeyStoreException(); + } + + AlgorithmParameterSpec spec; + + if (keyAndVersion.getAndroidVersionWhenTheKeyHasBeenGenerated() >= Build.VERSION_CODES.M) { + spec = new GCMParameterSpec(AES_GCM_KEY_SIZE_IN_BITS, iv); + } else { + spec = new IvParameterSpec(iv); + } + + cipher.init(Cipher.DECRYPT_MODE, keyAndVersion.getSecretKey(), spec); + + return new CipherInputStream(in, cipher); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/ContentManager.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/ContentManager.java new file mode 100644 index 0000000000..740930edc0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/ContentManager.java @@ -0,0 +1,229 @@ +/* + * 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.util; + +import android.support.annotation.Nullable; + +import im.vector.matrix.android.internal.legacy.HomeServerConnectionConfig; +import im.vector.matrix.android.internal.legacy.RestClient; + +/** + * Class for accessing content from the current session. + */ +public class ContentManager { + private static final String LOG_TAG = ContentManager.class.getSimpleName(); + + public static final String MATRIX_CONTENT_URI_SCHEME = "mxc://"; + + public static final String METHOD_CROP = "crop"; + public static final String METHOD_SCALE = "scale"; + + public static final String URI_PREFIX_CONTENT_API = "/_matrix/media/v1/"; + + public static final String MATRIX_CONTENT_IDENTICON_PREFIX = "identicon/"; + + // HS config + private final HomeServerConnectionConfig mHsConfig; + + // the unsent events Manager + private final UnsentEventsManager mUnsentEventsManager; + + // AV scanner handling + private boolean mIsAvScannerEnabled; + private String mDownloadUrlPrefix; + + /** + * Default constructor. + * + * @param hsConfig the HomeserverConnectionConfig to use + * @param unsentEventsManager the unsent events manager + */ + public ContentManager(HomeServerConnectionConfig hsConfig, UnsentEventsManager unsentEventsManager) { + mHsConfig = hsConfig; + mUnsentEventsManager = unsentEventsManager; + // The AV scanner is disabled by default + configureAntiVirusScanner(false); + } + + /** + * Configure the anti-virus scanner. + * If the anti-virus server url is different than the home server url, + * it must be provided in HomeServerConnectionConfig. + * The home server url is considered by default. + * + * @param isEnabled true to enable the anti-virus scanner, false otherwise. + */ + public void configureAntiVirusScanner(boolean isEnabled) { + mIsAvScannerEnabled = isEnabled; + if (isEnabled) { + mDownloadUrlPrefix = mHsConfig.getAntiVirusServerUri().toString() + "/" + RestClient.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE; + } else { + mDownloadUrlPrefix = mHsConfig.getHomeserverUri().toString() + URI_PREFIX_CONTENT_API; + } + } + + public boolean isAvScannerEnabled() { + return mIsAvScannerEnabled; + } + + /** + * @return the hs config. + */ + public HomeServerConnectionConfig getHsConfig() { + return mHsConfig; + } + + /** + * @return the unsent events manager + */ + public UnsentEventsManager getUnsentEventsManager() { + return mUnsentEventsManager; + } + + /** + * Compute the identicon URL for an userId. + * + * @param userId the user id. + * @return the url + */ + public static String getIdenticonURL(String userId) { + // sanity check + if (null != userId) { + String urlEncodedUser = null; + try { + urlEncodedUser = java.net.URLEncoder.encode(userId, "UTF-8"); + } catch (Exception e) { + Log.e(LOG_TAG, "## getIdenticonURL() : java.net.URLEncoder.encode failed " + e.getMessage(), e); + } + + return ContentManager.MATRIX_CONTENT_URI_SCHEME + MATRIX_CONTENT_IDENTICON_PREFIX + urlEncodedUser; + } + + return null; + } + + /** + * Check whether an url is a valid matrix content url. + * + * @param contentUrl the content URL (in the form of "mxc://..."). + * @return true if contentUrl is valid. + */ + public static boolean isValidMatrixContentUrl(String contentUrl) { + return (null != contentUrl && contentUrl.startsWith(MATRIX_CONTENT_URI_SCHEME)); + } + + /** + * Returns the task identifier used to download the content at a Matrix media content URI + * (in the form of "mxc://..."). + * + * @param contentUrl the matrix content url. + * @return the task identifier, or null if the url is invalid.. + */ + @Nullable + public String downloadTaskIdForMatrixMediaContent(String contentUrl) { + if (isValidMatrixContentUrl(contentUrl)) { + // We extract the server name and the media id from the matrix content url + // to define a unique download task id + return contentUrl.substring(MATRIX_CONTENT_URI_SCHEME.length()); + } + + // do not allow non-mxc content URLs: we should not be making requests out to whatever + // http urls people send us + return null; + } + + /** + * Get the actual URL for accessing the full-size image of a Matrix media content URI. + * + * @param contentUrl the Matrix media content URI (in the form of "mxc://..."). + * @return the URL to access the described resource, or null if the url is invalid. + * @deprecated See getDownloadableUrl(contentUrl, isEncrypted). + */ + @Nullable + public String getDownloadableUrl(String contentUrl) { + // Suppose here by default that the content is not encrypted. + // FIXME this method should be removed as soon as possible + return getDownloadableUrl(contentUrl, false); + } + + /** + * Get the actual URL for accessing the full-size image of a Matrix media content URI. + * + * @param contentUrl the Matrix media content URI (in the form of "mxc://..."). + * @param isEncrypted tell whether the related content is encrypted (This information is + * required when the anti-virus scanner is enabled). + * @return the URL to access the described resource, or null if the url is invalid. + */ + @Nullable + public String getDownloadableUrl(String contentUrl, boolean isEncrypted) { + if (isValidMatrixContentUrl(contentUrl)) { + if (!isEncrypted || !mIsAvScannerEnabled) { + String mediaServerAndId = contentUrl.substring(MATRIX_CONTENT_URI_SCHEME.length()); + return mDownloadUrlPrefix + "download/" + mediaServerAndId; + } else { + // In case of encrypted content, a unique url is used when the scanner is enabled + // The encryption info must be sent in the body of the request. + return mDownloadUrlPrefix + "download_encrypted"; + } + } + + // do not allow non-mxc content URLs + return null; + } + + /** + * Get the actual URL for accessing the thumbnail image of a given Matrix media content URI. + * + * @param contentUrl the Matrix media content URI (in the form of "mxc://..."). + * @param width the desired width + * @param height the desired height + * @param method the desired scale method (METHOD_CROP or METHOD_SCALE) + * @return the URL to access the described resource, or null if the url is invalid. + */ + @Nullable + public String getDownloadableThumbnailUrl(String contentUrl, int width, int height, String method) { + if (isValidMatrixContentUrl(contentUrl)) { + String mediaServerAndId = contentUrl.substring(MATRIX_CONTENT_URI_SCHEME.length()); + + // ignore the #auto pattern + if (mediaServerAndId.endsWith("#auto")) { + mediaServerAndId = mediaServerAndId.substring(0, mediaServerAndId.length() - "#auto".length()); + } + + // Build the thumbnail url. + String url; + // Caution: identicon has no thumbnail path. + if (mediaServerAndId.startsWith(MATRIX_CONTENT_IDENTICON_PREFIX)) { + // identicon url still go to the media repo since they don’t need virus scanning + url = mHsConfig.getHomeserverUri().toString() + URI_PREFIX_CONTENT_API; + } else { + // Use the current download url prefix to take into account a potential antivirus scanner + url = mDownloadUrlPrefix + "thumbnail/"; + } + + url += mediaServerAndId; + url += "?width=" + width; + url += "&height=" + height; + url += "&method=" + method; + return url; + } + + // do not allow non-mxc content URLs + return null; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/ContentUtils.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/ContentUtils.java new file mode 100644 index 0000000000..2eb3316348 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/ContentUtils.java @@ -0,0 +1,187 @@ +/* + * 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.util; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import android.os.Build; +import android.os.StatFs; +import android.system.Os; +import android.webkit.MimeTypeMap; + +import im.vector.matrix.android.internal.legacy.rest.model.message.ImageInfo; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * Static content utility methods. + */ +public class ContentUtils { + private static final String LOG_TAG = ContentUtils.class.getSimpleName(); + + /** + * Build an ImageInfo object based on the image at the given path. + * + * @param filePath the path to the image in storage + * @return the image info + */ + public static ImageInfo getImageInfoFromFile(String filePath) { + ImageInfo imageInfo = new ImageInfo(); + try { + Bitmap imageBitmap = BitmapFactory.decodeFile(filePath); + imageInfo.w = imageBitmap.getWidth(); + imageInfo.h = imageBitmap.getHeight(); + + File file = new File(filePath); + imageInfo.size = file.length(); + + imageInfo.mimetype = getMimeType(filePath); + } catch (OutOfMemoryError oom) { + Log.e(LOG_TAG, "## getImageInfoFromFile() : oom", oom); + } + + return imageInfo; + } + + public static String getMimeType(String filePath) { + MimeTypeMap mime = MimeTypeMap.getSingleton(); + return mime.getMimeTypeFromExtension(filePath.substring(filePath.lastIndexOf('.') + 1).toLowerCase()); + } + + /** + * Delete a directory with its content + * + * @param directory the base directory + * @return true if the directory is deleted + */ + public static boolean deleteDirectory(File directory) { + // sanity check + if (null == directory) { + return false; + } + + boolean succeed = true; + + if (directory.exists()) { + File[] files = directory.listFiles(); + + if (null != files) { + for (int i = 0; i < files.length; i++) { + if (files[i].isDirectory()) { + succeed &= deleteDirectory(files[i]); + } else { + succeed &= files[i].delete(); + } + } + } + } + + return succeed && directory.delete(); + } + + /** + * Recursive method to compute a directory size + * + * @param context the context + * @param directory the directory + * @param logPathDepth the depth to log + * @return the directory size + */ + @SuppressLint("deprecation") + public static long getDirectorySize(Context context, File directory, int logPathDepth) { + StatFs statFs = new StatFs(directory.getAbsolutePath()); + long blockSize; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + blockSize = statFs.getBlockSizeLong(); + } else { + blockSize = statFs.getBlockSize(); + } + + if (blockSize < 0) { + blockSize = 1; + } + + return getDirectorySize(context, directory, logPathDepth, blockSize); + } + + /** + * Recursive method to compute a directory size + * + * @param context the context + * @param directory the directory + * @param logPathDepth the depth to log + * @param blockSize the filesystem block size + * @return the directory size + */ + public static long getDirectorySize(Context context, File directory, int logPathDepth, long blockSize) { + long size = 0; + + File[] files = directory.listFiles(); + + if (null != files) { + for (int i = 0; i < files.length; i++) { + File file = files[i]; + + if (!file.isDirectory()) { + size += (file.length() / blockSize + 1) * blockSize; + } else { + size += getDirectorySize(context, file, logPathDepth - 1); + } + } + } + + if (logPathDepth > 0) { + Log.d(LOG_TAG, "## getDirectorySize() " + directory.getPath() + " " + android.text.format.Formatter.formatFileSize(context, size)); + } + + return size; + } + + @SuppressLint("NewApi") + public static long getLastAccessTime(File file) { + long lastAccessTime = file.lastModified(); + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + lastAccessTime = Os.lstat(file.getAbsolutePath()).st_atime; + } else { + Class clazz = Class.forName("libcore.io.Libcore"); + Field field = clazz.getDeclaredField("os"); + if (!field.isAccessible()) { + field.setAccessible(true); + } + Object os = field.get(null); + + Method method = os.getClass().getMethod("lstat", String.class); + Object lstat = method.invoke(os, file.getAbsolutePath()); + + field = lstat.getClass().getDeclaredField("st_atime"); + if (!field.isAccessible()) { + field.setAccessible(true); + } + lastAccessTime = field.getLong(lstat); + } + } catch (Exception e) { + Log.e(LOG_TAG, "## getLastAccessTime() failed " + e.getMessage() + " for file " + file, e); + } + return lastAccessTime; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/EventDisplay.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/EventDisplay.java new file mode 100644 index 0000000000..7d09f7714c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/EventDisplay.java @@ -0,0 +1,620 @@ +/* + * 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.util; + +import android.content.Context; +import android.graphics.Typeface; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.Html; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +import im.vector.matrix.android.R; +import im.vector.matrix.android.internal.legacy.call.MXCallsManager; +import im.vector.matrix.android.internal.legacy.crypto.MXCryptoError; +import im.vector.matrix.android.internal.legacy.data.RoomState; +import im.vector.matrix.android.internal.legacy.interfaces.HtmlToolbox; +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.RedactedBecause; +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.pid.RoomThirdPartyInvite; + +/** + * Class helper to stringify an event + */ +public class EventDisplay { + private static final String LOG_TAG = EventDisplay.class.getSimpleName(); + + private static final String MESSAGE_IN_REPLY_TO_FIRST_PART = "
"; + private static final String MESSAGE_IN_REPLY_TO_LAST_PART = ""; + + // members + protected final Event mEvent; + protected final Context mContext; + protected final RoomState mRoomState; + + @Nullable + protected final HtmlToolbox mHtmlToolbox; + + protected boolean mPrependAuthor; + + // let the application defines if the redacted events must be displayed + public static final boolean mDisplayRedactedEvents = false; + + // constructor + public EventDisplay(Context context, Event event, RoomState roomState) { + this(context, event, roomState, null); + } + + // constructor + public EventDisplay(Context context, Event event, RoomState roomState, @Nullable HtmlToolbox htmlToolbox) { + mContext = context.getApplicationContext(); + mEvent = event; + mRoomState = roomState; + mHtmlToolbox = htmlToolbox; + } + + /** + *

Prepend the text with the author's name if they have not been mentioned in the text.

+ * This will prepend text messages with the author's name. This will NOT prepend things like + * emote, room topic changes, etc which already mention the author's name in the message. + * + * @param prepend true to prepend the message author. + */ + public void setPrependMessagesWithAuthor(boolean prepend) { + mPrependAuthor = prepend; + } + + /** + * Compute an "human readable" name for an user Id. + * + * @param userId the user id + * @param roomState the room state + * @return the user display name + */ + protected static String getUserDisplayName(String userId, RoomState roomState) { + if (null != roomState) { + return roomState.getMemberName(userId); + } else { + return userId; + } + } + + /** + * Stringify the linked event. + * + * @return The text or null if it isn't possible. + */ + public CharSequence getTextualDisplay() { + return getTextualDisplay(null); + } + + /** + * Stringify the linked event. + * + * @param displayNameColor the display name highlighted color. + * @return The text or null if it isn't possible. + */ + public CharSequence getTextualDisplay(Integer displayNameColor) { + CharSequence text = null; + + try { + JsonObject jsonEventContent = mEvent.getContentAsJsonObject(); + + String userDisplayName = getUserDisplayName(mEvent.getSender(), mRoomState); + String eventType = mEvent.getType(); + + if (mEvent.isCallEvent()) { + if (Event.EVENT_TYPE_CALL_INVITE.equals(eventType)) { + boolean isVideo = false; + // detect call type from the sdp + try { + JsonObject offer = jsonEventContent.get("offer").getAsJsonObject(); + JsonElement sdp = offer.get("sdp"); + String sdpValue = sdp.getAsString(); + isVideo = sdpValue.contains("m=video"); + } catch (Exception e) { + Log.e(LOG_TAG, "getTextualDisplay : " + e.getMessage(), e); + } + + if (isVideo) { + return mContext.getString(R.string.notice_placed_video_call, userDisplayName); + } else { + return mContext.getString(R.string.notice_placed_voice_call, userDisplayName); + } + } else if (Event.EVENT_TYPE_CALL_ANSWER.equals(eventType)) { + return mContext.getString(R.string.notice_answered_call, userDisplayName); + } else if (Event.EVENT_TYPE_CALL_HANGUP.equals(eventType)) { + return mContext.getString(R.string.notice_ended_call, userDisplayName); + } else { + return eventType; + } + } else if (Event.EVENT_TYPE_STATE_HISTORY_VISIBILITY.equals(eventType)) { + CharSequence subpart; + String historyVisibility = (null != jsonEventContent.get("history_visibility")) ? + jsonEventContent.get("history_visibility").getAsString() : RoomState.HISTORY_VISIBILITY_SHARED; + + if (TextUtils.equals(historyVisibility, RoomState.HISTORY_VISIBILITY_SHARED)) { + subpart = mContext.getString(R.string.notice_room_visibility_shared); + } else if (TextUtils.equals(historyVisibility, RoomState.HISTORY_VISIBILITY_INVITED)) { + subpart = mContext.getString(R.string.notice_room_visibility_invited); + } else if (TextUtils.equals(historyVisibility, RoomState.HISTORY_VISIBILITY_JOINED)) { + subpart = mContext.getString(R.string.notice_room_visibility_joined); + } else if (TextUtils.equals(historyVisibility, RoomState.HISTORY_VISIBILITY_WORLD_READABLE)) { + subpart = mContext.getString(R.string.notice_room_visibility_world_readable); + } else { + subpart = mContext.getString(R.string.notice_room_visibility_unknown, historyVisibility); + } + + text = mContext.getString(R.string.notice_made_future_room_visibility, userDisplayName, subpart); + } else if (Event.EVENT_TYPE_RECEIPT.equals(eventType)) { + // the read receipt should not be displayed + text = "Read Receipt"; + } else if (Event.EVENT_TYPE_MESSAGE.equals(eventType)) { + final String msgtype = (null != jsonEventContent.get("msgtype")) ? jsonEventContent.get("msgtype").getAsString() : ""; + // all m.room.message events should support the 'body' key fallback, so use it. + + text = jsonEventContent.has("body") ? jsonEventContent.get("body").getAsString() : null; + // check for html formatting + if (jsonEventContent.has("formatted_body") && jsonEventContent.has("format")) { + text = getFormattedMessage(mContext, jsonEventContent, mHtmlToolbox); + } + // avoid empty image name + if (TextUtils.equals(msgtype, Message.MSGTYPE_IMAGE) && TextUtils.isEmpty(text)) { + text = mContext.getString(R.string.summary_user_sent_image, userDisplayName); + } else if (TextUtils.equals(msgtype, Message.MSGTYPE_EMOTE)) { + text = "* " + userDisplayName + " " + text; + } else if (TextUtils.isEmpty(text)) { + text = ""; + } else if (mPrependAuthor) { + text = new SpannableStringBuilder(mContext.getString(R.string.summary_message, userDisplayName, text)); + + if (null != displayNameColor) { + ((SpannableStringBuilder) text).setSpan(new ForegroundColorSpan(displayNameColor), + 0, userDisplayName.length() + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + ((SpannableStringBuilder) text).setSpan(new StyleSpan(Typeface.BOLD), + 0, userDisplayName.length() + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + } else if (Event.EVENT_TYPE_STICKER.equals(eventType)) { + // all m.stickers events should support the 'body' key fallback, so use it. + text = jsonEventContent.has("body") ? jsonEventContent.get("body").getAsString() : null; + + if (TextUtils.isEmpty(text)) { + text = mContext.getString(R.string.summary_user_sent_sticker, userDisplayName); + } + + } else if (Event.EVENT_TYPE_MESSAGE_ENCRYPTION.equals(eventType)) { + text = mContext.getString(R.string.notice_end_to_end, userDisplayName, mEvent.getWireEventContent().algorithm); + } else if (Event.EVENT_TYPE_MESSAGE_ENCRYPTED.equals(eventType)) { + // don't display + if (mEvent.isRedacted()) { + String redactedInfo = EventDisplay.getRedactionMessage(mContext, mEvent, mRoomState); + + if (TextUtils.isEmpty(redactedInfo)) { + return null; + } else { + return redactedInfo; + } + } else { + String message = null; + + + if (null != mEvent.getCryptoError()) { + String errorDescription; + + MXCryptoError error = mEvent.getCryptoError(); + + if (TextUtils.equals(error.errcode, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE)) { + errorDescription = mContext.getResources().getString(R.string.notice_crypto_error_unkwown_inbound_session_id); + } else { + errorDescription = error.getLocalizedMessage(); + } + + message = mContext.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription); + } + + if (TextUtils.isEmpty(message)) { + message = mContext.getString(R.string.encrypted_message); + } + + SpannableString spannableStr = new SpannableString(message); + spannableStr.setSpan(new StyleSpan(Typeface.ITALIC), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + text = spannableStr; + } + } else if (Event.EVENT_TYPE_STATE_ROOM_TOPIC.equals(eventType)) { + String topic = jsonEventContent.getAsJsonPrimitive("topic").getAsString(); + + if (mEvent.isRedacted()) { + String redactedInfo = EventDisplay.getRedactionMessage(mContext, mEvent, mRoomState); + + if (TextUtils.isEmpty(redactedInfo)) { + return null; + } + + topic = redactedInfo; + } + + if (!TextUtils.isEmpty(topic)) { + text = mContext.getString(R.string.notice_topic_changed, userDisplayName, topic); + } else { + text = mContext.getString(R.string.notice_room_topic_removed, userDisplayName); + } + } else if (Event.EVENT_TYPE_STATE_ROOM_NAME.equals(eventType)) { + JsonPrimitive nameAsJson = jsonEventContent.getAsJsonPrimitive("name"); + String roomName = (null == nameAsJson) ? null : nameAsJson.getAsString(); + + if (mEvent.isRedacted()) { + String redactedInfo = EventDisplay.getRedactionMessage(mContext, mEvent, mRoomState); + + if (TextUtils.isEmpty(redactedInfo)) { + return null; + } + + roomName = redactedInfo; + } + + if (!TextUtils.isEmpty(roomName)) { + text = mContext.getString(R.string.notice_room_name_changed, userDisplayName, roomName); + } else { + text = mContext.getString(R.string.notice_room_name_removed, userDisplayName); + } + } else if (Event.EVENT_TYPE_STATE_ROOM_THIRD_PARTY_INVITE.equals(eventType)) { + RoomThirdPartyInvite invite = JsonUtils.toRoomThirdPartyInvite(mEvent.getContent()); + String displayName = invite.display_name; + + if (mEvent.isRedacted()) { + String redactedInfo = EventDisplay.getRedactionMessage(mContext, mEvent, mRoomState); + + if (TextUtils.isEmpty(redactedInfo)) { + return null; + } + + displayName = redactedInfo; + } + + text = mContext.getString(R.string.notice_room_third_party_invite, userDisplayName, displayName); + } else if (Event.EVENT_TYPE_STATE_ROOM_MEMBER.equals(eventType)) { + text = getMembershipNotice(mContext, mEvent, mRoomState); + } + } catch (Exception e) { + Log.e(LOG_TAG, "getTextualDisplay() " + e.getMessage(), e); + } + + return text; + } + + /** + * Compute the redact text for an event. + * + * @param context the context + * @param event the event + * @param roomState the room state + * @return the redacted event text + */ + public static String getRedactionMessage(Context context, Event event, RoomState roomState) { + // test if the redacted event must be displayed. + if (!mDisplayRedactedEvents) { + return null; + } + + // Check first whether the event has been redacted + String redactedInfo = null; + + if (event.isRedacted() && (null != roomState)) { + RedactedBecause redactedBecause = event.unsigned.redacted_because; + String redactedBy = redactedBecause.sender; + String redactedReason = null; + + if (null != redactedBecause.content) { + redactedReason = redactedBecause.content.reason; + } + + if (!TextUtils.isEmpty(redactedReason)) { + if (!TextUtils.isEmpty(redactedBy)) { + redactedBy = context.getString(R.string.notice_event_redacted_by, redactedBy) + + context.getString(R.string.notice_event_redacted_reason, redactedReason); + } else { + redactedBy = context.getString(R.string.notice_event_redacted_reason, redactedReason); + } + } else if (!TextUtils.isEmpty(redactedBy)) { + redactedBy = context.getString(R.string.notice_event_redacted_by, redactedBy); + } + + redactedInfo = context.getString(R.string.notice_event_redacted, redactedBy); + } + + return redactedInfo; + } + + /** + * Compute the sender display name + * + * @param event the event + * @param eventContent the event content + * @param prevEventContent the prev event content + * @param roomState the room state + * @return the "human readable" display name + */ + protected static String senderDisplayNameForEvent(Event event, EventContent eventContent, EventContent prevEventContent, RoomState roomState) { + String senderDisplayName = event.getSender(); + + if (!event.isRedacted()) { + if (null != roomState) { + // Consider first the current display name defined in provided room state + // (Note: this room state is supposed to not take the new event into account) + senderDisplayName = roomState.getMemberName(event.getSender()); + } + + // Check whether this sender name is updated by the current event (This happens in case of new joined member) + if ((null != eventContent) && TextUtils.equals(RoomMember.MEMBERSHIP_JOIN, eventContent.membership)) { + // detect if it is displayname update + // a display name update is detected when the previous state was join and there was a displayname + if (!TextUtils.isEmpty(eventContent.displayname) + || ((null != prevEventContent) + && TextUtils.equals(RoomMember.MEMBERSHIP_JOIN, prevEventContent.membership) + && !TextUtils.isEmpty(prevEventContent.displayname))) { + senderDisplayName = eventContent.displayname; + } + } + } + + return senderDisplayName; + } + + /** + * Build a membership notice text from its dedicated event. + * + * @param context the context. + * @param event the event. + * @param roomState the room state. + * @return the membership text. + */ + public static String getMembershipNotice(Context context, Event event, RoomState roomState) { + JsonObject content = event.getContentAsJsonObject(); + + // don't support redacted membership event + if ((null == content) || (content.entrySet().size() == 0)) { + return null; + } + + EventContent eventContent = JsonUtils.toEventContent(event.getContentAsJsonObject()); + EventContent prevEventContent = event.getPrevContent(); + + String senderDisplayName = senderDisplayNameForEvent(event, eventContent, prevEventContent, roomState); + String prevUserDisplayName = null; + + String prevMembership = null; + + if (null != prevEventContent) { + prevMembership = prevEventContent.membership; + } + + if ((null != prevEventContent)) { + prevUserDisplayName = prevEventContent.displayname; + } + + // use by default the provided display name + String targetDisplayName = eventContent.displayname; + + // if it is not provided, use the stateKey value + // and try to retrieve a valid display name + if (null == targetDisplayName) { + targetDisplayName = event.stateKey; + if ((null != targetDisplayName) && (null != roomState) && !event.isRedacted()) { + targetDisplayName = roomState.getMemberName(targetDisplayName); + } + } + + // Check whether the sender has updated his profile (the membership is then unchanged) + if (TextUtils.equals(prevMembership, eventContent.membership)) { + String redactedInfo = EventDisplay.getRedactionMessage(context, event, roomState); + + // Is redacted event? + if (event.isRedacted()) { + + // Here the event is ignored (no display) + if (null == redactedInfo) { + return null; + } + + return context.getString(R.string.notice_profile_change_redacted, senderDisplayName, redactedInfo); + } else { + String displayText = ""; + + if (!TextUtils.equals(senderDisplayName, prevUserDisplayName)) { + if (TextUtils.isEmpty(prevUserDisplayName)) { + if (!TextUtils.equals(event.getSender(), senderDisplayName)) { + displayText = context.getString(R.string.notice_display_name_set, event.getSender(), senderDisplayName); + } + } else if (TextUtils.isEmpty(senderDisplayName)) { + displayText = context.getString(R.string.notice_display_name_removed, event.getSender(), prevUserDisplayName); + } else { + displayText = context.getString(R.string.notice_display_name_changed_from, event.getSender(), prevUserDisplayName, senderDisplayName); + } + } + + // Check whether the avatar has been changed + String avatar = eventContent.avatar_url; + String prevAvatar = null; + + if (null != prevEventContent) { + prevAvatar = prevEventContent.avatar_url; + } + + if (!TextUtils.equals(prevAvatar, avatar)) { + if (!TextUtils.isEmpty(displayText)) { + displayText = displayText + " " + context.getString(R.string.notice_avatar_changed_too); + } else { + displayText = context.getString(R.string.notice_avatar_url_changed, senderDisplayName); + } + } + + return displayText; + } + } else if (RoomMember.MEMBERSHIP_INVITE.equals(eventContent.membership)) { + if (null != eventContent.third_party_invite) { + return context.getString(R.string.notice_room_third_party_registered_invite, targetDisplayName, eventContent.third_party_invite.display_name); + } else { + String selfUserId = null; + + if ((null != roomState) && (null != roomState.getDataHandler())) { + selfUserId = roomState.getDataHandler().getUserId(); + } + + if (TextUtils.equals(event.stateKey, selfUserId)) { + return context.getString(R.string.notice_room_invite_you, senderDisplayName); + } + + if (null == event.stateKey) { + return context.getString(R.string.notice_room_invite_no_invitee, senderDisplayName); + } + + // conference call case + if (targetDisplayName.equals(MXCallsManager.getConferenceUserId(event.roomId))) { + return context.getString(R.string.notice_requested_voip_conference, senderDisplayName); + } + + return context.getString(R.string.notice_room_invite, senderDisplayName, targetDisplayName); + } + } else if (RoomMember.MEMBERSHIP_JOIN.equals(eventContent.membership)) { + // conference call case + if (TextUtils.equals(event.sender, MXCallsManager.getConferenceUserId(event.roomId))) { + return context.getString(R.string.notice_voip_started); + } + + return context.getString(R.string.notice_room_join, senderDisplayName); + } else if (RoomMember.MEMBERSHIP_LEAVE.equals(eventContent.membership)) { + // conference call case + if (TextUtils.equals(event.sender, MXCallsManager.getConferenceUserId(event.roomId))) { + return context.getString(R.string.notice_voip_finished); + } + + // 2 cases here: this member may have left voluntarily or they may have been "left" by someone else ie. kicked + if (TextUtils.equals(event.getSender(), event.stateKey)) { + if ((null != prevEventContent) && TextUtils.equals(prevEventContent.membership, RoomMember.MEMBERSHIP_INVITE)) { + return context.getString(R.string.notice_room_reject, senderDisplayName); + } else { + + // use the latest known displayname + if ((null == eventContent.displayname) && (null != prevUserDisplayName)) { + senderDisplayName = prevUserDisplayName; + } + + return context.getString(R.string.notice_room_leave, senderDisplayName); + } + + } else if (null != prevMembership) { + if (prevMembership.equals(RoomMember.MEMBERSHIP_INVITE)) { + return context.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName); + } else if (prevMembership.equals(RoomMember.MEMBERSHIP_JOIN)) { + return context.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName); + } else if (prevMembership.equals(RoomMember.MEMBERSHIP_BAN)) { + return context.getString(R.string.notice_room_unban, senderDisplayName, targetDisplayName); + } + } + } else if (RoomMember.MEMBERSHIP_BAN.equals(eventContent.membership)) { + return context.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName); + } else if (RoomMember.MEMBERSHIP_KICK.equals(eventContent.membership)) { + return context.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName); + } else { + Log.e(LOG_TAG, "Unknown membership: " + eventContent.membership); + } + return null; + } + + + /** + * @param context the context + * @param jsonEventContent the current jsonEventContent + * @param htmlToolbox an optional htmlToolbox to manage html images and tag + * @return the formatted message as CharSequence + */ + private CharSequence getFormattedMessage(@NonNull final Context context, + @NonNull final JsonObject jsonEventContent, + @Nullable final HtmlToolbox htmlToolbox) { + final String format = jsonEventContent.getAsJsonPrimitive("format").getAsString(); + CharSequence text = null; + if (Message.FORMAT_MATRIX_HTML.equals(format)) { + String htmlBody = jsonEventContent.getAsJsonPrimitive("formatted_body").getAsString(); + if (htmlToolbox != null) { + htmlBody = htmlToolbox.convert(htmlBody); + } + // Special treatment for "In reply to" message + if (jsonEventContent.has("m.relates_to")) { + final JsonElement relatesTo = jsonEventContent.get("m.relates_to"); + if (relatesTo.isJsonObject()) { + if (relatesTo.getAsJsonObject().has("m.in_reply_to")) { + // Note: tag has been removed by HtmlToolbox.convert() + + // Replace
In reply to + // By
['In reply to' from resources] + // To disable the link and to localize the "In reply to" string + if (htmlBody.startsWith(MESSAGE_IN_REPLY_TO_FIRST_PART)) { + final int index = htmlBody.indexOf(MESSAGE_IN_REPLY_TO_LAST_PART); + if (index != -1) { + htmlBody = MESSAGE_IN_REPLY_TO_FIRST_PART + + context.getString(R.string.message_reply_to_prefix) + + htmlBody.substring(index + MESSAGE_IN_REPLY_TO_LAST_PART.length()); + } + } + } + } + } + // some markers are not supported so fallback on an ascii display until to find the right way to manage them + // an issue has been created https://github.com/vector-im/vector-android/issues/38 + // BMA re-enable
    and
  1. support (https://github.com/vector-im/riot-android/issues/2184) + if (!TextUtils.isEmpty(htmlBody)) { + final Html.ImageGetter imageGetter; + final Html.TagHandler tagHandler; + if (htmlToolbox != null) { + imageGetter = htmlToolbox.getImageGetter(); + tagHandler = htmlToolbox.getTagHandler(htmlBody); + } else { + imageGetter = null; + tagHandler = null; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + text = Html.fromHtml(htmlBody, + Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM | Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST, + imageGetter, tagHandler); + } else { + text = Html.fromHtml(htmlBody, imageGetter, tagHandler); + } + // fromHtml formats quotes (> character) with two newlines at the end + // remove any newlines at the end of the CharSequence + while (text.charAt(text.length() - 1) == '\n') { + text = text.subSequence(0, text.length() - 2); + } + } + } + return text; + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/EventUtils.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/EventUtils.java new file mode 100644 index 0000000000..9e15611ace --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/EventUtils.java @@ -0,0 +1,127 @@ +/* + * 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.util; + +import android.text.TextUtils; + +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; +import im.vector.matrix.android.internal.legacy.rest.model.RoomDirectoryVisibility; +import im.vector.matrix.android.internal.legacy.rest.model.bingrules.BingRule; + +import java.util.regex.Pattern; + +/** + * Utility methods for events. + */ +public class EventUtils { + private static final String LOG_TAG = EventUtils.class.getSimpleName(); + + /** + * Whether the given event should be highlighted in its chat room. + * + * @param session the session. + * @param event the event + * @return whether the event is important and should be highlighted + */ + public static boolean shouldHighlight(MXSession session, Event event) { + // sanity check + if ((null == session) || (null == event)) { + return false; + } + + boolean res = false; + + // search if the event fulfills a rule + BingRule rule = session.fulfillRule(event); + + if (null != rule) { + res = rule.shouldHighlight(); + + if (res) { + Log.d(LOG_TAG, "## shouldHighlight() : the event " + event.roomId + "/" + event.eventId + " is higlighted by " + rule); + } + } + + return res; + } + + /** + * Whether the given event should trigger a notification. + * + * @param session the current matrix session + * @param event the event + * @param activeRoomID the RoomID of disaplyed roomActivity + * @return true if the event should trigger a notification + */ + public static boolean shouldNotify(MXSession session, Event event, String activeRoomID) { + if ((null == event) || (null == session)) { + Log.e(LOG_TAG, "shouldNotify invalid params"); + return false; + } + + // Only room events trigger notifications + if (null == event.roomId) { + Log.e(LOG_TAG, "shouldNotify null room ID"); + return false; + } + + if (null == event.getSender()) { + Log.e(LOG_TAG, "shouldNotify null room ID"); + return false; + } + + // No notification if the user is currently viewing the room + if (TextUtils.equals(event.roomId, activeRoomID)) { + return false; + } + + if (shouldHighlight(session, event)) { + return true; + } + + Room room = session.getDataHandler().getRoom(event.roomId); + return RoomDirectoryVisibility.DIRECTORY_VISIBILITY_PRIVATE.equals(room.getVisibility()) + && !TextUtils.equals(event.getSender(), session.getCredentials().userId); + } + + /** + * Returns whether a string contains an occurrence of another, as a standalone word, regardless of case. + * + * @param subString the string to search for + * @param longString the string to search in + * @return whether a match was found + */ + public static boolean caseInsensitiveFind(String subString, String longString) { + // add sanity checks + if (TextUtils.isEmpty(subString) || TextUtils.isEmpty(longString)) { + return false; + } + + boolean res = false; + + try { + Pattern pattern = Pattern.compile("(\\W|^)" + Pattern.quote(subString) + "(\\W|$)", Pattern.CASE_INSENSITIVE); + res = pattern.matcher(longString).find(); + } catch (Exception e) { + Log.e(LOG_TAG, "## caseInsensitiveFind() : failed", e); + } + + return res; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/FilterUtil.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/FilterUtil.java new file mode 100644 index 0000000000..27af8db482 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/FilterUtil.java @@ -0,0 +1,152 @@ +/* + * 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.util; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import im.vector.matrix.android.internal.legacy.rest.model.filter.Filter; +import im.vector.matrix.android.internal.legacy.rest.model.filter.FilterBody; +import im.vector.matrix.android.internal.legacy.rest.model.filter.RoomEventFilter; +import im.vector.matrix.android.internal.legacy.rest.model.filter.RoomFilter; + +import java.util.ArrayList; + +public class FilterUtil { + + /** + * Patch the filterBody to enable or disable the data save mode + *

    + * If data save mode is on, FilterBody will contains + * "{\"room\": {\"ephemeral\": {\"types\": [\"m.receipt\"]}}, \"presence\":{\"notTypes\": [\"*\"]}}" + * + * @param filterBody filterBody to patch + * @param useDataSaveMode true to enable data save mode + */ + public static void enableDataSaveMode(@NonNull FilterBody filterBody, boolean useDataSaveMode) { + if (useDataSaveMode) { + // Enable data save mode + if (filterBody.room == null) { + filterBody.room = new RoomFilter(); + } + if (filterBody.room.ephemeral == null) { + filterBody.room.ephemeral = new RoomEventFilter(); + } + if (filterBody.room.ephemeral.types == null) { + filterBody.room.ephemeral.types = new ArrayList<>(); + } + if (!filterBody.room.ephemeral.types.contains("m.receipt")) { + filterBody.room.ephemeral.types.add("m.receipt"); + } + + if (filterBody.presence == null) { + filterBody.presence = new Filter(); + } + if (filterBody.presence.notTypes == null) { + filterBody.presence.notTypes = new ArrayList<>(); + } + if (!filterBody.presence.notTypes.contains("*")) { + filterBody.presence.notTypes.add("*"); + } + } else { + if (filterBody.room != null + && filterBody.room.ephemeral != null + && filterBody.room.ephemeral.types != null) { + filterBody.room.ephemeral.types.remove("m.receipt"); + + if (filterBody.room.ephemeral.types.isEmpty()) { + filterBody.room.ephemeral.types = null; + } + + if (!filterBody.room.ephemeral.hasData()) { + filterBody.room.ephemeral = null; + } + + if (!filterBody.room.hasData()) { + filterBody.room = null; + } + } + + if (filterBody.presence != null + && filterBody.presence.notTypes != null) { + filterBody.presence.notTypes.remove("*"); + + if (filterBody.presence.notTypes.isEmpty()) { + filterBody.presence.notTypes = null; + } + + if (!filterBody.presence.hasData()) { + filterBody.presence = null; + } + } + } + } + + /** + * Patch the filterBody to enable or disable the lazy loading + *

    + * If lazy loading is on, the filterBody will looks like + * {"room":{"state":{"lazy_load_members":true})} + * + * @param filterBody filterBody to patch + * @param useLazyLoading true to enable lazy loading + */ + public static void enableLazyLoading(FilterBody filterBody, boolean useLazyLoading) { + if (useLazyLoading) { + // Enable lazy loading + if (filterBody.room == null) { + filterBody.room = new RoomFilter(); + } + if (filterBody.room.state == null) { + filterBody.room.state = new RoomEventFilter(); + } + + filterBody.room.state.lazyLoadMembers = true; + } else { + if (filterBody.room != null + && filterBody.room.state != null) { + filterBody.room.state.lazyLoadMembers = null; + + if (!filterBody.room.state.hasData()) { + filterBody.room.state = null; + } + + if (!filterBody.room.hasData()) { + filterBody.room = null; + } + } + } + } + + /** + * Create a RoomEventFilter + * + * @param withLazyLoading true when lazy loading is enabled + * @return a RoomEventFilter or null if lazy loading if OFF + */ + @Nullable + public static RoomEventFilter createRoomEventFilter(boolean withLazyLoading) { + RoomEventFilter roomEventFilter = null; + + if (withLazyLoading) { + roomEventFilter = new RoomEventFilter(); + roomEventFilter.lazyLoadMembers = true; + } + + return roomEventFilter; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/ImageUtils.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/ImageUtils.java new file mode 100644 index 0000000000..c5e881e8fc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/ImageUtils.java @@ -0,0 +1,335 @@ +/* + * 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.util; + +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.ExifInterface; +import android.net.Uri; +import android.provider.MediaStore; +import android.text.TextUtils; + +import im.vector.matrix.android.internal.legacy.db.MXMediasCache; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ImageUtils { + private static final String LOG_TAG = ImageUtils.class.getSimpleName(); + + /** + * Gets the bitmap rotation angle from the {@link android.media.ExifInterface}. + * + * @param context Application context for the content resolver. + * @param uri The URI to find the orientation for. Must be local. + * @return The orientation value, which may be {@link android.media.ExifInterface#ORIENTATION_UNDEFINED}. + */ + public static int getRotationAngleForBitmap(Context context, Uri uri) { + int orientation = getOrientationForBitmap(context, uri); + + int rotationAngle = 0; + + if (ExifInterface.ORIENTATION_ROTATE_90 == orientation) { + rotationAngle = 90; + } else if (ExifInterface.ORIENTATION_ROTATE_180 == orientation) { + rotationAngle = 180; + } else if (ExifInterface.ORIENTATION_ROTATE_270 == orientation) { + rotationAngle = 270; + } + + return rotationAngle; + } + + /** + * Gets the {@link ExifInterface} value for the orientation for this local bitmap Uri. + * + * @param context Application context for the content resolver. + * @param uri The URI to find the orientation for. Must be local. + * @return The orientation value, which may be {@link ExifInterface#ORIENTATION_UNDEFINED}. + */ + public static int getOrientationForBitmap(Context context, Uri uri) { + int orientation = ExifInterface.ORIENTATION_UNDEFINED; + + if (uri == null) { + return orientation; + } + + if (TextUtils.equals(uri.getScheme(), "content")) { + String[] proj = {MediaStore.Images.Media.DATA}; + Cursor cursor = null; + try { + cursor = context.getContentResolver().query(uri, proj, null, null, null); + if (cursor != null && cursor.getCount() > 0) { + cursor.moveToFirst(); + int idxData = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); + String path = cursor.getString(idxData); + if (TextUtils.isEmpty(path)) { + Log.w(LOG_TAG, "Cannot find path in media db for uri " + uri); + return orientation; + } + ExifInterface exif = new ExifInterface(path); + orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); + } + } catch (Exception e) { + // eg SecurityException from com.google.android.apps.photos.content.GooglePhotosImageProvider URIs + // eg IOException from trying to parse the returned path as a file when it is an http uri. + Log.e(LOG_TAG, "Cannot get orientation for bitmap: " + e.getMessage(), e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } else if (TextUtils.equals(uri.getScheme(), "file")) { + try { + ExifInterface exif = new ExifInterface(uri.getPath()); + orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); + } catch (Exception e) { + Log.e(LOG_TAG, "Cannot get EXIF for file uri " + uri + " because " + e.getMessage(), e); + } + } + + return orientation; + } + + public static BitmapFactory.Options decodeBitmapDimensions(InputStream stream) { + BitmapFactory.Options o = new BitmapFactory.Options(); + o.inJustDecodeBounds = true; + BitmapFactory.decodeStream(stream, null, o); + if (o.outHeight == -1 || o.outWidth == -1) { + // this doesn't look like an image... + Log.e(LOG_TAG, "Cannot resize input stream, failed to get w/h."); + return null; + } + return o; + } + + public static int getSampleSize(int w, int h, int maxSize) { + int highestDimensionSize = (h > w) ? h : w; + double ratio = (highestDimensionSize > maxSize) ? (highestDimensionSize / maxSize) : 1.0; + int sampleSize = Integer.highestOneBit((int) Math.floor(ratio)); + if (sampleSize == 0) { + sampleSize = 1; + } + return sampleSize; + } + + /** + * Resize an image from its stream. + * + * @param fullImageStream the image stream + * @param maxSize the square side to draw the image in. -1 to ignore. + * @param aSampleSize the image dimension divider. + * @param quality the image quality (0 -> 100) + * @return a stream of the resized imaged + * @throws IOException file IO exception. + */ + public static InputStream resizeImage(InputStream fullImageStream, int maxSize, int aSampleSize, int quality) throws IOException { + /* + * This is all a bit of a mess because android doesn't ship with sensible bitmap streaming libraries. + * + * General structure here is: (N = size of file, M = decompressed size) + * - Copy inputstream to outstream (Usage: 2N) + * - Release inputstream (Usage: N) + * - Copy outstream to instream (Usage: 2N) --- This is done to make sure we can .reset() the stream else we would potentially + * have to re-download the file once we knew the dimensions of the image (!!!) + * - Release outstream (Usage: N) + * - Decode image dimensions, if the size is good, just return instream, else: + * - Decode the full image with the new sample size (Usage: N + M) + * - Release instream (Usage: M) + * - Bitmap compress to JPEG output stream (Usage: N + M) + * - Release bitmap (Usage: N) + * - Return input stream of output stream (Usage: N) + * Usages assume immediate GC, which is no guarantee. If it didn't, the total usage is 5N + M. In an extreme scenario + * of a full 8 MP image roughly 1.85MB file (3264x2448), this equates to roughly 25 MB of memory. On average, it will + * maybe not immediately release the streams but will probably in the future, so maybe 3N which is ~5.55MB - either + * way this isn't cool. + */ + + ByteArrayOutputStream outstream = new ByteArrayOutputStream(); + + // copy the bytes we just got to the byte array output stream so we can resize.... + byte[] buffer = new byte[2048]; + int l; + while ((l = fullImageStream.read(buffer)) != -1) { + outstream.write(buffer, 0, l); + } + + // we're done with the input stream now so get rid of it (bearing in mind this could be several MB..) + fullImageStream.close(); + + // get the width/height of the image without decoding ALL THE THINGS (though this still makes a copy of the compressed image :/) + ByteArrayInputStream bais = new ByteArrayInputStream(outstream.toByteArray()); + + // allow it to GC.. + outstream.close(); + + BitmapFactory.Options o = decodeBitmapDimensions(bais); + if (o == null) { + return null; + } + int w = o.outWidth; + int h = o.outHeight; + bais.reset(); // yay no need to re-read the stream (which is why we dumped to another stream) + int sampleSize = (maxSize == -1) ? aSampleSize : getSampleSize(w, h, maxSize); + + if (sampleSize == 1) { + // small optimisation + return bais; + } else { + // yucky, we have to decompress the entire (albeit subsampled) bitmap into memory then dump it back into a stream + o = new BitmapFactory.Options(); + o.inSampleSize = sampleSize; + Bitmap bitmap = BitmapFactory.decodeStream(bais, null, o); + if (bitmap == null) { + return null; + } + + bais.close(); + + // recopy it back into an input stream :/ + outstream = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outstream); + + // cleanup + bitmap.recycle(); + + return new ByteArrayInputStream(outstream.toByteArray()); + } + } + + /** + * Apply rotation to the cached image (stored at imageURL). + * The rotated image replaces the genuine one. + * + * @param context the application + * @param imageURL the genuine image URL. + * @param rotationAngle angle in degrees + * @param mediasCache the used media cache + * @return the rotated bitmap + */ + public static Bitmap rotateImage(Context context, String imageURL, int rotationAngle, MXMediasCache mediasCache) { + Bitmap rotatedBitmap = null; + + try { + Uri imageUri = Uri.parse(imageURL); + + // there is one + if (0 != rotationAngle) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + options.outWidth = -1; + options.outHeight = -1; + + // decode the bitmap + Bitmap bitmap = null; + try { + final String filename = imageUri.getPath(); + FileInputStream imageStream = new FileInputStream(new File(filename)); + bitmap = BitmapFactory.decodeStream(imageStream, null, options); + imageStream.close(); + } catch (OutOfMemoryError e) { + Log.e(LOG_TAG, "applyExifRotation BitmapFactory.decodeStream : " + e.getMessage(), e); + } catch (Exception e) { + Log.e(LOG_TAG, "applyExifRotation " + e.getMessage(), e); + } + + android.graphics.Matrix bitmapMatrix = new android.graphics.Matrix(); + bitmapMatrix.postRotate(rotationAngle); + Bitmap transformedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), bitmapMatrix, false); + + // Bitmap.createBitmap() can return the same bitmap, so do not recycle it if it is the case + if (transformedBitmap != bitmap) { + bitmap.recycle(); + } + + if (null != mediasCache) { + mediasCache.saveBitmap(transformedBitmap, imageURL); + } + + rotatedBitmap = transformedBitmap; + } + + } catch (OutOfMemoryError e) { + Log.e(LOG_TAG, "applyExifRotation " + e.getMessage(), e); + } catch (Exception e) { + Log.e(LOG_TAG, "applyExifRotation " + e.getMessage(), e); + } + + return rotatedBitmap; + } + + /** + * Apply exif rotation to the cached image (stored at imageURL). + * The rotated image replaces the genuine one. + * + * @param context the application + * @param imageURL the genuine image URL. + * @param mediasCache the used media cache + * @return the rotated bitmap if the operation succeeds. + */ + public static Bitmap applyExifRotation(Context context, String imageURL, MXMediasCache mediasCache) { + Bitmap rotatedBitmap = null; + + try { + Uri imageUri = Uri.parse(imageURL); + // get the exif rotation angle + final int rotationAngle = ImageUtils.getRotationAngleForBitmap(context, imageUri); + + if (0 != rotationAngle) { + rotatedBitmap = rotateImage(context, imageURL, rotationAngle, mediasCache); + } + + } catch (Exception e) { + Log.e(LOG_TAG, "applyExifRotation " + e.getMessage(), e); + } + + return rotatedBitmap; + } + + /** + * Scale and apply exif rotation to an image defines by its stream. + * + * @param context the context + * @param stream the image stream + * @param mimeType the mime type + * @param maxSide reduce the image to this square side. + * @param rotationAngle the rotation angle + * @param mediasCache the media cache. + * @return the media url + */ + public static String scaleAndRotateImage(Context context, InputStream stream, String mimeType, int maxSide, int rotationAngle, MXMediasCache mediasCache) { + String url = null; + + // sanity checks + if ((null != context) && (null != stream) && (null != mediasCache)) { + try { + InputStream scaledStream = ImageUtils.resizeImage(stream, maxSide, 0, 75); + url = mediasCache.saveMedia(scaledStream, null, mimeType); + rotateImage(context, url, rotationAngle, mediasCache); + } catch (Exception e) { + Log.e(LOG_TAG, "rotateAndScale " + e.getMessage(), e); + } + } + return url; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/JsonUtils.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/JsonUtils.java new file mode 100644 index 0000000000..98af30490c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/JsonUtils.java @@ -0,0 +1,664 @@ +/* + * 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.util; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import im.vector.matrix.android.internal.legacy.rest.json.BooleanDeserializer; +import im.vector.matrix.android.internal.legacy.rest.json.ConditionDeserializer; +import im.vector.matrix.android.internal.legacy.rest.json.MatrixFieldNamingStrategy; +import im.vector.matrix.android.internal.legacy.rest.model.ContentResponse; +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.MatrixError; +import im.vector.matrix.android.internal.legacy.rest.model.PowerLevels; +import im.vector.matrix.android.internal.legacy.rest.model.RoomCreateContent; +import im.vector.matrix.android.internal.legacy.rest.model.RoomMember; +import im.vector.matrix.android.internal.legacy.rest.model.RoomPinnedEventsContent; +import im.vector.matrix.android.internal.legacy.rest.model.RoomTags; +import im.vector.matrix.android.internal.legacy.rest.model.RoomTombstoneContent; +import im.vector.matrix.android.internal.legacy.rest.model.StateEvent; +import im.vector.matrix.android.internal.legacy.rest.model.User; +import im.vector.matrix.android.internal.legacy.rest.model.bingrules.Condition; +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.OlmEventContent; +import im.vector.matrix.android.internal.legacy.rest.model.crypto.OlmPayloadContent; +import im.vector.matrix.android.internal.legacy.rest.model.crypto.RoomKeyContent; +import im.vector.matrix.android.internal.legacy.rest.model.crypto.RoomKeyRequest; +import im.vector.matrix.android.internal.legacy.rest.model.login.RegistrationFlowResponse; +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.LocationMessage; +import im.vector.matrix.android.internal.legacy.rest.model.message.Message; +import im.vector.matrix.android.internal.legacy.rest.model.message.StickerJsonMessage; +import im.vector.matrix.android.internal.legacy.rest.model.message.StickerMessage; +import im.vector.matrix.android.internal.legacy.rest.model.message.VideoMessage; +import im.vector.matrix.android.internal.legacy.rest.model.pid.RoomThirdPartyInvite; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; +import java.util.Map; +import java.util.TreeSet; + +/** + * Static methods for converting json into objects. + */ +public class JsonUtils { + private static final String LOG_TAG = JsonUtils.class.getSimpleName(); + + private static final Gson gson = new GsonBuilder() + .setFieldNamingStrategy(new MatrixFieldNamingStrategy()) + .excludeFieldsWithModifiers(Modifier.PRIVATE, Modifier.STATIC) + .registerTypeAdapter(Condition.class, new ConditionDeserializer()) + .registerTypeAdapter(boolean.class, new BooleanDeserializer(false)) + .registerTypeAdapter(Boolean.class, new BooleanDeserializer(true)) + .create(); + + // add a call to serializeNulls(). + // by default the null parameters are not sent in the requests. + // serializeNulls forces to add them. + private static final Gson gsonWithNullSerialization = new GsonBuilder() + .setFieldNamingStrategy(new MatrixFieldNamingStrategy()) + .excludeFieldsWithModifiers(Modifier.PRIVATE, Modifier.STATIC) + .serializeNulls() + .registerTypeAdapter(Condition.class, new ConditionDeserializer()) + .registerTypeAdapter(boolean.class, new BooleanDeserializer(false)) + .registerTypeAdapter(Boolean.class, new BooleanDeserializer(true)) + .create(); + + // for crypto (canonicalize) + // avoid converting "=" to \u003d + private static final Gson gsonWithoutHtmlEscaping = new GsonBuilder() + .setFieldNamingStrategy(new MatrixFieldNamingStrategy()) + .disableHtmlEscaping() + .excludeFieldsWithModifiers(Modifier.PRIVATE, Modifier.STATIC) + .registerTypeAdapter(Condition.class, new ConditionDeserializer()) + .registerTypeAdapter(boolean.class, new BooleanDeserializer(false)) + .registerTypeAdapter(Boolean.class, new BooleanDeserializer(true)) + .create(); + + /** + * Provides the JSON parser. + * + * @param withNullSerialization true to serialise the null parameters + * @return the JSON parser + */ + public static Gson getGson(boolean withNullSerialization) { + return withNullSerialization ? gsonWithNullSerialization : gson; + } + + /** + * Convert a JSON object to a state event. + * The result is never null. + * + * @param jsonObject the json to convert + * @return a room state + */ + public static StateEvent toStateEvent(JsonElement jsonObject) { + return toClass(jsonObject, StateEvent.class); + } + + /** + * Convert a JSON object to an User. + * The result is never null. + * + * @param jsonObject the json to convert + * @return an user + */ + public static User toUser(JsonElement jsonObject) { + return toClass(jsonObject, User.class); + } + + /** + * Convert a JSON object to a RoomMember. + * The result is never null. + * + * @param jsonObject the json to convert + * @return a RoomMember + */ + public static RoomMember toRoomMember(JsonElement jsonObject) { + return toClass(jsonObject, RoomMember.class); + } + + /** + * Convert a JSON object to a RoomTags. + * The result is never null. + * + * @param jsonObject the json to convert + * @return a RoomTags + */ + public static RoomTags toRoomTags(JsonElement jsonObject) { + return toClass(jsonObject, RoomTags.class); + } + + /** + * Convert a JSON object to a MatrixError. + * The result is never null. + * + * @param jsonObject the json to convert + * @return a MatrixError + */ + public static MatrixError toMatrixError(JsonElement jsonObject) { + return toClass(jsonObject, MatrixError.class); + } + + /** + * Retrieves the message type from a Json object. + * + * @param jsonObject the json object + * @return the message type + */ + @Nullable + public static String getMessageMsgType(JsonElement jsonObject) { + try { + Message message = gson.fromJson(jsonObject, Message.class); + return message.msgtype; + } catch (Exception e) { + Log.e(LOG_TAG, "## getMessageMsgType failed " + e.getMessage(), e); + } + + return null; + } + + /** + * Convert a JSON object to a Message. + * The result is never null. + * + * @param jsonObject the json to convert + * @return a Message + */ + @NonNull + public static Message toMessage(JsonElement jsonObject) { + try { + Message message = gson.fromJson(jsonObject, Message.class); + + // Try to return the right subclass + if (Message.MSGTYPE_IMAGE.equals(message.msgtype)) { + return toImageMessage(jsonObject); + } + + if (Message.MSGTYPE_VIDEO.equals(message.msgtype)) { + return toVideoMessage(jsonObject); + } + + if (Message.MSGTYPE_LOCATION.equals(message.msgtype)) { + return toLocationMessage(jsonObject); + } + + // Try to return the right subclass + if (Message.MSGTYPE_FILE.equals(message.msgtype)) { + return toFileMessage(jsonObject); + } + + if (Message.MSGTYPE_AUDIO.equals(message.msgtype)) { + return toAudioMessage(jsonObject); + } + + // Fall back to the generic Message type + return message; + } catch (Exception e) { + Log.e(LOG_TAG, "## toMessage failed " + e.getMessage(), e); + } + + return new Message(); + } + + /** + * Convert a JSON object to an Event. + * The result is never null. + * + * @param jsonObject the json to convert + * @return an Event + */ + public static Event toEvent(JsonElement jsonObject) { + return toClass(jsonObject, Event.class); + } + + /** + * Convert a JSON object to an EncryptedEventContent. + * The result is never null. + * + * @param jsonObject the json to convert + * @return an EncryptedEventContent + */ + public static EncryptedEventContent toEncryptedEventContent(JsonElement jsonObject) { + return toClass(jsonObject, EncryptedEventContent.class); + } + + /** + * Convert a JSON object to an OlmEventContent. + * The result is never null. + * + * @param jsonObject the json to convert + * @return an OlmEventContent + */ + public static OlmEventContent toOlmEventContent(JsonElement jsonObject) { + return toClass(jsonObject, OlmEventContent.class); + } + + /** + * Convert a JSON object to an OlmPayloadContent. + * The result is never null. + * + * @param jsonObject the json to convert + * @return an OlmPayloadContent + */ + public static OlmPayloadContent toOlmPayloadContent(JsonElement jsonObject) { + return toClass(jsonObject, OlmPayloadContent.class); + } + + /** + * Convert a JSON object to an EventContent. + * The result is never null. + * + * @param jsonObject the json to convert + * @return an EventContent + */ + public static EventContent toEventContent(JsonElement jsonObject) { + return toClass(jsonObject, EventContent.class); + } + + /** + * Convert a JSON object to an RoomKeyContent. + * The result is never null. + * + * @param jsonObject the json to convert + * @return an RoomKeyContent + */ + public static RoomKeyContent toRoomKeyContent(JsonElement jsonObject) { + return toClass(jsonObject, RoomKeyContent.class); + } + + /** + * Convert a JSON object to an RoomKeyRequest. + * The result is never null. + * + * @param jsonObject the json to convert + * @return an RoomKeyRequest + */ + public static RoomKeyRequest toRoomKeyRequest(JsonElement jsonObject) { + return toClass(jsonObject, RoomKeyRequest.class); + } + + /** + * Convert a JSON object to an ForwardedRoomKeyContent. + * The result is never null. + * + * @param jsonObject the json to convert + * @return an ForwardedRoomKeyContent + */ + public static ForwardedRoomKeyContent toForwardedRoomKeyContent(JsonElement jsonObject) { + return toClass(jsonObject, ForwardedRoomKeyContent.class); + } + + /** + * Convert a JSON object to an ImageMessage. + * The result is never null. + * + * @param jsonObject the json to convert + * @return an ImageMessage + */ + public static ImageMessage toImageMessage(JsonElement jsonObject) { + return toClass(jsonObject, ImageMessage.class); + } + + /** + * Convert a JSON object to a StickerMessage. + * The result is never null. + * + * @param jsonObject the json to convert + * @return a StickerMessage + */ + public static StickerMessage toStickerMessage(JsonElement jsonObject) { + final StickerJsonMessage stickerJsonMessage = toClass(jsonObject, StickerJsonMessage.class); + return new StickerMessage(stickerJsonMessage); + } + + /** + * Convert a JSON object to an FileMessage. + * The result is never null. + * + * @param jsonObject the json to convert + * @return an FileMessage + */ + public static FileMessage toFileMessage(JsonElement jsonObject) { + return toClass(jsonObject, FileMessage.class); + } + + /** + * Convert a JSON object to an AudioMessage. + * The result is never null. + * + * @param jsonObject the json to convert + * @return an AudioMessage + */ + public static AudioMessage toAudioMessage(JsonElement jsonObject) { + return toClass(jsonObject, AudioMessage.class); + } + + /** + * Convert a JSON object to a VideoMessage. + * The result is never null. + * + * @param jsonObject the json to convert + * @return a VideoMessage + */ + public static VideoMessage toVideoMessage(JsonElement jsonObject) { + return toClass(jsonObject, VideoMessage.class); + } + + /** + * Convert a JSON object to a LocationMessage. + * The result is never null. + * + * @param jsonObject the json to convert + * @return a LocationMessage + */ + public static LocationMessage toLocationMessage(JsonElement jsonObject) { + return toClass(jsonObject, LocationMessage.class); + } + + /** + * Convert a JSON object to a ContentResponse. + * The result is never null. + * + * @param jsonString the json as string to convert + * @return a ContentResponse + */ + public static ContentResponse toContentResponse(String jsonString) { + return toClass(jsonString, ContentResponse.class); + } + + /** + * Convert a JSON object to a PowerLevels. + * The result is never null. + * + * @param jsonObject the json to convert + * @return a PowerLevels + */ + public static PowerLevels toPowerLevels(JsonElement jsonObject) { + return toClass(jsonObject, PowerLevels.class); + } + + /** + * Convert a JSON object to a RoomThirdPartyInvite. + * The result is never null. + * + * @param jsonObject the json to convert + * @return a RoomThirdPartyInvite + */ + public static RoomThirdPartyInvite toRoomThirdPartyInvite(JsonElement jsonObject) { + return toClass(jsonObject, RoomThirdPartyInvite.class); + } + + /** + * Convert a stringified JSON object to a RegistrationFlowResponse. + * The result is never null. + * + * @param jsonString the json as string to convert + * @return a RegistrationFlowResponse + */ + public static RegistrationFlowResponse toRegistrationFlowResponse(String jsonString) { + return toClass(jsonString, RegistrationFlowResponse.class); + } + + /** + * Convert a JSON object to a RoomTombstoneContent. + * The result is never null. + * + * @param jsonElement the json to convert + * @return a RoomTombstoneContent + */ + public static RoomTombstoneContent toRoomTombstoneContent(final JsonElement jsonElement) { + return toClass(jsonElement, RoomTombstoneContent.class); + } + + /** + * Convert a JSON object to a RoomCreateContent. + * The result is never null. + * + * @param jsonElement the json to convert + * @return a RoomCreateContent + */ + public static RoomCreateContent toRoomCreateContent(final JsonElement jsonElement) { + return toClass(jsonElement, RoomCreateContent.class); + } + + /** + * Convert a JSON object to a RoomPinnedEventsContent. + * The result is never null. + * + * @param jsonElement the json to convert + * @return a RoomPinnedEventsContent + */ + public static RoomPinnedEventsContent toRoomPinnedEventsContent(final JsonElement jsonElement) { + return toClass(jsonElement, RoomPinnedEventsContent.class); + } + + /** + * Convert a JSON object into a class instance. + * The returned value cannot be null. + * + * @param jsonObject the json object to convert + * @param aClass the class + * @return the converted object + */ + public static T toClass(JsonElement jsonObject, Class aClass) { + T object = null; + try { + object = gson.fromJson(jsonObject, aClass); + } catch (Exception e) { + Log.e(LOG_TAG, "## toClass failed " + e.getMessage(), e); + } + if (null == object) { + try { + final Constructor constructor = aClass.getConstructor(); + object = constructor.newInstance(); + } catch (Throwable t) { + Log.e(LOG_TAG, "## toClass failed " + t.getMessage(), t); + } + } + return object; + } + + /** + * Convert a stringified JSON into a class instance. + * The returned value cannot be null. + * + * @param jsonObjectAsString the json object as string to convert + * @param aClass the class + * @return the converted object + */ + public static T toClass(String jsonObjectAsString, Class aClass) { + T object = null; + try { + object = gson.fromJson(jsonObjectAsString, aClass); + } catch (Exception e) { + Log.e(LOG_TAG, "## toClass failed " + e.getMessage(), e); + } + if (null == object) { + try { + final Constructor constructor = aClass.getConstructor(); + object = constructor.newInstance(); + } catch (Throwable t) { + Log.e(LOG_TAG, "## toClass failed " + t.getMessage(), t); + } + } + return object; + } + + /** + * Convert an Event instance to a Json object. + * + * @param event the event instance. + * @return the json object + */ + public static JsonObject toJson(Event event) { + try { + return (JsonObject) gson.toJsonTree(event); + } catch (Exception e) { + Log.e(LOG_TAG, "## toJson failed " + e.getMessage(), e); + } + + return new JsonObject(); + } + + /** + * Convert an Message instance into a Json object. + * + * @param message the Message instance. + * @return the json object + */ + public static JsonObject toJson(Message message) { + try { + return (JsonObject) gson.toJsonTree(message); + } catch (Exception e) { + Log.e(LOG_TAG, "## toJson failed " + e.getMessage(), e); + } + + return null; + } + + /** + * Create a canonicalized json string for an object + * + * @param object the object to convert + * @return the canonicalized string + */ + public static String getCanonicalizedJsonString(Object object) { + String canonicalizedJsonString = null; + + if (null != object) { + if (object instanceof JsonElement) { + canonicalizedJsonString = gsonWithoutHtmlEscaping.toJson(canonicalize((JsonElement) object)); + } else { + canonicalizedJsonString = gsonWithoutHtmlEscaping.toJson(canonicalize(gsonWithoutHtmlEscaping.toJsonTree(object))); + } + + if (null != canonicalizedJsonString) { + canonicalizedJsonString = canonicalizedJsonString.replace("\\/", "/"); + } + } + + return canonicalizedJsonString; + } + + /** + * Canonicalize a JsonElement element + * + * @param src the src + * @return the canonicalize element + */ + public static JsonElement canonicalize(JsonElement src) { + // sanity check + if (null == src) { + return null; + } + + if (src instanceof JsonArray) { + // Canonicalize each element of the array + JsonArray srcArray = (JsonArray) src; + JsonArray result = new JsonArray(); + for (int i = 0; i < srcArray.size(); i++) { + result.add(canonicalize(srcArray.get(i))); + } + return result; + } else if (src instanceof JsonObject) { + // Sort the attributes by name, and the canonicalize each element of the object + JsonObject srcObject = (JsonObject) src; + JsonObject result = new JsonObject(); + TreeSet attributes = new TreeSet<>(); + + for (Map.Entry entry : srcObject.entrySet()) { + attributes.add(entry.getKey()); + } + for (String attribute : attributes) { + result.add(attribute, canonicalize(srcObject.get(attribute))); + } + return result; + } else { + return src; + } + } + + /** + * Convert a string from an UTF8 String + * + * @param s the string to convert + * @return the utf-16 string + */ + public static String convertFromUTF8(String s) { + String out = s; + + if (null != out) { + try { + byte[] bytes = out.getBytes(); + out = new String(bytes, "UTF-8"); + } catch (Exception e) { + Log.e(LOG_TAG, "## convertFromUTF8() failed " + e.getMessage(), e); + } + } + + return out; + } + + /** + * Convert a string to an UTF8 String + * + * @param s the string to convert + * @return the utf-8 string + */ + public static String convertToUTF8(String s) { + String out = s; + + if (null != out) { + try { + byte[] bytes = out.getBytes("UTF-8"); + out = new String(bytes); + } catch (Exception e) { + Log.e(LOG_TAG, "## convertToUTF8() failed " + e.getMessage(), e); + } + } + + return out; + } + + /** + * Returns a dedicated parameter as a string + * + * @param paramName the parameter name + * @return the string value, or null if not defined or not a String + */ + @Nullable + public static String getAsString(Map map, String paramName) { + if (map.containsKey(paramName) && map.get(paramName) instanceof String) { + return (String) map.get(paramName); + } + + return null; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/Log.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/Log.java new file mode 100644 index 0000000000..7b738885c7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/Log.java @@ -0,0 +1,306 @@ +/* + * Copyright 2017 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.util; + +import android.text.TextUtils; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; +import java.util.logging.FileHandler; +import java.util.logging.Formatter; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import im.vector.matrix.android.BuildConfig; + +/** + * Intended to mimic {@link android.util.Log} in terms of interface, but with a lot of extra behind the scenes stuff. + */ +public class Log { + private static final String LOG_TAG = "Log"; + + private static final String LINE_SEPARATOR = System.getProperty("line.separator"); + private static final int LOG_SIZE_BYTES = 50 * 1024 * 1024; // 50MB + + // relatively large rotation count because closing > opening the app rotates the log (!) + private static final int LOG_ROTATION_COUNT = 15; + + private static final Logger sLogger = Logger.getLogger("org.matrix.androidsdk"); + private static FileHandler sFileHandler = null; + private static File sCacheDirectory = null; + private static String sFileName = "matrix"; + + // determine if messsages with DEBUG level should be logged or not + public static boolean sShouldLogDebug = BuildConfig.DEBUG; + + public enum EventTag { + /** + * A navigation event, e.g. onPause + */ + NAVIGATION, + /** + * A user triggered event, e.g. onClick + */ + USER, + /** + * User-visible notifications + */ + NOTICE, + /** + * A background event e.g. incoming messages + */ + BACKGROUND + } + + /** + * Initialises the logger. Should be called AFTER {@link Log#setLogDirectory(File)}. + * + * @param fileName the base file name + */ + public static void init(String fileName) { + try { + if (!TextUtils.isEmpty(fileName)) { + sFileName = fileName; + } + sFileHandler = new FileHandler(sCacheDirectory.getAbsolutePath() + "/" + sFileName + ".%g.txt", LOG_SIZE_BYTES, LOG_ROTATION_COUNT); + sFileHandler.setFormatter(new LogFormatter()); + sLogger.setUseParentHandlers(false); + sLogger.setLevel(Level.ALL); + sLogger.addHandler(sFileHandler); + } catch (IOException e) { + } + } + + /** + * Set the directory to put log files. + * + * @param cacheDir The directory, usually {@link android.content.ContextWrapper#getCacheDir()} + */ + public static void setLogDirectory(File cacheDir) { + if (!cacheDir.exists()) { + cacheDir.mkdirs(); + } + sCacheDirectory = cacheDir; + } + + /** + * Set the directory to put log files. + * + * @return the cache directory + */ + public static File getLogDirectory() { + return sCacheDirectory; + } + + /** + * Adds our own log files to the provided list of files. + * + * @param files The list of files to add to. + * @return The same list with more files added. + */ + public static List addLogFiles(List files) { + try { + // reported by GA + if (null != sFileHandler) { + sFileHandler.flush(); + String absPath = sCacheDirectory.getAbsolutePath(); + + for (int i = 0; i <= LOG_ROTATION_COUNT; i++) { + String filepath = absPath + "/" + sFileName + "." + i + ".txt"; + File file = new File(filepath); + if (file.exists()) { + files.add(file); + } + } + } + } catch (Exception e) { + Log.e(LOG_TAG, "## addLogFiles() failed : " + e.getMessage(), e); + } + return files; + } + + private static void logToFile(String level, String tag, String content) { + if (null == sCacheDirectory) { + return; + } + + StringBuilder b = new StringBuilder(); + b.append(Thread.currentThread().getId()); + b.append(" "); + b.append(level); + b.append("/"); + b.append(tag); + b.append(": "); + b.append(content); + sLogger.info(b.toString()); + } + + /** + * Log an Throwable + * + * @param throwable the throwable to log + */ + private static void logToFile(Throwable throwable) { + if (null == sCacheDirectory || throwable == null) { + return; + } + + + StringWriter errors = new StringWriter(); + throwable.printStackTrace(new PrintWriter(errors)); + + sLogger.info(errors.toString()); + } + + /** + * Log events which can be automatically analysed + * + * @param tag the EventTag + * @param content Content to log + */ + public static void event(EventTag tag, String content) { + android.util.Log.v(tag.name(), content); + logToFile("EVENT", tag.name(), content); + } + + /** + * Log connection information, such as urls hit, incoming data, current connection status. + * + * @param tag Log tag + * @param content Content to log + */ + public static void con(String tag, String content) { + android.util.Log.v(tag, content); + logToFile("CON", tag, content); + } + + public static void v(String tag, String content) { + android.util.Log.v(tag, content); + logToFile("V", tag, content); + } + + public static void v(String tag, String content, Throwable throwable) { + android.util.Log.v(tag, content, throwable); + logToFile("V", tag, content); + logToFile(throwable); + } + + public static void d(String tag, String content) { + if (sShouldLogDebug) { + android.util.Log.d(tag, content); + logToFile("D", tag, content); + } + } + + public static void d(String tag, String content, Throwable throwable) { + if (sShouldLogDebug) { + android.util.Log.d(tag, content, throwable); + logToFile("D", tag, content); + logToFile(throwable); + } + } + + public static void i(String tag, String content) { + android.util.Log.i(tag, content); + logToFile("I", tag, content); + } + + public static void i(String tag, String content, Throwable throwable) { + android.util.Log.i(tag, content, throwable); + logToFile("I", tag, content); + logToFile(throwable); + } + + public static void w(String tag, String content) { + android.util.Log.w(tag, content); + logToFile("W", tag, content); + } + + public static void w(String tag, String content, Throwable throwable) { + android.util.Log.w(tag, content, throwable); + logToFile("W", tag, content); + logToFile(throwable); + } + + public static void e(String tag, String content) { + android.util.Log.e(tag, content); + logToFile("E", tag, content); + } + + public static void e(String tag, String content, Throwable throwable) { + android.util.Log.e(tag, content, throwable); + logToFile("E", tag, content); + logToFile(throwable); + } + + public static void wtf(String tag, String content) { + android.util.Log.wtf(tag, content); + logToFile("WTF", tag, content); + } + + public static void wtf(String tag, Throwable throwable) { + android.util.Log.wtf(tag, throwable); + logToFile("WTF", tag, throwable.getMessage()); + logToFile(throwable); + } + + public static void wtf(String tag, String content, Throwable throwable) { + android.util.Log.wtf(tag, content, throwable); + logToFile("WTF", tag, content); + logToFile(throwable); + } + + public static final class LogFormatter extends Formatter { + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US); + private static boolean mIsTimeZoneSet = false; + + @Override + public String format(LogRecord r) { + if (!mIsTimeZoneSet) { + DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); + mIsTimeZoneSet = true; + } + + Throwable thrown = r.getThrown(); + if (thrown != null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + sw.write(r.getMessage()); + sw.write(LINE_SEPARATOR); + thrown.printStackTrace(pw); + pw.flush(); + return sw.toString(); + } else { + StringBuilder b = new StringBuilder(); + String date = DATE_FORMAT.format(new Date(r.getMillis())); + b.append(date); + b.append("Z "); + b.append(r.getMessage()); + b.append(LINE_SEPARATOR); + return b.toString(); + } + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/MXOsHandler.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/MXOsHandler.java new file mode 100644 index 0000000000..bfa101b4f2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/MXOsHandler.java @@ -0,0 +1,64 @@ +/* + * 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.util; + +import android.os.Handler; +import android.os.Looper; + +/** + * Override the os handler to support unitary tests + */ +public class MXOsHandler { + + // the internal handler + private final android.os.Handler mHandler; + + /** + * Listener + */ + public interface IPostListener { + // a post has been done in the thread + void onPost(Looper looper); + } + + // static + public static final IPostListener mPostListener = null; + + /** + * Constructor + * + * @param looper the looper + */ + public MXOsHandler(Looper looper) { + mHandler = new Handler(looper); + } + + /** + * Post a runnable + * + * @param r the runnable + * @return true if the runnable is placed + */ + public boolean post(Runnable r) { + boolean result = mHandler.post(r); + + if (result && (null != mPostListener)) { + mPostListener.onPost(mHandler.getLooper()); + } + + return result; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/PermalinkUtils.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/PermalinkUtils.java new file mode 100644 index 0000000000..5fa63c7512 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/PermalinkUtils.java @@ -0,0 +1,103 @@ +/* + * 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.util; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import im.vector.matrix.android.internal.legacy.rest.model.Event; + +/** + * Useful methods to deals with Matrix permalink + */ +public class PermalinkUtils { + + private static final String MATRIX_TO_URL_BASE = "https://matrix.to/#/"; + + /** + * Creates a permalink for an event. + * Ex: "https://matrix.to/#/!nbzmcXAqpxBXjAdgoX:matrix.org/$1531497316352799BevdV:matrix.org" + * + * @param event the event + * @return the permalink, or null in case of error + */ + @Nullable + public static String createPermalink(Event event) { + if (event == null) { + return null; + } + + return createPermalink(event.roomId, event.eventId); + } + + /** + * Creates a permalink for an id (can be a user Id, Room Id, etc.). + * Ex: "https://matrix.to/#/@benoit:matrix.org" + * + * @param id the id + * @return the permalink, or null in case of error + */ + @Nullable + public static String createPermalink(String id) { + if (TextUtils.isEmpty(id)) { + return null; + } + + return MATRIX_TO_URL_BASE + escape(id); + } + + /** + * Creates a permalink for an event. If you have an event you can use {@link #createPermalink(Event)} + * Ex: "https://matrix.to/#/!nbzmcXAqpxBXjAdgoX:matrix.org/$1531497316352799BevdV:matrix.org" + * + * @param roomId the id of the room + * @param eventId the id of the event + * @return the permalink + */ + @NonNull + public static String createPermalink(@NonNull String roomId, @NonNull String eventId) { + return MATRIX_TO_URL_BASE + escape(roomId) + "/" + escape(eventId); + } + + /** + * Extract the linked id from the universal link + * + * @param url the universal link, Ex: "https://matrix.to/#/@benoit:matrix.org" + * @return the id from the url, ex: "@benoit:matrix.org", or null if the url is not a permalink + */ + public static String getLinkedId(@Nullable String url) { + boolean isSupported = url != null && url.startsWith(MATRIX_TO_URL_BASE); + + if (isSupported) { + return url.substring(MATRIX_TO_URL_BASE.length()); + } + + return null; + } + + + /** + * Escape '/' in id, because it is used as a separator + * + * @param id the id to escape + * @return the escaped id + */ + private static String escape(String id) { + return id.replaceAll("/", "%2F"); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/PolymorphicRequestBodyConverter.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/PolymorphicRequestBodyConverter.java new file mode 100644 index 0000000000..a26c0156e3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/PolymorphicRequestBodyConverter.java @@ -0,0 +1,61 @@ +package im.vector.matrix.android.internal.legacy.util; + + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.LinkedHashMap; +import java.util.Map; + +import okhttp3.RequestBody; +import retrofit2.Converter; +import retrofit2.Retrofit; + +public final class PolymorphicRequestBodyConverter implements Converter { + public static final Factory FACTORY = new Factory() { + @Override public Converter requestBodyConverter( + Type type, + Annotation[] parameterAnnotations, + Annotation[] methodAnnotations, + Retrofit retrofit + ) { + return new PolymorphicRequestBodyConverter<>( + this, parameterAnnotations, methodAnnotations, retrofit + ); + } + }; + + private final Factory skipPast; + private final Annotation[] parameterAnnotations; + private final Annotation[] methodsAnnotations; + private final Retrofit retrofit; + private final Map, Converter> cache = new LinkedHashMap<>(); + + PolymorphicRequestBodyConverter( + Factory skipPast, + Annotation[] parameterAnnotations, + Annotation[] methodsAnnotations, + Retrofit retrofit) { + this.skipPast = skipPast; + this.parameterAnnotations = parameterAnnotations; + this.methodsAnnotations = methodsAnnotations; + this.retrofit = retrofit; + } + + @Override public RequestBody convert(T value) throws IOException { + Class cls = value.getClass(); + Converter requestBodyConverter; + synchronized (cache) { + requestBodyConverter = cache.get(cls); + } + if (requestBodyConverter == null) { + requestBodyConverter = retrofit.nextRequestBodyConverter( + skipPast, cls, parameterAnnotations, methodsAnnotations + ); + synchronized (cache) { + cache.put(cls, requestBodyConverter); + } + } + return requestBodyConverter.convert(value); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/ResourceUtils.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/ResourceUtils.java new file mode 100755 index 0000000000..11f8049f10 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/ResourceUtils.java @@ -0,0 +1,192 @@ +/* + * 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.util; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.text.TextUtils; +import android.webkit.MimeTypeMap; + +import java.io.InputStream; + +/** + * Static resource utility methods. + */ +public class ResourceUtils { + private static final String LOG_TAG = ResourceUtils.class.getSimpleName(); + + /** + * Mime types + **/ + public static final String MIME_TYPE_JPEG = "image/jpeg"; + public static final String MIME_TYPE_JPG = "image/jpg"; + public static final String MIME_TYPE_IMAGE_ALL = "image/*"; + public static final String MIME_TYPE_ALL_CONTENT = "*/*"; + + + public static class Resource { + public InputStream mContentStream; + public String mMimeType; + + public Resource(InputStream contentStream, String mimeType) { + mContentStream = contentStream; + mMimeType = mimeType; + } + + /** + * Close the content stream. + */ + public void close() { + try { + mMimeType = null; + + if (null != mContentStream) { + mContentStream.close(); + mContentStream = null; + } + } catch (Exception e) { + Log.e(LOG_TAG, "Resource.close failed " + e.getLocalizedMessage(), e); + } + } + + /** + * Tells if the opened resource is a jpeg one. + * + * @return true if the opened resource is a jpeg one. + */ + public boolean isJpegResource() { + return MIME_TYPE_JPEG.equals(mMimeType) || MIME_TYPE_JPG.equals(mMimeType); + } + } + + /** + * Get a resource stream and metadata about it given its URI returned from onActivityResult. + * + * @param context the context. + * @param uri the URI + * @param mimetype the mimetype + * @return a {@link Resource} encapsulating the opened resource stream and associated metadata + * or {@code null} if opening the resource stream failed. + */ + public static Resource openResource(Context context, Uri uri, String mimetype) { + try { + // if the mime type is not provided, try to find it out + if (TextUtils.isEmpty(mimetype)) { + mimetype = context.getContentResolver().getType(uri); + + // try to find the mimetype from the filename + if (null == mimetype) { + String extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString().toLowerCase()); + if (extension != null) { + mimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } + } + } + + return new Resource( + context.getContentResolver().openInputStream(uri), + mimetype); + + } catch (Exception e) { + Log.e(LOG_TAG, "Failed to open resource input stream", e); + } + + return null; + } + + /** + * Creates a thumbnail bitmap from a media Uri + * + * @param context the context + * @param mediaUri the media Uri + * @param maxThumbWidth max thumbnail width + * @param maxThumbHeight max thumbnail height + * @return the bitmap. + */ + public static Bitmap createThumbnailBitmap(Context context, Uri mediaUri, int maxThumbWidth, int maxThumbHeight) { + Bitmap thumbnailBitmap = null; + ResourceUtils.Resource resource = ResourceUtils.openResource(context, mediaUri, null); + + // check if the resource can be i + if (null == resource) { + return null; + } + + try { + // need to decompress the high res image + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + resource = ResourceUtils.openResource(context, mediaUri, null); + + // get the full size bitmap + Bitmap fullSizeBitmap = null; + + if (null != resource) { + try { + fullSizeBitmap = BitmapFactory.decodeStream(resource.mContentStream, null, options); + } catch (Exception e) { + Log.e(LOG_TAG, "BitmapFactory.decodeStream fails " + e.getLocalizedMessage(), e); + } + } + + // succeeds to retrieve the full size bitmap + if (null != fullSizeBitmap) { + + // the bitmap is smaller that max sizes + if ((fullSizeBitmap.getHeight() < maxThumbHeight) && (fullSizeBitmap.getWidth() < maxThumbWidth)) { + thumbnailBitmap = fullSizeBitmap; + } else { + double thumbnailWidth = maxThumbWidth; + double thumbnailHeight = maxThumbHeight; + + double imageWidth = fullSizeBitmap.getWidth(); + double imageHeight = fullSizeBitmap.getHeight(); + + if (imageWidth > imageHeight) { + thumbnailHeight = thumbnailWidth * imageHeight / imageWidth; + } else { + thumbnailWidth = thumbnailHeight * imageWidth / imageHeight; + } + + try { + thumbnailBitmap = Bitmap.createScaledBitmap((null == fullSizeBitmap) ? thumbnailBitmap : fullSizeBitmap, + (int) thumbnailWidth, (int) thumbnailHeight, false); + } catch (OutOfMemoryError ex) { + Log.e(LOG_TAG, "createThumbnailBitmap " + ex.getMessage(), ex); + } + } + + // reduce the memory consumption + if (null != fullSizeBitmap) { + fullSizeBitmap.recycle(); + System.gc(); + } + } + + if (null != resource) { + resource.mContentStream.close(); + } + + } catch (Exception e) { + Log.e(LOG_TAG, "createThumbnailBitmap fails " + e.getLocalizedMessage()); + } + + return thumbnailBitmap; + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/SecretKeyAndVersion.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/SecretKeyAndVersion.java new file mode 100644 index 0000000000..1258ba88f9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/SecretKeyAndVersion.java @@ -0,0 +1,57 @@ +/* + * 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.util; + +import javax.crypto.SecretKey; + +/** + * Tuple which contains the secret key and the version of Android when the key has been generated + */ +public class SecretKeyAndVersion { + // The key + private final SecretKey secretKey; + + // the android version when the key has been generated + private final int androidVersionWhenTheKeyHasBeenGenerated; + + /** + * @param secretKey the key + * @param androidVersionWhenTheKeyHasBeenGenerated the android version when the key has been generated + */ + public SecretKeyAndVersion(SecretKey secretKey, int androidVersionWhenTheKeyHasBeenGenerated) { + this.secretKey = secretKey; + this.androidVersionWhenTheKeyHasBeenGenerated = androidVersionWhenTheKeyHasBeenGenerated; + } + + /** + * Get the key + * + * @return the key + */ + public SecretKey getSecretKey() { + return secretKey; + } + + /** + * Get the android version when the key has been generated + * + * @return the android version when the key has been generated + */ + public int getAndroidVersionWhenTheKeyHasBeenGenerated() { + return androidVersionWhenTheKeyHasBeenGenerated; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/UnsentEventsManager.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/UnsentEventsManager.java new file mode 100644 index 0000000000..7a2229b257 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/UnsentEventsManager.java @@ -0,0 +1,577 @@ +/* + * 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.util; + +import android.content.Context; +import android.text.TextUtils; + +import im.vector.matrix.android.internal.legacy.MXDataHandler; +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.callback.ApiCallback; +import im.vector.matrix.android.internal.legacy.rest.callback.RestAdapterCallback; +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.ssl.UnrecognizedCertificateException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Timer; +import java.util.TimerTask; + +import retrofit2.Response; + +/** + * unsent matrix events manager + * This manager schedules the unsent events sending. + * 1 - it keeps the unsent events order (i.e. wait that the first event is resent before sending the second one) + * 2 - Apply the retry rules (event time life, 3 tries...) + */ +public class UnsentEventsManager { + + private static final String LOG_TAG = UnsentEventsManager.class.getSimpleName(); + + // 3 minutes + private static final int MAX_MESSAGE_LIFETIME_MS = 180000; + + // perform only MAX_RETRIES retries + private static final int MAX_RETRIES = 4; + + // The jitter value to apply to compute a random retry time. + private static final int RETRY_JITTER_MS = 3000; + + // the network receiver + private final NetworkConnectivityReceiver mNetworkConnectivityReceiver; + // faster way to check if the event is already sent + private final Map mUnsentEventsMap = new HashMap<>(); + // get the sending order + private final List mUnsentEvents = new ArrayList<>(); + // true of the device is connected to a data network + private boolean mbIsConnected = false; + + // matrix error management + private final MXDataHandler mDataHandler; + + /** + * storage class + */ + private class UnsentEventSnapshot { + // first time the message has been sent + // -1 to ignore age test + private long mAge; + // the number of retries + // it should be limited + private int mRetryCount; + // retry callback. + private RestAdapterCallback.RequestRetryCallBack mRequestRetryCallBack; + // retry timer + private Timer mAutoResendTimer = null; + + public Timer mLifeTimeTimer = null; + // the retry is in progress + public boolean mIsResending = false; + // human description of the event + // The snapshot creator can hide some fields + public String mEventDescription = null; + + /** + * + */ + public boolean waitToBeResent() { + return (null != mAutoResendTimer); + } + + /** + * Resend the event after a delay. + * + * @param delayMs the delay in milliseconds. + * @return true if the operation succeeds + */ + public boolean resendEventAfter(int delayMs) { + stopTimer(); + + try { + if (null != mEventDescription) { + Log.d(LOG_TAG, "Resend after " + delayMs + " [" + mEventDescription + "]"); + } + + mAutoResendTimer = new Timer(); + mAutoResendTimer.schedule(new TimerTask() { + @Override + public void run() { + try { + mIsResending = true; + + if (null != mEventDescription) { + Log.d(LOG_TAG, "Resend [" + mEventDescription + "]"); + } + + mRequestRetryCallBack.onRetry(); + } catch (Throwable throwable) { + mIsResending = false; + Log.e(LOG_TAG, "## resendEventAfter() : " + mEventDescription + " + onRetry failed " + throwable.getMessage(), throwable); + } + } + }, delayMs); + return true; + + } catch (Throwable t) { + Log.e(LOG_TAG, "## resendEventAfter failed " + t.getMessage(), t); + } + + return false; + } + + /** + * Stop any pending resending timer. + */ + public void stopTimer() { + if (null != mAutoResendTimer) { + mAutoResendTimer.cancel(); + mAutoResendTimer = null; + } + } + + /** + * Stop timers. + */ + public void stopTimers() { + if (null != mAutoResendTimer) { + mAutoResendTimer.cancel(); + mAutoResendTimer = null; + } + + if (null != mLifeTimeTimer) { + mLifeTimeTimer.cancel(); + mLifeTimeTimer = null; + } + } + } + + /** + * Constructor + * + * @param networkConnectivityReceiver the network received + * @param dataHandler the data handler + */ + public UnsentEventsManager(NetworkConnectivityReceiver networkConnectivityReceiver, MXDataHandler dataHandler) { + mNetworkConnectivityReceiver = networkConnectivityReceiver; + + // add a default listener + // to resend the unsent messages + mNetworkConnectivityReceiver.addEventListener(new IMXNetworkEventListener() { + @Override + public void onNetworkConnectionUpdate(boolean isConnected) { + mbIsConnected = isConnected; + + if (isConnected) { + resentUnsents(); + } + } + }); + + mbIsConnected = mNetworkConnectivityReceiver.isConnected(); + + mDataHandler = dataHandler; + } + + /** + * Warn that the apiCallback has been called + * + * @param apiCallback the called apiCallback + */ + public void onEventSent(ApiCallback apiCallback) { + if (null != apiCallback) { + UnsentEventSnapshot snapshot = null; + + synchronized (mUnsentEventsMap) { + if (mUnsentEventsMap.containsKey(apiCallback)) { + snapshot = mUnsentEventsMap.get(apiCallback); + } + } + + if (null != snapshot) { + if (null != snapshot.mEventDescription) { + Log.d(LOG_TAG, "Resend Succeeded [" + snapshot.mEventDescription + "]"); + } + + snapshot.stopTimers(); + + synchronized (mUnsentEventsMap) { + mUnsentEventsMap.remove(apiCallback); + mUnsentEvents.remove(snapshot); + } + + resentUnsents(); + } + } + } + + /** + * Clear the session data + */ + public void clear() { + synchronized (mUnsentEventsMap) { + for (UnsentEventSnapshot snapshot : mUnsentEvents) { + snapshot.stopTimers(); + } + + mUnsentEvents.clear(); + mUnsentEventsMap.clear(); + } + } + + /** + * @return the network connectivity receiver + */ + public NetworkConnectivityReceiver getNetworkConnectivityReceiver() { + return mNetworkConnectivityReceiver; + } + + /** + * @return the context + */ + public Context getContext() { + return mDataHandler.getStore().getContext(); + } + + /** + * The event failed to be sent and cannot be resent. + * It triggers the error callbacks. + * + * @param eventDescription the event description + * @param exception the exception + * @param callback the callback. + */ + private static void triggerErrorCallback(MXDataHandler dataHandler, String eventDescription, Response response, Exception exception, ApiCallback callback) { + if ((null != exception) && !TextUtils.isEmpty(exception.getMessage())) { + // privacy + //Log.e(LOG_TAG, error.getMessage() + " url=" + error.getUrl()); + Log.e(LOG_TAG, exception.getLocalizedMessage(), exception); + } + + if (null == exception) { + try { + if (null != eventDescription) { + Log.e(LOG_TAG, "Unexpected Error " + eventDescription); + } + if (null != callback) { + callback.onUnexpectedError(null); + } + } catch (Exception e) { + // privacy + //Log.e(LOG_TAG, "Exception UnexpectedError " + e.getMessage() + " while managing " + error.getUrl()); + Log.e(LOG_TAG, "Exception UnexpectedError " + e.getMessage(), e); + } + } else if (exception instanceof IOException) { + try { + if (null != eventDescription) { + Log.e(LOG_TAG, "Network Error " + eventDescription); + } + if (null != callback) { + callback.onNetworkError(exception); + } + } catch (Exception e) { + // privacy + //Log.e(LOG_TAG, "Exception NetworkError " + e.getMessage() + " while managing " + error.getUrl()); + Log.e(LOG_TAG, "Exception NetworkError " + e.getMessage(), e); + } + } else { + // Try to convert this into a Matrix error + MatrixError mxError; + try { + mxError = JsonUtils.getGson(false).fromJson(response.errorBody().string(), MatrixError.class); + } catch (Exception e) { + mxError = null; + } + if (mxError != null) { + try { + if (null != eventDescription) { + Log.e(LOG_TAG, "Matrix Error " + mxError + " " + eventDescription); + } + + if (MatrixError.isConfigurationErrorCode(mxError.errcode)) { + dataHandler.onConfigurationError(mxError.errcode); + } else if (null != callback) { + callback.onMatrixError(mxError); + } + + } catch (Exception e) { + // privacy + //Log.e(LOG_TAG, "Exception MatrixError " + e.getMessage() + " while managing " + error.getUrl()); + Log.e(LOG_TAG, "Exception MatrixError " + e.getLocalizedMessage(), e); + } + } else { + try { + if (null != eventDescription) { + Log.e(LOG_TAG, "Unexpected Error " + eventDescription); + } + + if (null != callback) { + callback.onUnexpectedError(exception); + } + } catch (Exception e) { + // privacy + //Log.e(LOG_TAG, "Exception UnexpectedError " + e.getMessage() + " while managing " + error.getUrl()); + Log.e(LOG_TAG, "Exception UnexpectedError " + e.getLocalizedMessage(), e); + } + } + } + } + + /** + * A request fails with an unknown matrix token error code. + * + * @param matrixErrorCode the matrix error code + * @param eventDescription the event description + */ + public void onConfigurationErrorCode(final String matrixErrorCode, final String eventDescription) { + Log.e(LOG_TAG, eventDescription + " failed because of an unknown matrix token"); + mDataHandler.onConfigurationError(matrixErrorCode); + } + + /** + * warns that an event failed to be sent. + * + * @param eventDescription the event description + * @param ignoreEventTimeLifeInOffline tell if the event timelife is ignored in offline mode + * @param response Retrofit response + * @param exception Retrofit Exception + * @param apiCallback the apiCallback. + * @param requestRetryCallBack requestRetryCallBack. + */ + public void onEventSendingFailed(final String eventDescription, + final boolean ignoreEventTimeLifeInOffline, + final Response response, + final Exception exception, + final ApiCallback apiCallback, + final RestAdapterCallback.RequestRetryCallBack requestRetryCallBack) { + boolean isManaged = false; + + if (null != eventDescription) { + Log.d(LOG_TAG, "Fail to send [" + eventDescription + "]"); + } + + if ((null != requestRetryCallBack) && (null != apiCallback)) { + synchronized (mUnsentEventsMap) { + UnsentEventSnapshot snapshot; + + // Try to convert this into a Matrix error + MatrixError mxError = null; + + if (null != response) { + try { + mxError = JsonUtils.getGson(false).fromJson(response.errorBody().string(), MatrixError.class); + } catch (Exception e) { + mxError = null; + } + } + + // trace the matrix error. + if ((null != eventDescription) && (null != mxError)) { + Log.d(LOG_TAG, "Matrix error " + mxError.errcode + " " + mxError.getMessage() + " [" + eventDescription + "]"); + + if (MatrixError.isConfigurationErrorCode(mxError.errcode)) { + Log.e(LOG_TAG, "## onEventSendingFailed() : invalid token detected"); + mDataHandler.onConfigurationError(mxError.errcode); + triggerErrorCallback(mDataHandler, eventDescription, response, exception, apiCallback); + return; + } + } + + int matrixRetryTimeout = -1; + + if ((null != mxError) && MatrixError.LIMIT_EXCEEDED.equals(mxError.errcode) && (null != mxError.retry_after_ms)) { + matrixRetryTimeout = mxError.retry_after_ms + 200; + } + + if (null != exception) { + UnrecognizedCertificateException unrecCertEx = CertUtil.getCertificateException(exception); + + if (null != unrecCertEx) { + Log.e(LOG_TAG, "## onEventSendingFailed() : SSL issue detected"); + mDataHandler.onSSLCertificateError(unrecCertEx); + triggerErrorCallback(mDataHandler, eventDescription, response, exception, apiCallback); + return; + } + } + + // some matrix errors are not trapped. + if ((null == mxError) || !mxError.isSupportedErrorCode() || MatrixError.LIMIT_EXCEEDED.equals(mxError.errcode)) { + // is it the first time that the event has been sent ? + if (mUnsentEventsMap.containsKey(apiCallback)) { + snapshot = mUnsentEventsMap.get(apiCallback); + + snapshot.mIsResending = false; + snapshot.stopTimer(); + + // assume that LIMIT_EXCEEDED error is not a default retry + if (matrixRetryTimeout < 0) { + snapshot.mRetryCount++; + } + + // any event has a time life to avoid very old messages + long timeLife = 0; + + // age < 0 means that the event time life is ignored + if (snapshot.mAge > 0) { + timeLife = System.currentTimeMillis() - snapshot.mAge; + } + + if ((timeLife > MAX_MESSAGE_LIFETIME_MS) || (snapshot.mRetryCount > MAX_RETRIES)) { + snapshot.stopTimers(); + mUnsentEventsMap.remove(apiCallback); + mUnsentEvents.remove(snapshot); + + if (null != eventDescription) { + Log.d(LOG_TAG, "Cancel [" + eventDescription + "]"); + } + + isManaged = false; + } else { + isManaged = true; + } + } else { + snapshot = new UnsentEventSnapshot(); + + try { + snapshot.mAge = ignoreEventTimeLifeInOffline ? -1 : System.currentTimeMillis(); + snapshot.mRequestRetryCallBack = requestRetryCallBack; + snapshot.mRetryCount = 1; + snapshot.mEventDescription = eventDescription; + mUnsentEventsMap.put(apiCallback, snapshot); + mUnsentEvents.add(snapshot); + + if (mbIsConnected || !ignoreEventTimeLifeInOffline) { + // the event has a life time + final UnsentEventSnapshot fSnapshot = snapshot; + fSnapshot.mLifeTimeTimer = new Timer(); + fSnapshot.mLifeTimeTimer.schedule(new TimerTask() { + @Override + public void run() { + try { + + if (null != eventDescription) { + Log.d(LOG_TAG, "Cancel to send [" + eventDescription + "]"); + } + + fSnapshot.stopTimers(); + synchronized (mUnsentEventsMap) { + mUnsentEventsMap.remove(apiCallback); + mUnsentEvents.remove(fSnapshot); + } + + triggerErrorCallback(mDataHandler, eventDescription, response, exception, apiCallback); + } catch (Exception e) { + Log.e(LOG_TAG, "## onEventSendingFailed() : failure Msg=" + e.getMessage(), e); + } + } + }, MAX_MESSAGE_LIFETIME_MS); + } else if (ignoreEventTimeLifeInOffline) { + Log.d(LOG_TAG, "The request " + eventDescription + " will be sent when a network will be available"); + } + } catch (Throwable throwable) { + Log.e(LOG_TAG, "## snapshot creation failed " + throwable.getMessage(), throwable); + + if (null != snapshot.mLifeTimeTimer) { + snapshot.mLifeTimeTimer.cancel(); + } + + mUnsentEventsMap.remove(apiCallback); + mUnsentEvents.remove(snapshot); + + try { + triggerErrorCallback(mDataHandler, eventDescription, response, exception, apiCallback); + } catch (Exception e) { + Log.e(LOG_TAG, "## onEventSendingFailed() : failure Msg=" + e.getMessage(), e); + } + } + + isManaged = true; + } + + // retry to send the message ? + if (isManaged) { + // resend the event only if there is an available network + // retrofitError.isNetworkError() does not provide a valid description of the failure + // 1- there is no available network / the connection is lost. (what we could expect) + // 2- the server did not response after 15s : the client would wrongly behave, it would wait until to switch to a valid network + // It never happens, so the message is never resent. + // + if (mbIsConnected) { + int jitterTime = ((int) Math.pow(2, snapshot.mRetryCount)) + + (Math.abs(new Random(System.currentTimeMillis()).nextInt()) % RETRY_JITTER_MS); + isManaged = snapshot.resendEventAfter((matrixRetryTimeout > 0) ? matrixRetryTimeout : jitterTime); + } + } + } + } + } + + if (!isManaged) { + Log.d(LOG_TAG, "Cannot resend it"); + triggerErrorCallback(mDataHandler, eventDescription, response, exception, apiCallback); + } + } + + /** + * check if some messages must be resent + */ + private void resentUnsents() { + Log.d(LOG_TAG, "resentUnsents"); + + synchronized (mUnsentEventsMap) { + if (mUnsentEvents.size() > 0) { + List staledSnapShots = new ArrayList<>(); + + // retry the first + for (int index = 0; index < mUnsentEvents.size(); index++) { + UnsentEventSnapshot unsentEventSnapshot = mUnsentEvents.get(index); + + // check if there is no required delay to resend the message + if (!unsentEventSnapshot.waitToBeResent()) { + // if the message is already resending, + if (unsentEventSnapshot.mIsResending) { + // do not resend any other one to try to keep the messages sending order. + } else { + if (null != unsentEventSnapshot.mEventDescription) { + Log.d(LOG_TAG, "Automatically resend " + unsentEventSnapshot.mEventDescription); + } + + try { + unsentEventSnapshot.mIsResending = true; + unsentEventSnapshot.mRequestRetryCallBack.onRetry(); + break; + } catch (Exception e) { + unsentEventSnapshot.mIsResending = false; + staledSnapShots.add(unsentEventSnapshot); + Log.e(LOG_TAG, "## resentUnsents() : " + unsentEventSnapshot.mEventDescription + " onRetry() failed " + e.getMessage(), e); + } + } + } + } + + mUnsentEvents.removeAll(staledSnapShots); + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/VersionsUtil.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/VersionsUtil.java new file mode 100644 index 0000000000..0b7724fa3a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/util/VersionsUtil.java @@ -0,0 +1,42 @@ +/* + * 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.util; + +import android.support.annotation.Nullable; + +import im.vector.matrix.android.internal.legacy.rest.model.Versions; + +/** + * Companion for class {@link Versions} + */ +public class VersionsUtil { + + private static final String FEATURE_LAZY_LOAD_MEMBERS = "m.lazy_load_members"; + + /** + * Return true if the server support the lazy loading of room members + * + * @param versions the Versions object from the server request + * @return true if the server support the lazy loading of room members + */ + public static boolean supportLazyLoadMembers(@Nullable Versions versions) { + return versions != null + && versions.unstableFeatures != null + && versions.unstableFeatures.containsKey(FEATURE_LAZY_LOAD_MEMBERS) + && versions.unstableFeatures.get(FEATURE_LAZY_LOAD_MEMBERS); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/view/AutoScrollDownListView.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/view/AutoScrollDownListView.java new file mode 100644 index 0000000000..5fad5f8aa4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/view/AutoScrollDownListView.java @@ -0,0 +1,88 @@ +/* + * 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.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ListView; + +import im.vector.matrix.android.internal.legacy.util.Log; + +/** + * The listView automatically scrolls down when its height is updated. + * It is used to scroll the list when the keyboard is displayed + * Note that the list scrolls down automatically thank to android:transcriptMode="normal" in the XML + */ +public class AutoScrollDownListView extends ListView { + private static final String LOG_TAG = AutoScrollDownListView.class.getSimpleName(); + + private boolean mLockSelectionOnResize = false; + + public AutoScrollDownListView(Context context) { + super(context); + } + + public AutoScrollDownListView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AutoScrollDownListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onSizeChanged(int xNew, int yNew, int xOld, int yOld) { + super.onSizeChanged(xNew, yNew, xOld, yOld); + + if (!mLockSelectionOnResize) { + // check if the keyboard is displayed + // we don't want that the list scrolls to the bottom when the keyboard is hidden. + if (yNew < yOld) { + postDelayed(new Runnable() { + @Override + public void run() { + setSelection(getCount() - 1); + } + }, 100); + } + } + } + + /** + * The listview selection is locked even if the view position is updated. + */ + public void lockSelectionOnResize() { + mLockSelectionOnResize = true; + } + + @Override + protected void layoutChildren() { + // the adapter items are added without refreshing the list (back pagination only) + // to reduce the number of refresh + try { + super.layoutChildren(); + } catch (Exception e) { + Log.e(LOG_TAG, "## layoutChildren() failed " + e.getMessage(), e); + } + } + + @Override + // require to avoid lint errors with MatrixMessageListFragment + public void setSelectionFromTop(int position, int y) { + super.setSelectionFromTop(position, y); + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/view/HtmlTagHandler.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/view/HtmlTagHandler.java new file mode 100755 index 0000000000..18d0bec58f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/view/HtmlTagHandler.java @@ -0,0 +1,315 @@ +/* + * Copyright (C) 2013-2015 Dominik Schürmann + * Copyright (C) 2013-2015 Juha Kuitunen + * Copyright (C) 2013 Mohammed Lakkadshaw + * Copyright (C) 2007 The Android Open Source Project + * + * 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.view; + +import android.content.Context; +import android.support.annotation.ColorInt; +import android.support.v4.content.ContextCompat; +import android.text.Editable; +import android.text.Html; +import android.text.Layout; +import android.text.Spannable; +import android.text.Spanned; +import android.text.style.AlignmentSpan; +import android.text.style.BackgroundColorSpan; +import android.text.style.BulletSpan; +import android.text.style.LeadingMarginSpan; +import android.text.style.StrikethroughSpan; +import android.text.style.TypefaceSpan; + +import org.xml.sax.XMLReader; + +import java.util.Stack; + +/** + * Some parts of this code are based on android.text.Html + */ +// custom implementation of org.sufficientlysecure.htmltextview.HtmlTagHandler +// to have the same UI as the webclient +public class HtmlTagHandler implements Html.TagHandler { + /** + * Keeps track of lists (ol, ul). On bottom of Stack is the outermost list + * and on top of Stack is the most nested list + */ + private final Stack lists = new Stack<>(); + /** + * Tracks indexes of ordered lists so that after a nested list ends + * we can continue with correct index of outer list + */ + private final Stack olNextIndex = new Stack<>(); + + /** + * Running HTML table string based off of the root table tag. Root table tag being the tag which + * isn't embedded within any other table tag. Example: + * + * + * ... + *
    + * ... + *
    + * ... + * + * + */ + StringBuilder tableHtmlBuilder = new StringBuilder(); + /** + * Tells us which level of table tag we're on; ultimately used to find the root table tag. + */ + int tableTagLevel = 0; + + private static final int indent = 10; + private static final int listItemIndent = indent * 2; + private static final BulletSpan bullet = new BulletSpan(indent); + + public Context mContext; + + public int mCodeBlockBackgroundColor = -1; + + private static class Ul { + } + + private static class Ol { + } + + private static class Code { + } + + private static class Center { + } + + private static class Strike { + } + + private static class Table { + } + + private static class Tr { + } + + private static class Th { + } + + private static class Td { + } + + /** + * Defines the code block background color + * @param color the new color + */ + public void setCodeBlockBackgroundColor(@ColorInt int color) { + mCodeBlockBackgroundColor = color; + } + + @Override + public void handleTag(final boolean opening, final String tag, Editable output, final XMLReader xmlReader) { + if (opening) { + + if (tag.equalsIgnoreCase("ul")) { + lists.push(tag); + } else if (tag.equalsIgnoreCase("ol")) { + lists.push(tag); + olNextIndex.push(1); + } else if (tag.equalsIgnoreCase("li")) { + if (output.length() > 0 && output.charAt(output.length() - 1) != '\n') { + output.append("\n"); + } + String parentList = lists.peek(); + if (parentList.equalsIgnoreCase("ol")) { + start(output, new Ol()); + output.append(olNextIndex.peek().toString()).append(". "); + olNextIndex.push(olNextIndex.pop() + 1); + } else if (parentList.equalsIgnoreCase("ul")) { + start(output, new Ul()); + } + } else if (tag.equalsIgnoreCase("code")) { + start(output, new Code()); + } else if (tag.equalsIgnoreCase("center")) { + start(output, new Center()); + } else if (tag.equalsIgnoreCase("s") || tag.equalsIgnoreCase("strike")) { + start(output, new Strike()); + } else if (tag.equalsIgnoreCase("table")) { + start(output, new Table()); + if (tableTagLevel == 0) { + tableHtmlBuilder = new StringBuilder(); + // We need some text for the table to be replaced by the span because + // the other tags will remove their text when their text is extracted + output.append("table placeholder"); + } + + tableTagLevel++; + } + else if (tag.equalsIgnoreCase("tr")) { + start(output, new Tr()); + } else if (tag.equalsIgnoreCase("th")) { + start(output, new Th()); + } else if (tag.equalsIgnoreCase("td")) { + start(output, new Td()); + } + } else { + if (tag.equalsIgnoreCase("ul")) { + lists.pop(); + } else if (tag.equalsIgnoreCase("ol")) { + lists.pop(); + olNextIndex.pop(); + } else if (tag.equalsIgnoreCase("li")) { + if (lists.peek().equalsIgnoreCase("ul")) { + if (output.length() > 0 && output.charAt(output.length() - 1) != '\n') { + output.append("\n"); + } + // Nested BulletSpans increases distance between bullet and text, so we must prevent it. + int bulletMargin = indent; + if (lists.size() > 1) { + bulletMargin = indent - bullet.getLeadingMargin(true); + if (lists.size() > 2) { + // This get's more complicated when we add a LeadingMarginSpan into the same line: + // we have also counter it's effect to BulletSpan + bulletMargin -= (lists.size() - 2) * listItemIndent; + } + } + BulletSpan newBullet = new BulletSpan(bulletMargin); + end(output, Ul.class, false, + new LeadingMarginSpan.Standard(listItemIndent * (lists.size() - 1)), + newBullet); + } else if (lists.peek().equalsIgnoreCase("ol")) { + if (output.length() > 0 && output.charAt(output.length() - 1) != '\n') { + output.append("\n"); + } + int numberMargin = listItemIndent * (lists.size() - 1); + if (lists.size() > 2) { + // Same as in ordered lists: counter the effect of nested Spans + numberMargin -= (lists.size() - 2) * listItemIndent; + } + end(output, Ol.class, false, new LeadingMarginSpan.Standard(numberMargin)); + } + } else if (tag.equalsIgnoreCase("code")) { + if (-1 == mCodeBlockBackgroundColor) { + mCodeBlockBackgroundColor = ContextCompat.getColor(mContext, android.R.color.darker_gray); + } + + end(output, Code.class, false, new BackgroundColorSpan(mCodeBlockBackgroundColor), new TypefaceSpan("monospace")); + } else if (tag.equalsIgnoreCase("center")) { + end(output, Center.class, true, new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER)); + } else if (tag.equalsIgnoreCase("s") || tag.equalsIgnoreCase("strike")) { + end(output, Strike.class, false, new StrikethroughSpan()); + } else if (tag.equalsIgnoreCase("table")) { + tableTagLevel--; + + // When we're back at the root-level table + end(output, Table.class, false); + } + else if (tag.equalsIgnoreCase("tr")) { + end(output, Tr.class, false); + } else if (tag.equalsIgnoreCase("th")) { + end(output, Th.class, false); + } else if (tag.equalsIgnoreCase("td")) { + end(output, Td.class, false); + } + } + + storeTableTags(opening, tag); + } + + /** + * If we're arriving at a table tag or are already within a table tag, then we should store it + * the raw HTML for our ClickableTableSpan + */ + private void storeTableTags(boolean opening, String tag) { + if (tableTagLevel > 0 || tag.equalsIgnoreCase("table")) { + tableHtmlBuilder.append("<"); + if (!opening) { + tableHtmlBuilder.append("/"); + } + tableHtmlBuilder + .append(tag.toLowerCase()) + .append(">"); + } + } + + /** + * Mark the opening tag by using private classes + */ + private void start(Editable output, Object mark) { + int len = output.length(); + output.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK); + } + + /** + * Modified from {@link android.text.Html} + */ + private void end(Editable output, Class kind, boolean paragraphStyle, Object... replaces) { + Object obj = getLast(output, kind); + // start of the tag + int where = output.getSpanStart(obj); + // end of the tag + int len = output.length(); + + // If we're in a table, then we need to store the raw HTML for later + if (tableTagLevel > 0) { + final CharSequence extractedSpanText = extractSpanText(output, kind); + tableHtmlBuilder.append(extractedSpanText); + } + + output.removeSpan(obj); + + if (where != len) { + int thisLen = len; + // paragraph styles like AlignmentSpan need to end with a new line! + if (paragraphStyle) { + output.append("\n"); + thisLen++; + } + for (Object replace : replaces) { + output.setSpan(replace, where, thisLen, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + } + + /** + * Returns the text contained within a span and deletes it from the output string + */ + private CharSequence extractSpanText(Editable output, Class kind) { + final Object obj = getLast(output, kind); + // start of the tag + final int where = output.getSpanStart(obj); + // end of the tag + final int len = output.length(); + + final CharSequence extractedSpanText = output.subSequence(where, len); + output.delete(where, len); + return extractedSpanText; + } + + /** + * Get last marked position of a specific tag kind (private class) + */ + private static Object getLast(Editable text, Class kind) { + Object[] objs = text.getSpans(0, text.length(), kind); + if (objs.length == 0) { + return null; + } else { + for (int i = objs.length; i > 0; i--) { + if (text.getSpanFlags(objs[i - 1]) == Spannable.SPAN_MARK_MARK) { + return objs[i - 1]; + } + } + return null; + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index cdf4214242..a187b9ce71 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -1,7 +1,7 @@ package im.vector.matrix.android.internal.session -import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig +import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.internal.di.SessionModule import im.vector.matrix.android.internal.events.sync.SyncModule import im.vector.matrix.android.internal.events.sync.Synchronizer diff --git a/matrix-sdk-android/src/main/res/drawable-xxhdpi/matrix_user.png b/matrix-sdk-android/src/main/res/drawable-xxhdpi/matrix_user.png new file mode 100755 index 0000000000..4a47aaf96d Binary files /dev/null and b/matrix-sdk-android/src/main/res/drawable-xxhdpi/matrix_user.png differ diff --git a/matrix-sdk-android/src/main/res/layout/adapter_item_icon_and_text.xml b/matrix-sdk-android/src/main/res/layout/adapter_item_icon_and_text.xml new file mode 100644 index 0000000000..4227dd9be6 --- /dev/null +++ b/matrix-sdk-android/src/main/res/layout/adapter_item_icon_and_text.xml @@ -0,0 +1,30 @@ + + + + + + + + + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/layout/fragment_dialog_icon_text_list.xml b/matrix-sdk-android/src/main/res/layout/fragment_dialog_icon_text_list.xml new file mode 100644 index 0000000000..bc246d8e67 --- /dev/null +++ b/matrix-sdk-android/src/main/res/layout/fragment_dialog_icon_text_list.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/layout/fragment_matrix_message_list_fragment.xml b/matrix-sdk-android/src/main/res/layout/fragment_matrix_message_list_fragment.xml new file mode 100644 index 0000000000..22ce4e9ae9 --- /dev/null +++ b/matrix-sdk-android/src/main/res/layout/fragment_matrix_message_list_fragment.xml @@ -0,0 +1,22 @@ + + + + + + + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-ar/strings.xml b/matrix-sdk-android/src/main/res/values-ar/strings.xml new file mode 100644 index 0000000000..5d4d33f63a --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-ar/strings.xml @@ -0,0 +1,76 @@ + +عُدّة تطوير البرمجيات لِ‍«ماترِكس» على أندرويد + + أرسل ⁨%1$s⁩ صورة. + + دعوة من ⁨%s⁩ + دعى ⁨%1$s⁩ ⁨%2$s⁩ + دعاك ⁨%1$s⁩ + انضمّ ⁨%1$s⁩ + غادر ⁨%1$s⁩ + رفض ⁨%1$s⁩ الدعوة + طرد ⁨%1$s⁩ ⁨%2$s⁩ + رفع ⁨%1$s⁩ المنع عن ⁨%2$s⁩ + منع ⁨%1$s⁩ ⁨%2$s⁩ + غيّر ⁨%1$s⁩ صورته + ضبط ⁨%1$s⁩ اسم العرض على ⁨%2$s⁩ + غيّر ⁨%1$s⁩ اسم العرض من ⁨%2$s⁩ إلى ⁨%3$s⁩ + أزال ⁨%1$s⁩ اسم العرض (⁨%2$s⁩) + غيّر ⁨%1$s⁩ الموضوع إلى: ⁨%2$s⁩ + غيّر ⁨%1$s⁩ اسم الغرفة إلى: ⁨%2$s⁩ + ردّ ⁨%s⁩ على المكالمة. + أنهى ⁨%s⁩ المكالمة. + جعل ⁨%1$s⁩ تأريخ الغرفة مستقبلًا ظاهرا على %2$s + كل أعضاء الغرفة من لحظة دعوتهم. + كل أعضاء الغرفة من لحظة انضمامهم. + كل أعضاء الغرفة. + الكل. + المجهول (⁨%s⁩). + فعّل ⁨%1$s⁩ تعمية الطرفين (⁨%2$s⁩) + + طلب ⁨%1$s⁩ اجتماع VoIP + بدأ اجتماع VoIP + انتهى اجتماع VoIP + + أزال ⁨%1$s⁩ اسم الغرفة + أزال ⁨%1$s⁩ موضوع الغرفة + " [السبب: ⁨%1$s⁩]" + حدّث ⁨%1$s⁩ اللاحة ⁨%2$s⁩ + أرسل ⁨%1$s⁩ دعوة إلى ⁨%2$s⁩ للانضمام إلى الغرفة + ** تعذّر فك التعمية: ⁨%s⁩ ** + لم يُرسل جهاز المرسل مفاتيح هذه الرسالة. + + تعذّر إرسال الرسالة + + فشل رفع الصورة + + خطأ في الشبكة + خطأ في «ماترِكس» + + ليس ممكنا الانضمام ثانيةً إلى غرفة فارغة. + + رسالة معمّاة + + عنوان البريد الإلكتروني + رقم الهاتف + +‏‏⁨%1$s⁩: ‏⁨%2$s⁩ + انسحب ⁨%1$s⁩ من دعوة ⁨%2$s⁩ + أجرى ⁨%s⁩ مكالمة مرئية. + أجرى ⁨%s⁩ مكالمة صوتية. + "هُذّبت ⁨%1$s⁩ " + " على يد ⁨%1$s⁩" + قبل ⁨%1$s⁩ دعوة ⁨%2$s⁩ + + تعذر التهذيب + أرسل ⁨%1$s⁩ ملصقا. + + (تغيّرت الصورة أيضا) + ردا على + + أرسل صورة. + أرسل فديوهًا. + أرسل ملف صوت. + أرسل ملفًا. + + diff --git a/matrix-sdk-android/src/main/res/values-bg/strings.xml b/matrix-sdk-android/src/main/res/values-bg/strings.xml new file mode 100644 index 0000000000..70de11ac48 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-bg/strings.xml @@ -0,0 +1,76 @@ + +Matrix Android SDK + + %1$s: %2$s + %1$s изпрати снимка. + + Поканата на %s + %1$s покани %2$s + %1$s Ви покани + %1$s се присъедини + %1$s напусна + %1$s отхвърли поканата + %1$s изгони %2$s + %1$s отблокира %2$s + %1$s блокира %2$s + %1$s оттегли поканата си за %2$s + %1$s смени своята профилна снимка + %1$s си сложи име %2$s + %1$s смени своето име от %2$s на %3$s + %1$s премахна своето име (%2$s) + %1$s смени темата на: %2$s + %1$s смени името на стаята на: %2$s + %s започна видео разговор. + %s започна гласов разговор. + %s отговори на повикването. + %s прекрати разговора. + %1$s направи бъдещата история на стаята видима за %2$s + всички членове, от момента на поканването им в нея. + всички членове, от момента на присъединяването им в нея. + всички членове в нея. + всеки. + непозната (%s). + %1$s включи шифроване от край до край (%2$s) + + %1$s заяви VoIP групов разговор + Започна VoIP групов разговор + Груповият разговор приключи + + (профилната снимка също беше сменена) + %1$s премахна името на стаята + %1$s премахна темата на стаята + "изтрито %1$s " + " от %1$s" + " [причина: %1$s]" + %1$s обнови своя профил %2$s + %1$s изпрати покана на %2$s да се присъедини към стаята + %1$s прие поканата за %2$s + + ** Неуспешно разшифроване: %s ** + Неуспешно премахване + Неуспешно изпращане на съобщението + + Неуспешно качване на снимката + + Грешка в мрежата + Matrix грешка + + В момента не е възможно да се присъедините отново към празна стая. + + Шифровано съобщение + + Имейл адрес + Телефонен номер + +Устройството на подателя не изпрати ключовете за това съобщение. + + %1$s изпрати стикер. + + В отговор на + + изпрати снимка. + изпрати видео. + изпрати аудио файл. + изпрати файл. + + diff --git a/matrix-sdk-android/src/main/res/values-bs/strings.xml b/matrix-sdk-android/src/main/res/values-bs/strings.xml new file mode 100644 index 0000000000..a6b3daec93 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-bs/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-ca/strings.xml b/matrix-sdk-android/src/main/res/values-ca/strings.xml new file mode 100644 index 0000000000..539ca6a14f --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-ca/strings.xml @@ -0,0 +1,67 @@ + +%1$s: %2$s + %1$s ha enviat una imatge. + + %1s ha sortit + %1s ha entrat + Número de telèfon + +Correu electrònic + Missatge encriptat + + la invitació de %s + %1$s ha convidat a %2$s + %1$s vos ha convidat + %1$s ha rebutjat la invitació + %1$s ha fet fora a %2$s + Android SDK de Matrix + + %1$s ha canviat el seu nom visible de %2$s a %3$s + %1$s ha eliminat el seu nom visible (%2$s) + %1$s ha canviat el tema a: %2$s + %1$s ha canviat el nom de la sala a: %2$s + %s ha contestat la trucada. + %s ha finalitzat la trucada. + tots el membres de la sala, des del punt en què són convidats. + tots els membres de la sala. + desconegut (%s). + %1$s ha activat l\'encriptació d\'extrem a extrem (%2$s) + + %1$s ha sol·licitat una conferència VoIP + %1$s ha readmès a %2$s + %1$s ha expulsat a %2$s + %1$s ha retirat la invitació de %2$s + %1$s ha canviat el seu avatar + %1$s ha permès a %2$s veure l\'historial que es generi a partir d\'ara + tots els membres de la sala des del punt en què hi entrin. + qualsevol. + S\'ha iniciat la conferència VoIP + S\'ha finalitzat la conferència VoIP + + (s\'ha canviat també l\'avatar) + %1$s ha eliminat el nom de la sala + %1$s ha eliminat el tema de la sala + "ha redactat %1$s " + " per %1$s" + " [raó: %1$s]" + %1$s ha actualitzat el seu perfil %2$s + %1$s ha enviat una invitació a l\'usuari %2$s per a entrar a la sala + %1$s ha acceptat la invitació de l\'usuari %2$s + + ** No s\'ha pogut desencriptar: %s ** + El dispositiu del remitent no ha enviat les claus per aquest missatge. + + No s\'ha pogut redactar + No s\'ha pogut enviar el missatge + + No s\'ha pogut pujar la imatge + + S\'ha produït un error de xarxa + S\'ha produït un error de Matrix + + Actualment no es pot tornar a entrar a una sala buida. + + %1$s a canviat el seu nom visible a %2$s + %s ha iniciat una trucada de vídeo. + %s ha iniciat una trucada de veu. + diff --git a/matrix-sdk-android/src/main/res/values-cs/strings.xml b/matrix-sdk-android/src/main/res/values-cs/strings.xml new file mode 100644 index 0000000000..a6b3daec93 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-cs/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-da/strings.xml b/matrix-sdk-android/src/main/res/values-da/strings.xml new file mode 100644 index 0000000000..d7c37c1ddd --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-da/strings.xml @@ -0,0 +1,67 @@ + +Matrix Android SDK + + %1$s: %2$s + %1$s sendte et billede. + + %ss invitation + %1$s inviterede %2$s + %1$s inviterede dig + %1$s forbandt + %1$s forlod rummet + %1$s afviste invitationen + %1$s kickede %2$s + %1$s unbannede %2$s + %1$s bannede %2$s + %1$s trak %2$ss invitation tilbage + %1$s skiftede sin avatar + %1$s satte sit viste navn til %2$s + %1$s ændrede sit viste navn fra %2$s til %3$s + %1$s fjernede sit viste navn (%2$s) + %1$s ændrede emnet til: %2$s + %1$s ændrede rumnavnet til: %2$s + %s startede et videoopkald. + %s startede et stemmeopkald. + %s svarede opkaldet. + %s stoppede opkaldet. + %1$s gjorde den fremtidige rum historik synlig for %2$s + alle medlemmer af rummet, fra det tidspunkt de er inviteret. + alle medlemmer af rummet, fra det tidspunkt de er forbundede. + Alle medlemmer af rummet. + alle. + ukendt (%s). + %1$s slog ende-til-ende kryptering til (%2$s) + + %1$s forespurgte en VoIP konference + VoIP konference startet + VoIP konference afsluttet + + (avatar blev også ændret) + %1$s fjernede navnet på rummet + %1$s fjernede emnet for rummet + "hemmeligholdte %1$s " + " af %1$s" + " [årsag: %1$s]" + %1$s opdaterede sin profil %2$s + %1$s inviterede %2$s til rummet + %1$s accepterede invitationen til %2$s + + ** Kunne ikke dekryptere: %s ** + Afsenderens enhed har ikke sendt os nøglerne til denne besked. + + Kunne ikke hemmeligholde + Kunne ikke sende besked + + Kunne ikke uploade billede + + Netværks fejl + Matrix fejl + + Det er i øjeblikket ikke muligt at genforbinde til et tomt rum. + + Krypteret besked + + mailadresse + Telefonnummer + + diff --git a/matrix-sdk-android/src/main/res/values-de/strings.xml b/matrix-sdk-android/src/main/res/values-de/strings.xml new file mode 100644 index 0000000000..d58c94045d --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-de/strings.xml @@ -0,0 +1,87 @@ + + + Matrix-Android-SDK + + %1$s: %2$s + %1$s hat ein Bild gesendet. + + %s\'s Einladung + %1$s hat %2$s eingeladen + %1$s hat dich eingeladen + %1$s hat den Raum betreten + %1$s hat den Raum verlassen + %1$s hat die Einladung abgelehnt + %1$s hat %2$s gekickt + %1$s hat die Verbannung von %2$s aufgehoben + %1$s hat %2$s verbannt + %1$s hat die Einladung für %2$s zurückgezogen + %1$s hat das Profilbild geändert + %1$s hat den Anzeigenamen geändert in %2$s + %1$s hat den Anzeigenamen von %2$s auf %3$s geändert + %1$s hat den Anzeigenamen gelöscht (%2$s) + %1$s hat das Raumthema geändert auf: %2$s + %1$s hat den Raumnamen geändert in: %2$s + %s hat einen Videoanruf durchgeführt. + %s hat einen Sprachanruf getätigt. + %s hat den Anruf angenommen. + %s hat den Anruf beendet. + %1$s hat den zukünftigen Chatverlauf sichtbar gemacht für %2$s + Alle Mitglieder (ab dem Zeitpunkt, an dem sie eingeladen wurden). + Alle Mitglieder (ab dem Zeitpunkt, an dem sie den Raum betreten haben). + alle Raum-Mitglieder. + Jeder. + Unbekannt (%s). + %1$s hat die Ende-zu-Ende-Verschlüsselung aktiviert (%2$s) + + %1$s möchte eine VoIP-Konferenz beginnen + VoIP-Konferenz gestartet + VoIP-Konferenz beendet + + (Profilbild wurde ebenfalls geändert) + %1$s hat den Raumnamen entfernt + %1$s hat das Raum-Thema entfernt + "Verborgen %1$s " + durch %1$s + [Grund: %1$s] + %1$s hat das Benutzerprofil aktualisiert %2$s + %1$s hat eine Einladung an %2$s gesendet + %1$s hat die Einladung für %2$s akzeptiert + + ** Nicht entschlüsselbar: %s ** + Das absendende Gerät hat uns keine Schlüssel für diese Nachricht übermittelt. + + + Entfernen nicht möglich + Nachricht kann nicht gesendet werden + + Bild konnte nicht hochgeladen werden + + + Netzwerk-Fehler + Matrix-Fehler + + + + + + + + + Es ist aktuell nicht möglich, einen leeren Raum erneut zu betreten. + + Verschlüsselte Nachricht + + + E-Mail-Adresse + Telefonnummer + +%1$s sandte einen Sticker. + + Als Antwort auf + + sandte ein Bild. + sandte ein Video. + sandte eine Audio-Datei. + sandte eine Datei. + + diff --git a/matrix-sdk-android/src/main/res/values-el/strings.xml b/matrix-sdk-android/src/main/res/values-el/strings.xml new file mode 100644 index 0000000000..ff855d714e --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-el/strings.xml @@ -0,0 +1,60 @@ + +Ηλεκτρονική διεύθυνση + %1$s: %2$s + Ο/Η %1$s έστειλε μια εικόνα. + Ο/Η %1$s έστειλε ένα αυτοκόλλητο. + + Ο/Η %1$s σας προσκάλεσε + Ο/Η %1$s αποχώρησε + Ο/Η %1$s απέρριψε την πρόσκληση + Ο/Η %1$s έδιωξε τον/την %2$s + Ο/Η %1$s προσκάλεσε τον/την %2$s + Η πρόσκληση του/της %s + Αριθμός τηλεφώνου + +Ο/Η %1$s απέκλεισε τον/την %2$s + Ο/Η %1$s απέσυρε την πρόσκληση του/της %2$s + Ο/Η %1$s άλλαξε εικονίδιο χρήστη + Ο/Η %1$s άλλαξε το εμφανιζόμενό του/της όνομα σε %2$s + Ο/Η %1$s άλλαξε το εμφανιζόμενό του/της όνομα από %2$s σε %3$s + Ο/Η %1$s αφαίρεσε το εμφανιζόμενό του/της όνομα (%2$s) + Ο/Η %1$s άλλαξε το θέμα σε: %2$s + Ο/Η %1$s άλλαξε το όνομα του δωματίου σε: %2$s + Ο/Η %s απάντησε στην κλήση. + Ο/Η %s τερμάτισε την κλήση. + SDK για Android του Matrix + + Ο/Η %s πραγματοποίησε μια κλήση βίντεο. + Ο/Η %s πραγματοποίησε μια κλήση ήχου. + Ο/Η %1$s κατέστησε το μελλοντικό ιστορικό του δωματίου ορατό στον/στην %2$s + όλα τα μέλη του δωματίου, από την στιγμή που προσκλήθηκαν. + όλα τα μέλη του δωματίου. + οποιοσδήποτε. + άγνωστος/η (%s). + (έγινε αλλαγή και του εικονιδίου χρήστη) + Ο/Η %1$s αφαίρεσε το όνομα του δωματίου + Ο/Η %1$s αφαίρεσε το θέμα του δωματίου + " από τον/την %1$s" + " [λόγος: %1$s]" + Ο/Η %1$s ανανέωσε το προφίλ του/της %2$s + Ο/Η %1$s δέχτηκε την πρόσκληση για το %2$s + + ** Αδυναμία αποκρυπτογράφησης: %s ** + Η συσκευή του/της αποστολέα δεν μας έχει στείλει τα κλειδιά για αυτό το μήνυμα. + + Προς απάντηση στο + + Αποτυχία αποστολής μηνύματος + + Αποτυχία αναφόρτωσης εικόνας + + Σφάλμα δικτύου + Σφάλμα του Matrix + + Κρυπτογραφημένο μήνυμα + + Ο/Η %1$s ζήτησε μια VoIP διάσκεψη + Η VoIP διάσκεψη ξεκίνησε + Η VoIP διάσκεψη έληξε + + diff --git a/matrix-sdk-android/src/main/res/values-eo/strings.xml b/matrix-sdk-android/src/main/res/values-eo/strings.xml new file mode 100644 index 0000000000..916ca209ad --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-eo/strings.xml @@ -0,0 +1,16 @@ + +%1$s sendis bildon. + %1$s sendis glumarkon. + + invito de %s + %1$s invitis %2$s + %1$s invitis vin + %1$s alvenis + %1$s foriris + %1$s malakceptis la inviton + %1$s forpelis %2$s + %1$s malforbaris %2$s + %1$s forbaris %2$s + %1$s malinvitis %2$s + %1$s ŝanĝis sian profilbildon + diff --git a/matrix-sdk-android/src/main/res/values-es-rMX/strings.xml b/matrix-sdk-android/src/main/res/values-es-rMX/strings.xml new file mode 100644 index 0000000000..57fb929b49 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-es-rMX/strings.xml @@ -0,0 +1,87 @@ + + + Matrix Android SDK + + %1$s: %2$s + %1$s envió una imagen. + + la invitación de %s + %1$s invitó a %2$s + %1$s te invitó + %1$s se unió + %1$s salió + %1$s rechazó la invitación + %1$s quitó a %2$s + %1$s desprohibió a %2$s + %1$s prohibió %2$s + %1$s retiró la invitación de %2$s + %1$s cambio su foto de perfil + %1$s estableció %2$s como su nombre visible + %1$s cambió su nombre visible de %2$s a %3$s + %1$s retiró su nombre visible (%2$s) + %1$s cambio el tema a: %2$s + %1$s cambió el nombre de la sala a: %2$s + %s comenzó una llamada de video. + %s comenzó una llamada de voz. + %s recibió la llmada. + %s terminó la llamada. + %1$s dejo que %2$s vea el historial del futuro + todos los miembros de la sala, desde su invitación. + todos los miembros de la sala, desde cuando entraron. + todos los miembros de la sala. + todos. + desconocido (%s). + %1$s encendió el cifrado de extremo a extremo (%2$s) + + %1$s solicitó una conferencia VoIP + conferencia VoIP comenzó + conferencia VoIP finalizó + + (foto de perfil también se cambió) + %1$s retiró el nombre de la sala + %1$s retiro el tema de la sala + redactado %1$s + por %1$s + [razón: %1$s] + %1$s actualizó su perfil %2$s + %1$s envió una invitación a %2$s para entrar a la sala + %1$s aceptó la invitacion de %2$s + + ** No se puede descifrar: %s ** + El dispositivo del remitente no nos ha enviado las claves de este mensaje. + + + No se pudo redactar + No se puede enviar el mensaje + + La subida de la imagen falló + + + Error de la red + Error de Matrix + + + + + + + + + No es posible volver a entrar en una sala vacía. + + Mensaje cifrado + + + Correo electrónico + Número telefónico + +%1$s envió una pegatina. + + En respuesta a + + envió una imagen. + envió un vídeo. + envió un archivo de audio. + envió un archivo. + + diff --git a/matrix-sdk-android/src/main/res/values-es/strings.xml b/matrix-sdk-android/src/main/res/values-es/strings.xml new file mode 100644 index 0000000000..a6cc2bd706 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-es/strings.xml @@ -0,0 +1,87 @@ + + + SDK de Matrix Android + + %1$s: %2$s + %1$s envió una imagen. + + la invitación de %s + %1$s invitó a %2$s + %1$s te invitó + %1$s se unió + %1$s salió + %1$s rechazó la invitación + %1$s expulsó a %2$s + %1$s le quitó el veto a %2$s + %1$s vetó a %2$s + %1$s retiró la invitación de %2$s + %1$s cambió su avatar + %1$s estableció %2$s como su nombre público + %1$s cambió su nombre público de %2$s a %3$s + %1$s eliminó su nombre público (%2$s) + %1$s cambió el tema a: %2$s + %1$s cambió el nombre de la sala a: %2$s + %s realizó una llamada de vídeo. + %s realizó una llamada de voz. + %s contestó la llamada. + %s finalizó la llamada. + %1$s hizo visible el historial futuro de la sala para %2$s + todos los miembros de la sala, desde el momento en que son invitados. + todos los miembros de la sala, desde el momento en que se unieron. + todos los miembros de la sala. + cualquier persona. + desconocido (%s). + %1$s activó el cifrado de extremo a extremo (%2$s) + + %1$s solicitó una conferencia de vozIP + conferencia de vozIP iniciada + conferencia de vozIP finalizada + + (el avatar también se cambió) + %1$s eliminó el nombre de la sala + %1$s eliminó el tema de la sala + redactado %1$s + por %1$s + [razón: %1$s] + %1$s actualizó su perfil %2$s + %1$s invitó a %2$s a unirse a la sala + %1$s aceptó la invitación para %2$s + + ** No es posible descifrar: %s ** + El dispositivo emisor no nos ha enviado las claves para este mensaje. + + + No se pudo redactar + No es posible enviar el mensaje + + No se pudo cargar la imagen + + + Error de red + Error de Matrix + + + + + + + + + Actualmente no es posible volver a unirse a una sala vacía. + + Mensaje cifrado + + + Dirección de correo electrónico + Número telefónico + + %1$s envió una pegatina. + + En respuesta a + + envió una imagen. + envió un vídeo. + envió un archivo de audio. + envió un archivo. + + diff --git a/matrix-sdk-android/src/main/res/values-eu/strings.xml b/matrix-sdk-android/src/main/res/values-eu/strings.xml new file mode 100644 index 0000000000..41d306f671 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-eu/strings.xml @@ -0,0 +1,76 @@ + +Matrix Android SDK + + %1$s: %2$s + %1$s erabiltzaileak irudi bat bidali du. + + %s erabiltzailearen gonbidapena + %1$s erabiltzaileak %2$s gonbidatu du + %1$s erabiltzaileak gonbidatu zaitu + %1$s elkartu da + %1$s atera da + %1$s erabiltzaileak gonbidapena baztertu du + %1$s erabiltzaileak %2$s kanporatu du + %1$s erabiltzaileak debekua kendu dio %2$s erabiltzaileari + %1$s erabiltzaileak %2$s debekatu du + %1$s erabiltzaileak %2$s erabiltzailearen gonbidapena atzera bota du + %1$s erabiltzaileak abatarra aldatu du + %1$s erabiltzaileak bere pantaila-izena aldatu du beste honetara: %2$s + %1$s erabiltzaileak bere pantaila-izena aldatu du, honetatik: %2$s honetara: %3$s + %1$s erabiltzaileak bere pantaila-izena kendu du (%2$s) + %1$s erabiltzaileak mintzagaia honetara aldatu du: %2$s + %1$s erabiltzaileak gelaren izena honetara aldatu du: %2$s + %s erabiltzaileak bideo deia hasi du. + %s erabiltzaileak ahots deia hasi du. + %s erabiltzaileak deia erantzun du. + %s erabiltzaileak deia amaitu du. + %1$s erabiltzaileak gelaren historiala ikusgai jarri du hauentzat: %2$s + gelako kide guztiak, gonbidatu zitzaienetik. + gelako kide guztiak, elkartu zirenetik. + gelako kide guztiak. + edonor. + ezezaguna (%s). + %1$s erabiltzaileak muturretik muturrera zifratzea aktibatu du (%2$s) + + %1$s erabiltzaileak VoIP konferentzia bat eskatu du + VoIP konferentzia hasita + VoIP konferentzia amaituta + + (abatarra ere aldatu da) + %1$s erabiltzaileak gelaren izena kendu du + %1$s erabiltzaileak gelaren mintzagaia kendu du + "kenduta %1$s " + " nork: %1$s" + " [arrazoia: %1$s]" + %1$s erabiltzaileak bere profila eguneratu du %2$s + %1$s erabiltzaileak gelara elkartzeko gonbidapen bat bidali dio %2$s erabiltzaileari + %1$s erabiltzaileak %2$s gelarako gonbidapena onartu du + + ** Ezin izan da deszifratu: %s ** + Igorlearen gailuak ez dizkigu mezu honetarako gakoak bidali. + + Ezin izan da kendu + Ezin izan da mezua bidali + + Huts egin du irudia igotzean + + Sare errorea + Matrix errorea + + Ezin da oraingoz hutsik dagoen gela batetara berriro sartu. + + Zifratutako mezua + + E-mail helbidea + Telefono zenbakia + +%1$s erabiltzaileak eranskailu bat bidali du. + + Honi erantzunez + + irudi bat bidali du. + bideo bat bidali du. + audio fitxategi bat bidali du. + fitxategi bat bidali du. + + diff --git a/matrix-sdk-android/src/main/res/values-fi/strings.xml b/matrix-sdk-android/src/main/res/values-fi/strings.xml new file mode 100644 index 0000000000..dac28a3bba --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-fi/strings.xml @@ -0,0 +1,66 @@ + +%1$s lähetti kuvan. + + %s:n kutsu + %1$s kutsui %2$s + %1$s kutsui sinut + %1$s liittyi + %1$s poistui + %1$s hylkäsi kutsun + %1$s poisti käyttäjän %2$s + %1$s poisti porttikiellon käyttäjältä %2$s + %1$s antoi porttikiellon käyttäjälle %2$s + %1$s veti takaisin kutsun käyttäjälle %2$s + %1$s vaihtoi profiilikuvaa + %1$s asetti näyttönimekseen %2$s + %1$s muutti näyttönimensä %2$s -> %3$s + %1$s poisti näyttönimensä (%2$s) + %1$s muutti aiheeksi %2$s + %1$s muutti huoneen nimeksi %2$s + %s soitti videopuhelun. + %s soitti puhelun. + %s vastasi puheluun. + %s lopetti puhelun. + %1$s muutti tulevan huonehistorian näkyväksi käyttäjälle %2$s + kaikki jäsenet, heidän kutsuistaan asti. + kaikki jäsenet, heidän liittymisestään asti. + kaikki huoneen jäsenet. + kaikki. + tuntematon (%s). + %1$s kytki päälle päästä päähän-salauksen (%2$s) + + %1$s lähetti VoIP konferenssi-pyynnön + VoIP konferenssi alkoi + VoIP konferenssi loppui + + (profiilikuva muuttui myös) + %1$s poisti huoneen nimen + %1$s poisti huoneen aiheen + " [syy: %1$s]" + %1$s päivitti profiilinsa %2$s + %1$s lähetti liittymiskutsun käyttäjälle %2$s + %1$s hyväksyi kutsun käyttäjän %2$s puolesta + ** Salauksen purku epäonnistui: %s ** + Lähettäjän laite ei ole lähettänyt avaimia tähän viestiin. + + Viestin lähetys epäonnistui + + Kuvan lataaminen epäonnistui + + Verkkovirhe + Matrix virhe + + Tällä hetkellä ei ole mahdollista liittyä uudelleen tyhjään huoneeseen. + + Salattu viesti + + Sähköpostiosoite + Puhelinnumero + +Matrix Android SDK + + " käyttäjän %1$s toimesta" + Takaisinveto epäonnistui + %1$s: %2$s + "veti takaisin %1$s " + diff --git a/matrix-sdk-android/src/main/res/values-fr/strings.xml b/matrix-sdk-android/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000000..039e71290b --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-fr/strings.xml @@ -0,0 +1,76 @@ + +SDK Matrix Android + + %1$s : %2$s + %1$s a envoyé une image. + + invitation de %s + %1$s a invité %2$s + %1$s vous a invité + %1$s a rejoint la discussion + %1$s est parti + %1$s a rejeté l\'invitation + %1$s a exclu %2$s + %1$s a révoqué le bannissement de %2$s + %1$s a banni %2$s + %1$s a annulé l\'invitation de %2$s + %1$s a changé d\'avatar + %1$s a réglé son nom affiché en %2$s + %1$s a modifié son nom affiché %2$s en %3$s + %1$s a supprimé son nom affiché (%2$s) + %1$s a changé le sujet en : %2$s + %1$s a changé le nom du salon en : %2$s + %s a passé un appel vidéo. + %s a passé un appel vocal. + %s a répondu à l\'appel. + %s a raccroché. + %1$s a rendu l\'historique futur du salon visible pour %2$s + tous les membres du salon, depuis qu\'ils ont été invités. + tous les membres du salon, depuis qu\'ils l\'ont rejoint. + tous les membres du salon. + n\'importe qui. + inconnu (%s). + %1$s a activé le chiffrement de bout en bout (%2$s) + + %1$s a demandé une téléconférence VoIP + Téléconférence VoIP démarrée + Téléconférence VoIP terminée + + (l\'avatar a aussi changé) + %1$s a supprimé le nom du salon + %1$s a supprimé le sujet du salon + "a effacé %1$s " + " de %1$s" + " [motif : %1$s]" + %1$s a mis à jour son profil %2$s + %1$s a envoyé une invitation à %2$s pour rejoindre le salon + %1$s a accepté l\'invitation pour %2$s + + ** Déchiffrement impossible : %s ** + L\'appareil de l\'expéditeur ne nous a pas envoyé les clés pour ce message. + + Effacement impossible + Envoi du message impossible + + L\'envoi de l\'image a échoué + + Erreur de réseau + Erreur de Matrix + + Il est impossible pour le moment de revenir dans un salon vide. + + Message chiffré + + Adresse e-mail + Numéro de téléphone + +%1$s a envoyé un sticker. + + En réponse à + + a envoyé une image. + a envoyé une vidéo. + a envoyé un fichier audio. + a envoyé un fichier. + + diff --git a/matrix-sdk-android/src/main/res/values-gl/strings.xml b/matrix-sdk-android/src/main/res/values-gl/strings.xml new file mode 100644 index 0000000000..5110fb773a --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-gl/strings.xml @@ -0,0 +1,75 @@ + +Enderezo de correo + Fallo ao subir a páxina + + Matrix Android SDK + + %1$s: %2$s + %1$s enviou unha imaxe. + %1$s enviou unha icona. + + Convite de %s + %1$s convidou a %2$s + %1$s convidouno + %1$s entrou + %1$s saíu + %1$s rexeitou o convite + %1$s expulsou a %2$s + %1$s desbloqueou a %2$s + %1$s bloqueou a %2$s + %1$s cancelou o convite de %2$s + %1$s cambiou o seu avatar + %1$s cambiou o seu nome a %2$s + %1$s cambiou o seu nome de %2$s a %3$s + %1$s borrou o seu nome público (%2$s) + %1$s cambiou o tema desta sala para: %2$s + %1$s cambiou o nome desta sala para: %2$s + %s iniciou unha chamada de vídeo. + %s iniciou unha chamada de voz. + %s respondeu á chamada. + %s terminou a chamada. + %1$s fixo visible os próximos históricos para %2$s + toda a xente que integran esta sala, desde o momento en que foron convidados. + todas a xente da sala, desde o momento en que entraron. + todas os membros da sala. + todos. + descoñecido (%s). + %1$s activou a criptografía par-a-par (%2$s) + + %1$s solicitou unha conferencia VoIP + A conferencia VoIP comenzou + A conferencia VoIP terminou + + (o avatar tamén foi cambiado) + %1$s borrou o nome da sala + %1$s removeu o tema da sala + "redactou %1$s " + " de %1s" + " [motivo: %1$s]" + %1$s actualizou o seu perfil %2$s + %1$s envioulle un convite a %2$s para que entre na sala + %1$s aceptou o convite para %2$s + + ** Imposíbel descifrar: %s ** + O dispositivo do que envía non enviou as chaves desta mensaxe. + + Respondéndolle a + + Non se puido redactar + Non foi posíbel enviar a mensaxe + + Erro da conexión + Erro de Matrix + + Aínda non é posíbel volver a entrar nunha sala baleira. + + Mensaxe cifrada + + Número de teléfono + +Responder a + enviar un vídeo. + enviar un ficheiro de son. + enviar un ficheiro. + + diff --git a/matrix-sdk-android/src/main/res/values-hu/strings.xml b/matrix-sdk-android/src/main/res/values-hu/strings.xml new file mode 100644 index 0000000000..ccbcead0c1 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-hu/strings.xml @@ -0,0 +1,76 @@ + +Matrix Android SDK + + %1$s: %2$s + %1$s küldött egy képet. + + %s meghívója + %1$s meghívta %2$s -t + %1$s meghívott + %1$s csatlakozott + %1$s kilépett + %1$s elutasította a meghívást + %1$s kidobta %2$s -t + %1$s feloldotta tiltását %2$s -nak/nek + %1$s kitiltotta %2$s -t + %1$s visszavonta %2$s\'s meghívását + %1$s megváltoztatták a felhasználó képüket + %1$s megváltoztatták a megjelenő nevüket erre: %2$s + %1$s megváltoztatták a megjelenő nevüket erről %2$s erre %3$s + %1$s eltávolították a megjelenő nevüket (%2$s) + %1$s megváltoztatta a témát erre: %2$s + %1$s megváltoztatta a szoba nevét erre: %2$s + %s videóhívást kezdeményezett. + %s hanghívást kezdeményezett. + %s elfogadta a hívást. + %s befejezte a hívást. + %1$s láthatóvá tette a jövőbeli előzményeket %2$s számára + az összes szoba tag, onnantól, hogy meg lettek hívva. + az összes szoba tag, onnantól, hogy csatlakoztak. + az összes szoba tag. + bárki. + ismeretlen (%s). + %1$s bekapcsolta a végtől végig titkosítást (%2$s) + + %1$s hanghívás konferenciát kérelmezett + Hanghívás konferencia elindult + Hanghívás konferencia befejeződött + + (profilképp is meg lett változtatva) + %1$s eltávolította a szoba nevét + %1$s eltávolította a szoba témáját + "szerkesztett %1$s " + " %1$s által" + " [ok: %1$s]" + %1$s megváltoztatták a profiljukat %2$s + "%1$s meghívót küldött %2$s -nak/-nek hogy csatlakozzon a szobához" + %1$s elfogadta a meghívót a %2$s -hoz + + ** Visszafejtés sikertelen: %s ** + A küldő eszköze nem küldte el a kulcsokat ehhez az üzenethez. + + Szerkesztés sikertelen + Üzenet küldése sikertelen + + Kép feltöltése sikertelen + + Hálózat hiba + Matrix hiba + + Jelenleg nem lehetséges újracsatlakozni egy üres szobába. + + Titkosított üzenet + + Email cím + Telefonszám + +%1$s küldött egy matricát. + + Válasz erre: + + kép elküldve. + videó elküldve. + hangfájl elküldve. + fájl elküldve. + + diff --git a/matrix-sdk-android/src/main/res/values-is/strings.xml b/matrix-sdk-android/src/main/res/values-is/strings.xml new file mode 100644 index 0000000000..e665055ac3 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-is/strings.xml @@ -0,0 +1,70 @@ + +Matrix Android SDK + + %1$s: %2$s + %1$s sendi mynd. + %1$s sendi límmerki. + + %s sendi boð um þátttöku + %1$s bauð %2$s + %1$s bauð þér + %1$s gekk í hópinn + %1$s hætti + %1$s hafnaði boðinu + %1$s sparkaði %2$s + %1$s afbannaði %2$s + %1$s bannaði %2$s + %1$s breyttu auðkennismynd sinni + allir meðlimir spjallrásar, síðan þeim var boðið. + allir meðlimir spjallrásar, síðan þeir skráðu sig. + allir meðlimir spjallrásar. + hver sem er. + óþekktur (%s). + VoIP-símafundur hafinn + VoIP-símafundi lokið + + (einnig var skipt um auðkennismynd) + " af %1$s" + " [ástæða: %1$s]" + ** Mistókst að afkóða: %s ** + Sem svar til + + Gat ekki sent skilaboð + + Gat ekki sent inn mynd + + Villa í netkerfi + Villa í Matrix + + Dulrituð skilaboð + + Tölvupóstfang + Símanúmer + +%1$s tók til baka boð frá %2$s + %1$s setti birtingarnafn sitt sem %2$s + %1$s breytti birtingarnafni sínu úr %2$s í %3$s + %1$s fjarlægði birtingarnafn sitt (%2$s) + %1$s breytti umræðuefninu í: %2$s + %1$s breytti heiti spjallrásarinnar í: %2$s + %s hringdi myndsamtal. + %s hringdi raddsamtal. + %s svaraði símtalinu. + %s lauk símtalinu. + %1$s kveikti á enda-í-enda dulritun (%2$s) + + %1$s bað um VoIP-símafund + %1$s fjarlægði heiti spjallrásar + %1$s fjarlægði umfjöllunarefni spjallrásar + %1$s gerði ferilskrá spjallrásar héðan í frá sýnilega fyrir %2$s + "ritstýrði %1$s " + %1$s uppfærði notandasniðið sitt %2$s + %1$s sendi boð til %2$s um þátttöku í spjallrásinni + %1$s samþykkti boð um að taka þátt í %2$s + + Tæki sendandans hefur ekki sent okkur dulritunarlyklana fyrir þessi skilaboð. + + Gat ekki ritstýrt + Ekki er í augnablikinu hægt að taka aftur þátt í spjallrás sem er tóm. + + diff --git a/matrix-sdk-android/src/main/res/values-it/strings.xml b/matrix-sdk-android/src/main/res/values-it/strings.xml new file mode 100644 index 0000000000..5348662fe5 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-it/strings.xml @@ -0,0 +1,76 @@ + +Matrix Android SDK + + %1$s: %2$s + %1$s ha inviato un\'immagine. + + Invito di %s + %1$s ha invitato %2$s + %1$s ti ha invitato + %1$s è entrato + %1$s è uscito + %1$s ha rifiutato l\'invito + %1$s ha buttato fuori %2$s + %1$s ha tolto il bando a %2$s + %1$s ha bandito %2$s + %1$s ha revocato l\'invito per %2$s + %1$s ha modificato il suo avatar + %1$s hanno cambiato il nome visualizzato con %2$s + %1$s ha cambiato il nome visualizzato da %2$s a %3$s + %1$s ha rimosso il nome visibile (%2$s) + %1$s ha cambiato l\'argomento con: %2$s + %1$s ha cambiato il nome della stanza con: %2$s + %s ha iniziato una chiamata video. + %s ha iniziato una chiamata vocale. + %s ha risposto alla chiamata. + %s ha terminato la chiamata. + %1$s ha reso la futura cronologia della stanza visibile a %2$s + tutti i membri della stanza, dal momento del loro invito. + tutti i membri della stanza, dal momento in cui sono entrati. + tutti i membri della stanza. + chiunque. + sconosciuto (%s). + %1$s ha attivato la crittografia end-to-end (%2$s) + + %1$s ha richiesto una conferenza VoIP + Conferenza VoIP iniziata + Conferenza VoIP terminata + + (anche l\'avatar è cambiato) + %1$s ha rimosso il nome della stanza + %1$s ha rimosso l\'argomento della stanza + " da %1$s" + " [motivo: %1$s]" + %1$s ha aggiornato il profilo %2$s + %1$s ha mandato un invito a %2$s per unirsi alla stanza + %1$s ha accettato l\'invito per %2$s + + ** Impossibile decriptare: %s ** + Il dispositivo del mittente non ci ha inviato le chiavi per questo messaggio. + + Impossibile revisionare + Impossibile inviare il messaggio + + Invio dell\'immagine fallito + + Errore di rete + Errore di Matrix + + Al momento non è possibile rientrare in una stanza vuota. + + Messaggio criptato + + Indirizzo email + Numero di telefono + +"revisionato %1$s " + %1$s ha inviato un adesivo. + + In risposta a + + inviata un\'immagine. + inviato un video. + inviato un file audio. + inviato un file. + + diff --git a/matrix-sdk-android/src/main/res/values-lv/strings.xml b/matrix-sdk-android/src/main/res/values-lv/strings.xml new file mode 100644 index 0000000000..0e7a9cc0ba --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-lv/strings.xml @@ -0,0 +1,67 @@ + +Matrix Android SDK + + %1$s: %2$s + %1$s nosūtīja attēlu. + + %s\'s uzaicinājums + %1$s uzaicināja %2$s + %1$s uzaicināja tevi + %1$s pievienojās + %1$s atstāja + %1$s noraidīja uzaicinājumu + %1$s \"izspēra\" ārā %2$s + %1$s atbanoja (atcēla pieejas liegumu) %2$s + %1$s liedza pieeju (banoja) %2$s + %1$s atsauca %2$s uzaicinājumu + %1$s nomainīja profila attēlu + %1$s uzstādīja redzamo vārdu uz %2$s + %1$s nomainīja redzamo vārdu no %2$s uz %3$s + %1$s dzēsa savu redzamo vārdu (%2$s) + %1$s nomainīja tēmas nosaukumu uz: %2$s + %1$s nomainīja istabas nosaukumu uz: %2$s + %s veica video zvanu. + %s veica audio zvanu. + %s atbildēja zvanam. + %s beidza zvanu. + %1$s padarīja istabas nākamo ziņu vēsturi redzamu %2$s + visi istabas biedri no brīža, kad tika uzaicināti. + visi istabas biedri no brīža, kad tika pievienojušies. + visi istabas biedri. + ikviens. + nezināms (%s). + %1$s ieslēdza ierīce-ierīce šifrēšanu (%2$s) + + %1$s vēlas VoIP konferenci + VoIP konference sākusies + VoIP konference ir beigusies + + (arī profila attēls mainījās) + %1$s dzēsa istabas nosaukumu + %1$s dzēsa istabas tēmas nosaukumu + "rediģēts %1$s " + " no %1$s" + " [iemesls: %1$s]" + %1$s atjaunoja profila informāciju %2$s + %1$s nosūtīja uzaicinājumu %2$s pievienoties istabai + %1$s apstiprināja uzaicinājumu priekš %2$s + + ** Nav iespējams atkodēt: %s ** + Sūtītāja ierīce mums nenosūtīja atslēgas priekš šīs ziņas. + + Nevarēja rediģēt + Nav iespējams nosūtīt ziņu + + Neizdevās augšuplādēt attēlu + + Tīkla kļūda + Matrix kļūda + + Šobrīd nav iespējams atkārtoti pievienoties tukšai istabai. + + Šifrēta ziņa + + Epasta adrese + Telefona numurs + + diff --git a/matrix-sdk-android/src/main/res/values-nl/strings.xml b/matrix-sdk-android/src/main/res/values-nl/strings.xml new file mode 100644 index 0000000000..11f8e438d1 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-nl/strings.xml @@ -0,0 +1,85 @@ + + + Matrix Android SDK + + %1$s: %2$s + %1$s stuurde een afbeelding. + + %s\'s uitnodiging + %1$s nodigde %2$s uit + %1$s heeft jou uitgenodigd + %1$s is tot de ruimte toegetreden + %1$s heeft de ruimte verlaten + %1$s heeft de uitnodiging niet geaccepteerd + %1$s verwijderde %2$s + %1$s heeft %2$s ontbannen + %1$s heeft %2$s verbannen + %1$s heeft de uitnodiging van %2$s teruggetrokken + %1$s heeft zijn of haar avatar aangepast + %1$s heeft zijn of haar naam aangepast naar %2$s + %1$s heeft zijn of haar naam aangepast van %2$s naar %3$s + %1$s heeft zijn of haar naam verwijderd (%2$s) + %1$s heeft het onderwerp veranderd naar: %2$s + %1$s heeft de ruimtenaam veranderd naar: %2$s + %s heeft een video-oproep geplaatst. + %s heeft een spraak-oproep geplaatst. + %s heeft de oproep beantwoord. + %s heeft de oproep beëindigd. + %1$s heeft de toekomstige geschiedenis beschikbaar gemaakt voor %2$s + alle ruimte deelnemers, vanaf het punt dat ze zijn uitgenodigd. + alle ruimte deelnemers, vanaf het punt dat ze zijn toegetreden. + alle ruimte deelnemers. + Iedereen. + onbekend (%s). + %1$s heeft eind-tot-eind encryptie aangezet (%2$s) + + %1$s heeft een VoIP vergadering aangevraagd + VoIP vergadering gestart + VoIP vergadering gestopt + + (avatar was veranderd naar) + %1$s heeft de ruimtenaam verwijderd + %1$s heeft het ruimteonderwerp verwijderd + verdwijderd %1$s + door %1$s + [reden: %1$s] + %1$s heeft zijn of haar profiel %2$s geüpdatet + %1$s stuurde een uitnodiging naar %2$s om de ruimte toe te treden + %1$s accepteerde de uitnodiging voor %2$s + + ** Niet in staat tot het decoderen van: %s ** + De afzender\'s apparaat heeft geen sleutels voor dit bericht gestuurd. + + + Kon niet verwijderd worden + Niet in staat om het bericht te sturen + + Uploaden van de afbeelding mislukt + + + Netwerkfout + Matrix fout + + + + + + + Het is momenteel niet mogelijk om een lege ruimte opnieuw toe te treden. + + Versleuteld bericht + + + E-mailadres + Telefoonnummer + +%1$s heeft een sticker gestuurd. + + Als antwoord op + + verstuurde een plaatje. + verstuurde een video. + verstuurde een audiobestand. + verstuurde een bestand. + + diff --git a/matrix-sdk-android/src/main/res/values-nn/strings.xml b/matrix-sdk-android/src/main/res/values-nn/strings.xml new file mode 100644 index 0000000000..b9010f0ceb --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-nn/strings.xml @@ -0,0 +1,75 @@ + +Kryptert melding + + Matrix-Android-SDK + + %1$s: %2$s + %1$s sende eit bilete. + %1$s sende eit klistremerke. + + %s si innbyding + %1$s baud %2$s inn + %1$s baud deg inn + %1$s kom inn + %1$s fór ut + %1$s sa nei til innbydinga + %1$s sparka %2$s + %1$s slapp %2$s inn att + %1$s stengde %2$s ute + %1$s tok attende %2$s si innbyding + %1$s endra avataren sin + %1$s sette visingsnamnet sitt som %2$s + %1$s endra visingsnamnet sitt frå %2$s til %3$s + %1$s fjerna visingsnamnet sitt (%2$s) + %1$s endra emnet til: %2$s + %1$s endra romnamnet til: %2$s + %s starta ei videosamtale. + %s starta ei røystsamtale. + %s tok røret. + %s la på røret. + %1$s gjorde den framtidige romhistoria synleg for %2$s + alle rommedlemer, frå då dei vart bodne inn. + alle rommedlemer, frå då dei kom inn. + alle rommedlemer. + kven som helst. + ukjend (%s). + %1$s skrudde ende-til-ende-enkryptering på (%2$s) + + %1$s bad om ei VoIP-gruppesamtale + VoIP-gruppesamtala er starta + VoIP-gruppesamtala er ferdig + + (avataren vart endra òg) + %1$s fjerna romnamnet + %1$s fjerna romemnet + "gjorde om på %1$s " + " av %1$s" + " [grunnlag: %1$s]" + %1$s oppdaterte profilbiletet sitt %2$s + %1$s baud %2$s inn i rommet + %1$s sa ja til innbydinga til %2$s + + ** Klarte ikkje dekryptere: %s ** + Avsendareininga har ikkje sendt oss nyklane for denne meldinga. + + Som svar til + + Kunne ikkje gjera om + Klarte ikkje å senda meldinga + + Fekk ikkje til å lasta biletet opp + + Noko gjekk gale med nettverket + Noko gjekk gale med Matrix + + Det er førebels ikkje mogeleg å fara inn att i eit tomt rom. + + Epostadresse + Telefonnummer + + sende eit bilete. + sende ein video. + sende ein ljodfil. + sende ei fil. + + diff --git a/matrix-sdk-android/src/main/res/values-pl/strings.xml b/matrix-sdk-android/src/main/res/values-pl/strings.xml new file mode 100644 index 0000000000..9cdc88bd10 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-pl/strings.xml @@ -0,0 +1,44 @@ + +Matrix Android SDK + + %1$s: %2$s + %1$s wysłał zdjęcie. + + Zaproszenie od %s + %1$s zaprosił %2$s + %1$s zaprosił Cię + %1$s dołączył + %1$s wyszedł + %1$s odrzucił zaproszenie + %1$s wyrzucił %2$s + %1$s odblokował %2$s + %1$s zablokował %2$s + %1$s zmienił awatar + %1$s zmienił wyświetlaną nazwę na %2$s + %1$s zmienił wyświetlaną nazwę z %2$s na %3$s + %1$s usunął swoją wyświetlaną nazwę (%2$s) + %1$s zmienił temat na: %2$s + Nie udało się wysłać wiadomości + + Nie udało się wysłać zdjęcia + + ogólne błędy + Błąd Matrixa + + Wiadomość zaszyfrowana + + Adres e-mail + Numer telefonu + +wszyscy członkowie pokoju. + wszyscy. + %1$s zmienił znawę pokoju na: %2$s + %s zakończył rozmowę. + %1$s usunął nazwę pokoju + %1$s usunął temat pokoju + " [powód: %1$s]" + %1$s wysłał naklejkę. + + %1$s włączył szyfrowanie end-to-end (%2$s) + + diff --git a/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml b/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 0000000000..25e5739093 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,88 @@ + + + Matrix Android SDK + + %1$s: %2$s + %1$s enviou uma imagem. + + convite de %s + %1$s convidou %2$s + %1$s convidou você + %1$s entrou + %1$s saiu + %1$s recusou o convite + %1$s expulsou %2$s + %1$s des-baniu %2$s + %1$s baniu %2$s + %1$s cancelou o convite de %2$s + %1$s mudou seu avatar + %1$s definiu seu nome público como %2$s + %1$s alterou seu nome públido de %2$s para %3$s + %1$s apagou seu nome público (%2$s) + %1$s alterou o tópico desta sala para: %2$s + %1$s alterou o nome desta sala para: %2$s + %s iniciou uma chamada de vídeo. + %s iniciou uma chamada de voz. + %s respondeu à chamada. + %s encerrou a chamada. + %1$s deixou o histórico futuro desta sala visível para %2$s + todas as pessoas que integram esta sala, a partir do momento em que foram convidadas. + todas as pessoas que integram esta sala, a partir do momento em que entraram. + todas as pessoas que integram esta sala. + qualquer pessoa. + desconhedido (%s). + %1$s ativou a criptografia ponta-a-ponta (%2$s) + + %1$s solicitou uma conferência VoIP + A conferência VoIP começou + A conferência VoIP terminou + + (o avatar também foi alterado) + %1$s apagou o nome da sala + %1$s apagou o tópico da sala + removeu a mensagem %1$s + por %1$s + [razão: %1$s] + %1$s atualizou o seu perfil %2$s + %1$s enviou um convite para que %2$s se junte à sala + %1$s aceitou o convite para %2$s + + ** Impossível descriptografar: %s ** + O dispositivo de quem enviou a mensagem não nos enviou as chaves para esta mensagem. + + + Não foi possível apagar + Não foi possível enviar a mensagem + + O envio da imagem falhou + + + Erro de conexão à internet + Erro no servidor Matrix + + + + + + + + + Ainda não é possível voltar a entrar em uma sala vazia. + + Mensagem criptografada + + + Endereço de email + Número de telefone + + +%1$s enviou um sticker. + + Em resposta a + + enviou uma imagem. + enviou um vídeo. + enviou um arquivo de áudio. + enviou um arquivo. + + diff --git a/matrix-sdk-android/src/main/res/values-pt/strings.xml b/matrix-sdk-android/src/main/res/values-pt/strings.xml new file mode 100644 index 0000000000..6d8500a6a4 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-pt/strings.xml @@ -0,0 +1,79 @@ + + + Matrix Android SDK + + %1$s: %2$s + %1$s enviou uma imagem. + + convite de %s + %1$s convidou %2$s + %1$s convidou-o + %1$s entrou + %1$s saiu + %1$s recusou o convite + %1$s expulsou %2$s + %1$s des-baniu %2$s + %1$s baniu %2$s + %1$s cancelou o convite de %2$s + %1$s mudou o seu avatar + %1$s definiu seu nome público como %2$s + %1$s alterou seu nome público de %2$s para %3$s + %1$s apagou o seu nome público (%2$s) + %1$s alterou o tópico desta sala para: %2$s + %1$s alterou o nome desta sala para: %2$s + %s iniciou uma chamada de vídeo. + %s iniciou uma chamada de voz. + %s respondeu à chamada. + %s terminou a chamada. + %1$s tornou o histórico futuro desta sala visível para %2$s + todas os membros que integram esta sala, a partir do momento em que foram convidados. + todas os membros da sala, a partir do momento em que entraram. + todas os membros da sala. + todos. + desconhecida (%s). + %1$s ativou a criptografia ponta-a-ponta (%2$s) + + %1$s solicitou uma conferência VoIP + A conferência VoIP começou + A conferência VoIP terminou + + (o avatar também foi alterado) + %1$s removeu o nome da sala + %1$s removeu o tópico da sala + "apagou a mensagem %1$s " + por %1$s + [razão: %1$s] + %1$s atualizou o seu perfil %2$s + %1$s enviou um convite para que %2$s se junte à sala + %1$s aceitou o convite para %2$s + + ** Impossível decifrar: %s ** + O dispositivo de quem enviou a mensagem não nos enviou as chaves para esta mensagem. + + + Não foi possível apagar + Não foi possível enviar a mensagem + + O envio da imagem falhou + + + Erro de conexão à Internet + Erro do Matrix + + + + + + + + + Ainda não é possível voltar a entrar numa sala vazia. + + Mensagem cifrada + + + Endereço de e-mail + Número de telefone + + + diff --git a/matrix-sdk-android/src/main/res/values-ru/strings.xml b/matrix-sdk-android/src/main/res/values-ru/strings.xml new file mode 100644 index 0000000000..704ea394df --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-ru/strings.xml @@ -0,0 +1,87 @@ + + + Matrix Android SDK + + %1$s: %2$s + %1$s отправил(а) изображение. + + %s приглашение + %1$s пригласил(а) %2$s + %1$s пригласил(а) вас + %1$s присоединился(лась) + %1$s покинул(а) + %1$s отклонил(а) приглашение + %1$s выгнан %2$s + %1$s разблокировал(а) %2$s + %1$s заблокировал(а) %2$s + %1$s отозвал(а) приглашение %2$s + %1$s изменил(а) свой аватар + %1$s установил(а) имя %2$s + %1$s изменил(а) имя с %2$s на %3$s + %1$s удалил(а) свое имя (%2$s) + %1$s изменил(а) тему на: %2$s + %1$s изменил(а) название комнаты: %2$s + %s начал(а) видеовызов. + %s начал(а) голосовой вызов. + %s ответил(а) на звонок. + %s завершил(а) вызов. + %1$s сделал(а) будущую историю комнаты видимой %2$s + всем членам, с момента их приглашения. + всем членам, с момента присоединения. + всем членам. + всем. + неизвестно (%s). + %1$s включил(а) сквозное шифрование (%2$s) + + %1$s запросил(а) VoIP конференцию + VoIP-конференция начата + VoIP-конференция завершена + + (аватар также был изменен) + %1$s удалил(а) название комнаты + %1$s удалил(а) тему комнаты + "отредактировано %1$s " + %1$s + [причина: %1$s] + %1$s обновил(а) свой профиль %2$s + %1$s отправил(а) приглашение %2$s присоединиться к комнате + %1$s принял(а) приглашение от %2$s + + ** Невозможно расшифровать: %s ** + Устройство отправителя не предоставило нам ключ для расшифровки этого сообщения. + + + Не удалось изменить + Не удалось отправить сообщение + + Не удалось загрузить изображение + + + Сетевая ошибка + Ошибка Matrix + + + + + + + + + В настоящее время невозможно вновь присоединиться к пустой комнате. + + Зашифрованное сообщение + + + Адрес электронной почты + Номер телефона + + %1$s отправил стикер. + + В ответ на + + отправил изображение. + отправил видео. + отправил аудиофайл. + отправил файл. + + diff --git a/matrix-sdk-android/src/main/res/values-sk/strings.xml b/matrix-sdk-android/src/main/res/values-sk/strings.xml new file mode 100644 index 0000000000..fb98be0233 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-sk/strings.xml @@ -0,0 +1,76 @@ + +Matrix Android SDK + + %1$s: %2$s + %1$s poslal obrázok. + + Pozvanie %s + %1$s pozval %2$s + %1$s vás pozval + %1$s vstúpil + %1$s opustil + %1$s odmietol pozvanie + %1$s vykázal %2$s + %1$s povolil vstup %2$s + %1$s zakázal vstup %2$s + %1$s stiahol pozvanie %2$s + %1$s si zmenil obrázok v profile + %1$s si nastavil zobrazované meno %2$s + %1$s si zmenil zobrazované meno z %2$s na %3$s + %1$s odstránil svoje zobrazované meno (%2$s) + %1$s zmenil tému na: %2$s + %1$s zmenil názov miestnosti na: %2$s + %s uskutočnil video hovor. + %s uskutočnil audio hovor. + %s prijal hovor. + %s ukončil hovor. + %1$s sprístupnil budúcu históriu miestnosti %2$s + pre všetkých členov, od kedy boli pozvaní. + pre všetkých členov, od kedy vstúpili. + pre všetkých členov. + pre každého. + neznámym (%s). + %1$s povolil E2E šifrovanie (%2$s) + + %1$s požiadal o VoIP konferenciu + Začala VoIP konferencia + Skončila VoIP konferencia + + (a tiež obrázok v profile) + %1$s odstránil názov miestnosti + %1$s odstránil tému miestnosti + "zmazaná udalosť %1$s " + " používateľom %1$s" + " [dôvod: %1$s]" + %1$s aktualizoval svoj profil %2$s + %1$s pozval %2$s vstúpiť do miestnosti + %1$s prijal pozvanie do %2$s + + ** Nie je možné dešifrovať: %s ** + Zo zariadenia odosieľateľa nebolo možné získať kľúče potrebné na dešifrovanie tejto správy. + + Nie je ožné vymazať + Nie je možné odoslať správu + + Nepodarilo sa nahrať obrázok + + Chyba siete + Chyba Matrix + + V súčasnosti nie je možné znovu vstúpiť do prázdnej miestnosti. + + Šifrovaná správa + + Emailová adresa + Telefónne číslo + +%1$s poslal nálepku. + + Odpoveď na + + odoslal obrázok. + odoslal video. + odoslal zvukový súbor. + Odoslal súbor. + + diff --git a/matrix-sdk-android/src/main/res/values-sq/strings.xml b/matrix-sdk-android/src/main/res/values-sq/strings.xml new file mode 100644 index 0000000000..a6b3daec93 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-sq/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-te/strings.xml b/matrix-sdk-android/src/main/res/values-te/strings.xml new file mode 100644 index 0000000000..207e69c03f --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-te/strings.xml @@ -0,0 +1,67 @@ + +%s\'s ఆహ్వానం + %1$s ఆహ్వానించారు %2$s + %1$s వదిలి వెళారు + %1$s ఆహ్వానాన్ని తిరస్కరించారు + %1$s తన్నాడు %2$s + %1$s నిషేధాన్ని %2$s + %1$s నిషేధించారు %2$s + %1$s ఉపసంహరించుకుంది %2$s\'s ఆహ్వానం + %1$s వారి అవతార్ను మార్చారు + %1$s వారి డిస్ప్లే పేరును ని సెట్ చేసారు %2$s + %1$s వారి ప్రదర్శన పేరును %2$s నుండి %3$s మార్చారు + %1$s వారి ప్రదర్శన పేరుని తీసివేసారు (%2$s) + %1$s అంశం మార్చబడింది:%2$s + %1$s గది పెరు మార్చబడింది %2$s + %s ఒక వీడియో కాల్ని ఉంచింది. + %s వాయిస్ కాల్ని ఉంచారు. + %s కాల్కి సమాధానం ఇచ్చారు. + %s కాల్ ముగిసింది. + %1$s భవిష్యత్ గది చరిత్రను %2$s కి కనిపించేలా చేసింది + పాయింట్నుండి, అన్ని గది సభ్యుల వారు ఆహ్వానించబడ్డారు. + పాయింట్ నుండి, అన్ని గదుల సభ్యుల వారు చేరారు. + అన్ని గదుల సభ్యులు. + ఎవరైనా. + తెలియని (%s). + %1$s ఎండ్-టు-ఎండ్ ఎన్క్రిప్షన్ ఆన్ చెయ్యబడింది (%2$s) + + %1$s వి ఓ ఇ పి సమావేశాన్ని అభ్యర్థించారు + వి ఓ ఇ పి సమావేశం ప్రారంభమైంది + వి ఓ ఇ పి సమావేశం ముగిసింది + + (అవతార్ మార్చబడింది) + %1$s గది పేరు తొలగించబడింది + %1$s గది అంశాన్ని తీసివేసారు + "శోధించాడు %1$s " + " ద్వారా %1$s" + " [కారణం:%1$s]" + %1$s వారి ప్రొఫైల్ నవీకరించబడింది %2$s + %1$s గదిలో చేరడానికి %2$s కు ఆహ్వానాన్ని పంపారు + %2$sకోసం %1$s ఆహ్వానాన్ని అంగీకరించారు + + ** వ్యక్తీకరించడానికి సాధ్యం కాలేదు: %s ** + ఈ సందేశానికి పంపేవారి పరికరం మాకు కీలను పంపలేదు. + + గది స్క్రీన్ + సందేశం పంపడం సాధ్యం కాలేదు + + చిత్రాన్ని అప్లోడ్ చేయడంలో విఫలమైంది + + సాధారణ లోపాలు + మాట్రిక్స్ లోపం + + మళ్లీ ఖాళీ గది ని చేరడానికి ప్రస్తుతం ఇది సాధ్యం కాదు. + + ఎన్క్రిప్టెడ్ సందేశం + + ఇమెయిల్ చిరునామా + ఫోను నంబరు + +మ్యాట్రిక్స్ అండ్రొఇడ్ ఏస్ డి కె + + %1$s: %2$s + %1$s ఒక చిత్రం పంపారు. + + %1$s మిమ్మల్ని ఆహ్వానించారు + %1$s చేరారు + diff --git a/matrix-sdk-android/src/main/res/values-th/strings.xml b/matrix-sdk-android/src/main/res/values-th/strings.xml new file mode 100644 index 0000000000..d8374c668d --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-th/strings.xml @@ -0,0 +1,5 @@ + +Matrix Android SDK + + %1$s: %2$s + diff --git a/matrix-sdk-android/src/main/res/values-uk/strings.xml b/matrix-sdk-android/src/main/res/values-uk/strings.xml new file mode 100644 index 0000000000..24cf674461 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-uk/strings.xml @@ -0,0 +1,11 @@ + +Matrix Android SDK + + %1$s: %2$s + %1$s відправив зображення. + + %s запрошення + %1$s запросив(ла) %2$s + Закодоване повідомлення + + diff --git a/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000000..5d5edf5c41 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,76 @@ + +%1$s 发送了一张图片。 + + %s 的邀请 + %1$s 邀请了 %2$s + %1$s 邀请了您 + %1$s 加入了聊天室 + %1$s 退出了聊天室 + %1$s 拒绝了邀请 + %1$s 移除了 %2$s + %1$s 解封了 %2$s + %1$s 封禁了 %2$s + %1$s 更换了他们的头像 + %1$s 将他们的昵称设置为 %2$s + %1$s 把他们的昵称从 %2$s 改为 %3$s + %1$s 移除了他们的昵称 (%2$s) + %1$s 把主题改为: %2$s + %1$s 把聊天室名称改为: %2$s + %s 发起了一次视频通话。 + %s 发起了一次语音通话。 + %s 已接听通话。 + %s 已结束通话。 + 所有聊天室成员,从他们被邀请开始。 + 所有聊天室成员,从他们加入开始。 + 所有聊天室成员。 + 任何人。 + 未知(%s)。 + %1$s 开启了端对端加密(%2$s) + + %1$s 请求了一次 VoIP 会议 + VoIP 会议已开始 + VoIP 会议已结束 + + (头像也被更改) + %1$s 移除了聊天室名称 + %1$s 移除了聊天室主题 + " [理由:%1$s]" + ** 无法解密:%s ** + 发送者的设备没有向我们发送此消息的密钥。 + + 无法发送消息 + + 上传图像失败 + + 网络错误 + Matrix 错误 + + 目前无法重新加入一个空的聊天室。 + + 已加密消息 + + 邮箱地址 + 电话号码 + +%1$s 撤销了对 %2$s 的邀请 + %1$s 让之后的聊天室历史记录对 %2$s 可见 + "删改了 %1$s " + " 被 %1$s" + %1$s 更新了他们的配置文件 %2$s + %1$s 向 %2$s 发送了加入聊天室的邀请 + %1$s 接受了 %2$s 的邀请 + + 无法撤回 + Matrix Android SDK + + %1$s:%2$s + %1$s 发送了一张贴纸。 + + 发送了一张图片。 + 发送了一个视频。 + 发送了一段音频。 + 发送了一个文件。 + +回复 + + diff --git a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000000..516201cbf8 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,76 @@ + +Matrix Android 軟體開發工具 + + %1$s:%2$s + %1$s 傳送了一張圖片。 + + %s 的邀請 + %1$s 邀請了 %2$s + %1$s 邀請您 + %1$s 已加入 + %1$s 已離開 + %1$s 拒絕邀請 + %1$s 踢出 %2$s + %1$s 解除禁止 %2$s + %1$s 禁止 %2$s + %1$s 收回了對 %2$s 的邀請 + %1$s 變更了他們的大頭貼 + %1$s 設定了他們的顯示名稱為 %2$s + %1$s 變更了他們的顯示名稱從 %2$s 到 %3$s + %1$s 移除了他們的顯示名稱 (%2$s) + %1$s 變更主題為:%2$s + %1$s 變更房間名稱為:%2$s + %s 撥出了視訊通話。 + %s 撥出了語音通話。 + %s 回覆了通話。 + %s 結束通話。 + %1$s 讓房間未來可讓 %2$s 看到歷史紀錄 + 所有的房間成員,從他們被邀請的時間開始。 + 所有的房間成員,從他們加入的時間開始。 + 所有的房間成員。 + 任何人。 + 未知 (%s)。 + %1$s 開啟了端對端加密 (%2$s) + + %1$s 請求了 VoIP 會議通話 + VoIP 會議通話已開始 + VoIP 會議通話已結束 + + (大頭貼也變更了) + %1$s 移除了房間名稱 + %1$s 移除了房間主題 + "已編輯 %1$s " + " 由 %1$s" + " [理由:%1$s]" + %1$s 更新了他們的基本資料 %2$s + %1$s 傳送加入房間的邀請給 %2$s + %1$s 接受 %2$s 的邀請 + + ** 無法解密:%s ** + 傳送者的裝置並未在此訊息傳送他們的金鑰。 + + 無法編輯 + 無法傳送訊息 + + 上傳圖片失敗 + + 網路錯誤 + Matrix 錯誤 + + 目前無法重新加入空房間。 + + 已加密的訊息 + + 電子郵件 + 電話號碼 + +%1$s 傳送了一張貼圖。 + + 回覆 + + 傳送了圖片。 + 傳送了影片。 + 傳送了音訊檔案。 + 傳送了檔案。 + + diff --git a/matrix-sdk-android/src/main/res/values/dimens.xml b/matrix-sdk-android/src/main/res/values/dimens.xml new file mode 100644 index 0000000000..f11f7450a8 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values/dimens.xml @@ -0,0 +1,3 @@ + + + diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml index e13f2d6a26..28e684ed83 100644 --- a/matrix-sdk-android/src/main/res/values/strings.xml +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -1,3 +1,87 @@ - matrix-sdk-android + Matrix Android SDK + + %1$s: %2$s + %1$s sent an image. + %1$s sent a sticker. + + %s\'s invitation + %1$s invited %2$s + %1$s invited you + %1$s joined + %1$s left + %1$s rejected the invitation + %1$s kicked %2$s + %1$s unbanned %2$s + %1$s banned %2$s + %1$s withdrew %2$s\'s invitation + %1$s changed their avatar + %1$s set their display name to %2$s + %1$s changed their display name from %2$s to %3$s + %1$s removed their display name (%2$s) + %1$s changed the topic to: %2$s + %1$s changed the room name to: %2$s + %s placed a video call. + %s placed a voice call. + %s answered the call. + %s ended the call. + %1$s made future room history visible to %2$s + all room members, from the point they are invited. + all room members, from the point they joined. + all room members. + anyone. + unknown (%s). + %1$s turned on end-to-end encryption (%2$s) + + %1$s requested a VoIP conference + VoIP conference started + VoIP conference finished + + (avatar was changed too) + %1$s removed the room name + %1$s removed the room topic + redacted %1$s + by %1$s + [reason: %1$s] + %1$s updated their profile %2$s + %1$s sent an invitation to %2$s to join the room + %1$s accepted the invitation for %2$s + + ** Unable to decrypt: %s ** + The sender\'s device has not sent us the keys for this message. + + + In reply to + + + Could not redact + Unable to send message + + Failed to upload image + + + Network error + Matrix error + + + + + + + + + It is not currently possible to re-join an empty room. + + Encrypted message + + + Email address + Phone number + + + sent an image. + sent a video. + sent an audio file. + sent a file. +