Import old SDK as legacy code to replace smoothly

This commit is contained in:
ganfra 2018-10-09 12:30:01 +02:00
parent 058f1704fa
commit 3215fa47d5
400 changed files with 72167 additions and 551 deletions

View file

@ -44,7 +44,6 @@ class HomeActivity : RiotActivity() {
})
}
companion object {
fun newIntent(context: Context): Intent {

View file

@ -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"

Binary file not shown.

Binary file not shown.

View file

@ -1,2 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="im.vector.matrix.android" />
package="im.vector.matrix.android" >
<uses-permission android:name="android.permission.VIBRATE" />
</manifest>

View file

@ -1,3 +0,0 @@
package im.vector.matrix.android.api.events
class EventContent : HashMap<Any, Any>()

View file

@ -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"

View file

@ -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

View file

@ -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()

View file

@ -1,538 +0,0 @@
/*
* Copyright 2014 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*//*
package im.vector.matrix.android.internal.events.sync
import android.text.TextUtils
import android.util.Log
import im.vector.matrix.android.api.events.Event
import im.vector.matrix.android.api.events.EventType
import im.vector.matrix.android.internal.events.sync.data.SyncResponse
import java.util.*
class SyncResponseHandler {
*/
/**
* Manage the sync accountData field
*
* @param accountData the account data
* @param isInitialSync true if it is an initial sync response
*//*
private fun manageAccountData(accountData: Map<String, Any>?, isInitialSync: Boolean) {
try {
if (accountData!!.containsKey("events")) {
val events = accountData["events"] as List<Map<String, Any>>
if (!events.isEmpty()) {
// ignored users list
manageIgnoredUsers(events, isInitialSync)
// push rules
managePushRulesUpdate(events)
// direct messages rooms
manageDirectChatRooms(events, isInitialSync)
// URL preview
manageUrlPreview(events)
// User widgets
manageUserWidgets(events)
}
}
} catch (e: Exception) {
}
}
*/
/**
* Refresh the push rules from the account data events list
*
* @param events the account data events.
*//*
private fun managePushRulesUpdate(events: List<Map<String, Any>>) {
for (event in events) {
val type = event["type"] as String
if (TextUtils.equals(type, "m.push_rules")) {
if (event.containsKey("content")) {
val gson = JsonUtils.getGson(false)
// convert the data to PushRulesResponse
// because BingRulesManager supports only PushRulesResponse
val element = gson.toJsonTree(event["content"])
getBingRulesManager().buildRules(gson.fromJson(element, PushRulesResponse::class.java))
// warn the client that the push rules have been updated
onBingRulesUpdate()
}
return
}
}
}
*/
/**
* Check if the ignored users list is updated
*
* @param events the account data events list
*//*
private fun manageIgnoredUsers(events: List<Map<String, Any>>, isInitialSync: Boolean) {
val newIgnoredUsers = ignoredUsers(events)
if (null != newIgnoredUsers) {
val curIgnoredUsers = getIgnoredUserIds()
// the both lists are not empty
if (0 != newIgnoredUsers.size || 0 != curIgnoredUsers.size) {
// check if the ignored users list has been updated
if (newIgnoredUsers.size != curIgnoredUsers.size || !newIgnoredUsers.containsAll(curIgnoredUsers)) {
// update the store
mStore.setIgnoredUserIdsList(newIgnoredUsers)
mIgnoredUserIdsList = newIgnoredUsers
if (!isInitialSync) {
// warn there is an update
onIgnoredUsersListUpdate()
}
}
}
}
}
*/
/**
* Extract the ignored users list from the account data events list..
*
* @param events the account data events list.
* @return the ignored users list. null means that there is no defined user ids list.
*//*
private fun ignoredUsers(events: List<Map<String, Any>>): List<String>? {
var ignoredUsers: List<String>? = null
if (0 != events.size) {
for (event in events) {
val type = event["type"] as String
if (TextUtils.equals(type, AccountDataRestClient.ACCOUNT_DATA_TYPE_IGNORED_USER_LIST)) {
if (event.containsKey("content")) {
val contentDict = event["content"] as Map<String, Any>
if (contentDict.containsKey(AccountDataRestClient.ACCOUNT_DATA_KEY_IGNORED_USERS)) {
val ignored_users = contentDict[AccountDataRestClient.ACCOUNT_DATA_KEY_IGNORED_USERS] as Map<String, Any>
if (null != ignored_users) {
ignoredUsers = ArrayList(ignored_users.keys)
}
}
}
}
}
}
return ignoredUsers
}
*/
/**
* Extract the direct chat rooms list from the dedicated events.
*
* @param events the account data events list.
*//*
private fun manageDirectChatRooms(events: List<Map<String, Any>>, isInitialSync: Boolean) {
if (0 != events.size) {
for (event in events) {
val type = event["type"] as String
if (TextUtils.equals(type, AccountDataRestClient.ACCOUNT_DATA_TYPE_DIRECT_MESSAGES)) {
if (event.containsKey("content")) {
val contentDict = event["content"] as Map<String, List<String>>
Log.d(LOG_TAG, "## manageDirectChatRooms() : update direct chats map$contentDict")
mStore.setDirectChatRoomsDict(contentDict)
// reset the current list of the direct chat roomIDs
// to update it
mLocalDirectChatRoomIdsList = null
if (!isInitialSync) {
// warn there is an update
onDirectMessageChatRoomsListUpdate()
}
}
}
}
}
}
*/
/**
* Manage the URL preview flag
*
* @param events the events list
*//*
private fun manageUrlPreview(events: List<Map<String, Any>>) {
if (0 != events.size) {
for (event in events) {
val type = event["type"] as String
if (TextUtils.equals(type, AccountDataRestClient.ACCOUNT_DATA_TYPE_PREVIEW_URLS)) {
if (event.containsKey("content")) {
val contentDict = event["content"] as Map<String, Any>
Log.d(LOG_TAG, "## manageUrlPreview() : $contentDict")
var enable = true
if (contentDict.containsKey(AccountDataRestClient.ACCOUNT_DATA_KEY_URL_PREVIEW_DISABLE)) {
enable = !(contentDict[AccountDataRestClient.ACCOUNT_DATA_KEY_URL_PREVIEW_DISABLE] as Boolean)
}
mStore.setURLPreviewEnabled(enable)
}
}
}
}
}
*/
/**
* Manage the user widgets
*
* @param events the events list
*//*
private fun manageUserWidgets(events: List<Map<String, Any>>) {
if (0 != events.size) {
for (event in events) {
val type = event["type"] as String
if (TextUtils.equals(type, AccountDataRestClient.ACCOUNT_DATA_TYPE_WIDGETS)) {
if (event.containsKey("content")) {
val contentDict = event["content"] as Map<String, Any>
Log.d(LOG_TAG, "## manageUserWidgets() : $contentDict")
mStore.setUserWidgets(contentDict)
}
}
}
}
}
//================================================================================
// Sync V2
//================================================================================
*/
/**
* Handle a presence event.
*
* @param presenceEvent the presence event.
*//*
private fun handlePresenceEvent(presenceEvent: Event) {
// Presence event
if (presenceEvent.type == EventType.PRESENCE) {
val userPresence = presenceEvent.content<>()
// use the sender by default
if (!TextUtils.isEmpty(presenceEvent.sender)) {
userPresence.user_id = presenceEvent.sender
}
var user = mStore.getUser(userPresence.user_id)
if (user == null) {
user = userPresence
user!!.setDataHandler(this)
} else {
user!!.currently_active = userPresence.currently_active
user!!.presence = userPresence.presence
user!!.lastActiveAgo = userPresence.lastActiveAgo
}
user!!.setLatestPresenceTs(System.currentTimeMillis())
// check if the current user has been updated
if (mCredentials.userId.equals(user!!.user_id)) {
// always use the up-to-date information
getMyUser().displayname = user!!.displayname
getMyUser().avatar_url = user!!.getAvatarUrl()
mStore.setAvatarURL(user!!.getAvatarUrl(), presenceEvent.getOriginServerTs())
mStore.setDisplayName(user!!.displayname, presenceEvent.getOriginServerTs())
}
mStore.storeUser(user)
onPresenceUpdate(presenceEvent, user)
}
}
private fun manageResponse(syncResponse: SyncResponse?, fromToken: String?, isCatchingUp: Boolean) {
val isInitialSync = fromToken == null
var isEmptyResponse = true
if (syncResponse == null) {
return
}
// Handle the to device events before the room ones
// to ensure to decrypt them properly
if (syncResponse.toDevice?.events?.isNotEmpty() == true) {
for (toDeviceEvent in syncResponse.toDevice.events) {
handleToDeviceEvent(toDeviceEvent)
}
}
// Handle account data before the room events
// to be able to update direct chats dictionary during invites handling.
manageAccountData(syncResponse.accountData, isInitialSync)
// joined rooms events
if (syncResponse.rooms?.join?.isNotEmpty() == true) {
Log.d(LOG_TAG, "Received " + syncResponse.rooms.join.size + " joined rooms")
val roomIds = syncResponse.rooms.join.keys
// Handle first joined rooms
for (roomId in roomIds) {
try {
if (null != mLeftRoomsStore.getRoom(roomId)) {
Log.d(LOG_TAG, "the room $roomId moves from left to the joined ones")
mLeftRoomsStore.deleteRoom(roomId)
}
getRoom(roomId).handleJoinedRoomSync(syncResponse.rooms.join[roomId], isInitialSync)
} catch (e: Exception) {
Log.e(LOG_TAG, "## manageResponse() : handleJoinedRoomSync failed " + e.message + " for room " + roomId, e)
}
}
isEmptyResponse = false
}
// invited room management
if (syncResponse.rooms?.invite?.isNotEmpty() == true) {
Log.d(LOG_TAG, "Received " + syncResponse.rooms.invite.size + " invited rooms")
val roomIds = syncResponse.rooms.invite.keys
var updatedDirectChatRoomsDict: MutableMap<String, List<String>>? = null
var hasChanged = false
for (roomId in roomIds) {
try {
Log.d(LOG_TAG, "## manageResponse() : the user has been invited to $roomId")
val room = getRoom(roomId)
val invitedRoomSync = syncResponse.rooms.invite[roomId]
room.handleInvitedRoomSync(invitedRoomSync)
// Handle here the invites to a direct chat.
if (room.isDirectChatInvitation()) {
// Retrieve the inviter user id.
var participantUserId: String? = null
for (event in invitedRoomSync.inviteState.events) {
if (null != event.sender) {
participantUserId = event.sender
break
}
}
if (null != participantUserId) {
// Prepare the updated dictionary.
if (null == updatedDirectChatRoomsDict) {
if (null != getStore().getDirectChatRoomsDict()) {
// Consider the current dictionary.
updatedDirectChatRoomsDict = HashMap(getStore().getDirectChatRoomsDict())
} else {
updatedDirectChatRoomsDict = HashMap()
}
}
val roomIdsList: MutableList<String>
if (updatedDirectChatRoomsDict!!.containsKey(participantUserId)) {
roomIdsList = ArrayList(updatedDirectChatRoomsDict[participantUserId])
} else {
roomIdsList = ArrayList()
}
// Check whether the room was not yet seen as direct chat
if (roomIdsList.indexOf(roomId) < 0) {
Log.d(LOG_TAG, "## manageResponse() : add this new invite in direct chats")
roomIdsList.add(roomId) // update room list with the new room
updatedDirectChatRoomsDict[participantUserId] = roomIdsList
hasChanged = true
}
}
}
} catch (e: Exception) {
Log.e(LOG_TAG, "## manageResponse() : handleInvitedRoomSync failed " + e.message + " for room " + roomId, e)
}
}
isEmptyResponse = false
if (hasChanged) {
// Update account data to add new direct chat room(s)
mAccountDataRestClient.setAccountData(mCredentials.userId, AccountDataRestClient.ACCOUNT_DATA_TYPE_DIRECT_MESSAGES,
updatedDirectChatRoomsDict, object : ApiCallback<Void>() {
fun onSuccess(info: Void) {
Log.d(LOG_TAG, "## manageResponse() : succeeds")
}
fun onNetworkError(e: Exception) {
Log.e(LOG_TAG, "## manageResponse() : update account data failed " + e.message, e)
// TODO: we should try again.
}
fun onMatrixError(e: MatrixError) {
Log.e(LOG_TAG, "## manageResponse() : update account data failed " + e.getMessage())
}
fun onUnexpectedError(e: Exception) {
Log.e(LOG_TAG, "## manageResponse() : update account data failed " + e.message, e)
}
})
}
// left room management
// it should be done at the end but it seems there is a server issue
// when inviting after leaving a room, the room is defined in the both leave & invite rooms list.
if (null != syncResponse.rooms.leave && syncResponse.rooms.leave.size > 0) {
Log.d(LOG_TAG, "Received " + syncResponse.rooms.leave.size + " left rooms")
val roomIds = syncResponse.rooms.leave.keys
for (roomId in roomIds) {
// RoomSync leftRoomSync = syncResponse.rooms.leave.get(roomId);
// Presently we remove the existing room from the rooms list.
// FIXME SYNC V2 Archive/Display the left rooms!
// For that create 'handleArchivedRoomSync' method
var membership = RoomMember.MEMBERSHIP_LEAVE
val room = getRoom(roomId)
// Retrieve existing room
// check if the room still exists.
if (null != room) {
// use 'handleJoinedRoomSync' to pass the last events to the room before leaving it.
// The room will then be able to notify its listeners.
room!!.handleJoinedRoomSync(syncResponse.rooms.leave[roomId], isInitialSync)
val member = room!!.getMember(getUserId())
if (null != member) {
membership = member!!.membership
}
Log.d(LOG_TAG, "## manageResponse() : leave the room $roomId")
}
if (!TextUtils.equals(membership, RoomMember.MEMBERSHIP_KICK) && !TextUtils.equals(membership, RoomMember.MEMBERSHIP_BAN)) {
// ensure that the room data are properly deleted
getStore().deleteRoom(roomId)
onLeaveRoom(roomId)
} else {
onRoomKick(roomId)
}
// don't add to the left rooms if the user has been kicked / banned
if (mAreLeftRoomsSynced && TextUtils.equals(membership, RoomMember.MEMBERSHIP_LEAVE)) {
val leftRoom = getRoom(mLeftRoomsStore, roomId, true)
leftRoom.handleJoinedRoomSync(syncResponse.rooms.leave[roomId], isInitialSync)
}
}
isEmptyResponse = false
}
}
// groups
if (null != syncResponse.groups) {
// Handle invited groups
if (null != syncResponse.groups.invite && !syncResponse.groups.invite.isEmpty()) {
// Handle invited groups
for (groupId in syncResponse.groups.invite.keySet()) {
val invitedGroupSync = syncResponse.groups.invite.get(groupId)
mGroupsManager.onNewGroupInvitation(groupId, invitedGroupSync.profile, invitedGroupSync.inviter, !isInitialSync)
}
}
// Handle joined groups
if (null != syncResponse.groups.join && !syncResponse.groups.join.isEmpty()) {
for (groupId in syncResponse.groups.join.keySet()) {
mGroupsManager.onJoinGroup(groupId, !isInitialSync)
}
}
// Handle left groups
if (null != syncResponse.groups.leave && !syncResponse.groups.leave.isEmpty()) {
// Handle joined groups
for (groupId in syncResponse.groups.leave.keySet()) {
mGroupsManager.onLeaveGroup(groupId, !isInitialSync)
}
}
}
// Handle presence of other users
if (null != syncResponse.presence && null != syncResponse.presence.events) {
Log.d(LOG_TAG, "Received " + syncResponse.presence.events.size + " presence events")
for (presenceEvent in syncResponse.presence.events) {
handlePresenceEvent(presenceEvent)
}
}
if (null != mCrypto) {
mCrypto.onSyncCompleted(syncResponse, fromToken, isCatchingUp)
}
val store = getStore()
if (!isEmptyResponse && null != store) {
store!!.setEventStreamToken(syncResponse.nextBatch)
store!!.commit()
}
}
*/
/*
* Handle a 'toDevice' event
* @param event the event
*//*
private fun handleToDeviceEvent(event: Event) {
// Decrypt event if necessary
decryptEvent(event, null)
if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_MESSAGE)
&& null != event.getContent()
&& TextUtils.equals(JsonUtils.getMessageMsgType(event.getContent()), "m.bad.encrypted")) {
Log.e(LOG_TAG, "## handleToDeviceEvent() : Warning: Unable to decrypt to-device event : " + event.getContent())
} else {
onToDeviceEvent(event)
}
}
companion object {
private val LOG_TAG = MXDataHandler::class.java!!.getSimpleName()
private val LEFT_ROOMS_FILTER = "{\"room\":{\"timeline\":{\"limit\":1},\"include_leave\":true}}"
}
}
*/

View file

@ -4,6 +4,8 @@ import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.events.sync.data.SyncResponse
import im.vector.matrix.android.internal.legacy.rest.model.filter.FilterBody
import im.vector.matrix.android.internal.legacy.util.FilterUtil
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.util.CancelableCoroutine
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
@ -17,8 +19,10 @@ class Synchronizer(private val syncAPI: SyncAPI,
fun synchronize(callback: MatrixCallback<SyncResponse>): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
val params = HashMap<String, String>()
val filterBody = FilterBody()
FilterUtil.enableLazyLoading(filterBody, true)
params["timeout"] = "0"
params["filter"] = "{}"
params["filter"] = filterBody.toJSONString()
val syncResponse = executeRequest<SyncResponse> {
apiCall = syncAPI.sync(params)
moshi = jsonMapper

View file

@ -0,0 +1,532 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import im.vector.matrix.android.internal.legacy.rest.model.login.Credentials;
import im.vector.matrix.android.internal.legacy.ssl.Fingerprint;
import java.util.ArrayList;
import java.util.List;
import okhttp3.CipherSuite;
import okhttp3.TlsVersion;
/**
* Represents how to connect to a specific Homeserver, may include credentials to use.
*/
public class HomeServerConnectionConfig {
// the home server URI
private Uri mHsUri;
// the identity server URI
private Uri mIdentityServerUri;
// the anti-virus server URI
private Uri mAntiVirusServerUri;
// allowed fingerprints
private List<Fingerprint> mAllowedFingerprints = new ArrayList<>();
// the credentials
private Credentials mCredentials;
// tell whether we should reject X509 certs that were issued by trusts CAs and only trustcerts with matching fingerprints.
private boolean mPin;
// the accepted TLS versions
private List<TlsVersion> mTlsVersions;
// the accepted TLS cipher suites
private List<CipherSuite> mTlsCipherSuites;
// should accept TLS extensions
private boolean mShouldAcceptTlsExtensions = true;
// allow Http connection
private boolean mAllowHttpExtension;
// Force usage of TLS versions
private boolean mForceUsageTlsVersions;
/**
* Private constructor. Please use the Builder
*/
private HomeServerConnectionConfig() {
// Private constructor
}
/**
* Update the home server URI.
*
* @param uri the new HS uri
*/
public void setHomeserverUri(Uri uri) {
mHsUri = uri;
}
/**
* @return the home server uri
*/
public Uri getHomeserverUri() {
return mHsUri;
}
/**
* @return the identity server uri
*/
public Uri getIdentityServerUri() {
if (null != mIdentityServerUri) {
return mIdentityServerUri;
}
// Else consider the HS uri by default.
return mHsUri;
}
/**
* @return the anti-virus server uri
*/
public Uri getAntiVirusServerUri() {
if (null != mAntiVirusServerUri) {
return mAntiVirusServerUri;
}
// Else consider the HS uri by default.
return mHsUri;
}
/**
* @return the allowed fingerprints.
*/
public List<Fingerprint> getAllowedFingerprints() {
return mAllowedFingerprints;
}
/**
* @return the credentials
*/
public Credentials getCredentials() {
return mCredentials;
}
/**
* Update the credentials.
*
* @param credentials the new credentials
*/
public void setCredentials(Credentials credentials) {
mCredentials = credentials;
}
/**
* @return whether we should reject X509 certs that were issued by trusts CAs and only trust
* certs with matching fingerprints.
*/
public boolean shouldPin() {
return mPin;
}
/**
* TLS versions accepted for TLS connections with the home server.
*/
@Nullable
public List<TlsVersion> getAcceptedTlsVersions() {
return mTlsVersions;
}
/**
* TLS cipher suites accepted for TLS connections with the home server.
*/
@Nullable
public List<CipherSuite> getAcceptedTlsCipherSuites() {
return mTlsCipherSuites;
}
/**
* @return whether we should accept TLS extensions.
*/
public boolean shouldAcceptTlsExtensions() {
return mShouldAcceptTlsExtensions;
}
/**
* @return true if Http connection is allowed (false by default).
*/
public boolean isHttpConnectionAllowed() {
return mAllowHttpExtension;
}
/**
* @return true if the usage of TlsVersions has to be forced
*/
public boolean forceUsageOfTlsVersions() {
return mForceUsageTlsVersions;
}
@Override
public String toString() {
return "HomeserverConnectionConfig{" +
"mHsUri=" + mHsUri +
", mIdentityServerUri=" + mIdentityServerUri +
", mAntiVirusServerUri=" + mAntiVirusServerUri +
", mAllowedFingerprints size=" + mAllowedFingerprints.size() +
", mCredentials=" + mCredentials +
", mPin=" + mPin +
", mShouldAcceptTlsExtensions=" + mShouldAcceptTlsExtensions +
", mTlsVersions=" + (null == mTlsVersions ? "" : mTlsVersions.size()) +
", mTlsCipherSuites=" + (null == mTlsCipherSuites ? "" : mTlsCipherSuites.size()) +
'}';
}
/**
* Convert the object instance into a JSon object
*
* @return the JSon representation
* @throws JSONException the JSON conversion failure reason
*/
public JSONObject toJson() throws JSONException {
JSONObject json = new JSONObject();
json.put("home_server_url", mHsUri.toString());
json.put("identity_server_url", getIdentityServerUri().toString());
if (mAntiVirusServerUri != null) {
json.put("antivirus_server_url", mAntiVirusServerUri.toString());
}
json.put("pin", mPin);
if (mCredentials != null) json.put("credentials", mCredentials.toJson());
if (mAllowedFingerprints != null) {
List<JSONObject> fingerprints = new ArrayList<>(mAllowedFingerprints.size());
for (Fingerprint fingerprint : mAllowedFingerprints) {
fingerprints.add(fingerprint.toJson());
}
json.put("fingerprints", new JSONArray(fingerprints));
}
json.put("tls_extensions", mShouldAcceptTlsExtensions);
if (mTlsVersions != null) {
List<String> tlsVersions = new ArrayList<>(mTlsVersions.size());
for (TlsVersion tlsVersion : mTlsVersions) {
tlsVersions.add(tlsVersion.javaName());
}
json.put("tls_versions", new JSONArray(tlsVersions));
}
json.put("force_usage_of_tls_versions", mForceUsageTlsVersions);
if (mTlsCipherSuites != null) {
List<String> tlsCipherSuites = new ArrayList<>(mTlsCipherSuites.size());
for (CipherSuite tlsCipherSuite : mTlsCipherSuites) {
tlsCipherSuites.add(tlsCipherSuite.javaName());
}
json.put("tls_cipher_suites", new JSONArray(tlsCipherSuites));
}
return json;
}
/**
* Create an object instance from the json object.
*
* @param jsonObject the json object
* @return a HomeServerConnectionConfig instance
* @throws JSONException the conversion failure reason
*/
public static HomeServerConnectionConfig fromJson(JSONObject jsonObject) throws JSONException {
JSONArray fingerprintArray = jsonObject.optJSONArray("fingerprints");
List<Fingerprint> fingerprints = new ArrayList<>();
if (fingerprintArray != null) {
for (int i = 0; i < fingerprintArray.length(); i++) {
fingerprints.add(Fingerprint.fromJson(fingerprintArray.getJSONObject(i)));
}
}
JSONObject credentialsObj = jsonObject.optJSONObject("credentials");
Credentials creds = credentialsObj != null ? Credentials.fromJson(credentialsObj) : null;
Builder builder = new Builder()
.withHomeServerUri(Uri.parse(jsonObject.getString("home_server_url")))
.withIdentityServerUri(jsonObject.has("identity_server_url") ? Uri.parse(jsonObject.getString("identity_server_url")) : null)
.withCredentials(creds)
.withAllowedFingerPrints(fingerprints)
.withPin(jsonObject.optBoolean("pin", false));
// Set the anti-virus server uri if any
if (jsonObject.has("antivirus_server_url")) {
builder.withAntiVirusServerUri(Uri.parse(jsonObject.getString("antivirus_server_url")));
}
builder.withShouldAcceptTlsExtensions(jsonObject.optBoolean("tls_extensions", true));
// Set the TLS versions if any
if (jsonObject.has("tls_versions")) {
JSONArray tlsVersionsArray = jsonObject.optJSONArray("tls_versions");
if (tlsVersionsArray != null) {
for (int i = 0; i < tlsVersionsArray.length(); i++) {
builder.addAcceptedTlsVersion(TlsVersion.forJavaName(tlsVersionsArray.getString(i)));
}
}
}
builder.forceUsageOfTlsVersions(jsonObject.optBoolean("force_usage_of_tls_versions", false));
// Set the TLS cipher suites if any
if (jsonObject.has("tls_cipher_suites")) {
JSONArray tlsCipherSuitesArray = jsonObject.optJSONArray("tls_cipher_suites");
if (tlsCipherSuitesArray != null) {
for (int i = 0; i < tlsCipherSuitesArray.length(); i++) {
builder.addAcceptedTlsCipherSuite(CipherSuite.forJavaName(tlsCipherSuitesArray.getString(i)));
}
}
}
return builder.build();
}
/**
* Builder
*/
public static class Builder {
private HomeServerConnectionConfig mHomeServerConnectionConfig;
/**
* Builder constructor
*/
public Builder() {
mHomeServerConnectionConfig = new HomeServerConnectionConfig();
}
/**
* @param hsUri The URI to use to connect to the homeserver. Cannot be null
* @return this builder
*/
public Builder withHomeServerUri(final Uri hsUri) {
if (hsUri == null || (!"http".equals(hsUri.getScheme()) && !"https".equals(hsUri.getScheme()))) {
throw new RuntimeException("Invalid home server URI: " + hsUri);
}
// remove trailing /
if (hsUri.toString().endsWith("/")) {
try {
String url = hsUri.toString();
mHomeServerConnectionConfig.mHsUri = Uri.parse(url.substring(0, url.length() - 1));
} catch (Exception e) {
throw new RuntimeException("Invalid home server URI: " + hsUri);
}
} else {
mHomeServerConnectionConfig.mHsUri = hsUri;
}
return this;
}
/**
* @param identityServerUri The URI to use to manage identity. Can be null
* @return this builder
*/
public Builder withIdentityServerUri(@Nullable final Uri identityServerUri) {
if ((null != identityServerUri) && (!"http".equals(identityServerUri.getScheme()) && !"https".equals(identityServerUri.getScheme()))) {
throw new RuntimeException("Invalid identity server URI: " + identityServerUri);
}
// remove trailing /
if ((null != identityServerUri) && identityServerUri.toString().endsWith("/")) {
try {
String url = identityServerUri.toString();
mHomeServerConnectionConfig.mIdentityServerUri = Uri.parse(url.substring(0, url.length() - 1));
} catch (Exception e) {
throw new RuntimeException("Invalid identity server URI: " + identityServerUri);
}
} else {
mHomeServerConnectionConfig.mIdentityServerUri = identityServerUri;
}
return this;
}
/**
* @param credentials The credentials to use, if needed. Can be null.
* @return this builder
*/
public Builder withCredentials(@Nullable Credentials credentials) {
mHomeServerConnectionConfig.mCredentials = credentials;
return this;
}
/**
* @param allowedFingerprints If using SSL, allow server certs that match these fingerprints.
* @return this builder
*/
public Builder withAllowedFingerPrints(@Nullable List<Fingerprint> allowedFingerprints) {
if (allowedFingerprints != null) {
mHomeServerConnectionConfig.mAllowedFingerprints.addAll(allowedFingerprints);
}
return this;
}
/**
* @param pin If true only allow certs matching given fingerprints, otherwise fallback to
* standard X509 checks.
* @return this builder
*/
public Builder withPin(boolean pin) {
mHomeServerConnectionConfig.mPin = pin;
return this;
}
/**
* @param shouldAcceptTlsExtension
* @return this builder
*/
public Builder withShouldAcceptTlsExtensions(boolean shouldAcceptTlsExtension) {
mHomeServerConnectionConfig.mShouldAcceptTlsExtensions = shouldAcceptTlsExtension;
return this;
}
/**
* Add an accepted TLS version for TLS connections with the home server.
*
* @param tlsVersion the tls version to add to the set of TLS versions accepted.
* @return this builder
*/
public Builder addAcceptedTlsVersion(@NonNull TlsVersion tlsVersion) {
if (mHomeServerConnectionConfig.mTlsVersions == null) {
mHomeServerConnectionConfig.mTlsVersions = new ArrayList<>();
}
mHomeServerConnectionConfig.mTlsVersions.add(tlsVersion);
return this;
}
/**
* Force the usage of TlsVersion. This can be usefull for device on Android version < 20
*
* @param forceUsageOfTlsVersions set to true to force the usage of specified TlsVersions (with {@link #addAcceptedTlsVersion(TlsVersion)}
* @return this builder
*/
public Builder forceUsageOfTlsVersions(boolean forceUsageOfTlsVersions) {
mHomeServerConnectionConfig.mForceUsageTlsVersions = forceUsageOfTlsVersions;
return this;
}
/**
* Add a TLS cipher suite to the list of accepted TLS connections with the home server.
*
* @param tlsCipherSuite the tls cipher suite to add.
* @return this builder
*/
public Builder addAcceptedTlsCipherSuite(@NonNull CipherSuite tlsCipherSuite) {
if (mHomeServerConnectionConfig.mTlsCipherSuites == null) {
mHomeServerConnectionConfig.mTlsCipherSuites = new ArrayList<>();
}
mHomeServerConnectionConfig.mTlsCipherSuites.add(tlsCipherSuite);
return this;
}
/**
* Update the anti-virus server URI.
*
* @param antivirusServerUri the new anti-virus uri. Can be null
* @return this builder
*/
public Builder withAntiVirusServerUri(@Nullable Uri antivirusServerUri) {
if ((null != antivirusServerUri) && (!"http".equals(antivirusServerUri.getScheme()) && !"https".equals(antivirusServerUri.getScheme()))) {
throw new RuntimeException("Invalid antivirus server URI: " + antivirusServerUri);
}
mHomeServerConnectionConfig.mAntiVirusServerUri = antivirusServerUri;
return this;
}
/**
* For test only: allow Http connection
*/
@VisibleForTesting
public Builder withAllowHttpConnection() {
mHomeServerConnectionConfig.mAllowHttpExtension = true;
return this;
}
/**
* Convenient method to limit the TLS versions and cipher suites for this Builder
* Ref:
* - https://www.ssi.gouv.fr/uploads/2017/02/security-recommendations-for-tls_v1.1.pdf
* - https://developer.android.com/reference/javax/net/ssl/SSLEngine
*
* @param tlsLimitations true to use Tls limitations
* @param enableCompatibilityMode set to true for Android < 20
* @return this builder
*/
public Builder withTlsLimitations(boolean tlsLimitations, boolean enableCompatibilityMode) {
if (tlsLimitations) {
withShouldAcceptTlsExtensions(false);
// Tls versions
addAcceptedTlsVersion(TlsVersion.TLS_1_2);
addAcceptedTlsVersion(TlsVersion.TLS_1_3);
forceUsageOfTlsVersions(enableCompatibilityMode);
// Cipher suites
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256);
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256);
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256);
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256);
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384);
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384);
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256);
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256);
if (enableCompatibilityMode) {
// Adopt some preceding cipher suites for Android < 20 to be able to negotiate
// a TLS session.
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA);
addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA);
}
}
return this;
}
/**
* @return the {@link HomeServerConnectionConfig}
*/
public HomeServerConnectionConfig build() {
// Check mandatory parameters
if (mHomeServerConnectionConfig.mHsUri == null) {
throw new RuntimeException("Home server URI not set");
}
return mHomeServerConnectionConfig;
}
}
}

View file

@ -0,0 +1,138 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy;
import android.support.annotation.Nullable;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
/**
* This class contains pattern to match the different Matrix ids
*/
public class MXPatterns {
private MXPatterns() {
// Cannot be instantiated
}
// Note: TLD is not mandatory (localhost, IP address...)
private static final String DOMAIN_REGEX = ":[A-Z0-9.-]+(:[0-9]{2,5})?";
// regex pattern to find matrix user ids in a string.
// See https://matrix.org/speculator/spec/HEAD/appendices.html#historical-user-ids
private static final String MATRIX_USER_IDENTIFIER_REGEX = "@[A-Z0-9\\x21-\\x39\\x3B-\\x7F]+" + DOMAIN_REGEX;
public static final Pattern PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = Pattern.compile(MATRIX_USER_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE);
// regex pattern to find room ids in a string.
private static final String MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9]+" + DOMAIN_REGEX;
public static final Pattern PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER = Pattern.compile(MATRIX_ROOM_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE);
// regex pattern to find room aliases in a string.
private static final String MATRIX_ROOM_ALIAS_REGEX = "#[A-Z0-9._%#@=+-]+" + DOMAIN_REGEX;
public static final Pattern PATTERN_CONTAIN_MATRIX_ALIAS = Pattern.compile(MATRIX_ROOM_ALIAS_REGEX, Pattern.CASE_INSENSITIVE);
// regex pattern to find message ids in a string.
private static final String MATRIX_EVENT_IDENTIFIER_REGEX = "\\$[A-Z0-9]+" + DOMAIN_REGEX;
public static final Pattern PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER = Pattern.compile(MATRIX_EVENT_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE);
// regex pattern to find group ids in a string.
private static final String MATRIX_GROUP_IDENTIFIER_REGEX = "\\+[A-Z0-9=_\\-./]+" + DOMAIN_REGEX;
public static final Pattern PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER = Pattern.compile(MATRIX_GROUP_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE);
// regex pattern to find permalink with message id.
// Android does not support in URL so extract it.
private static final String PERMALINK_BASE_REGEX = "https://matrix\\.to/#/";
private static final String APP_BASE_REGEX = "https://[A-Z0-9.-]+\\.[A-Z]{2,}/[A-Z]{3,}/#/room/";
private static final String SEP_REGEX = "/";
private static final String LINK_TO_ROOM_ID_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX;
public static final Pattern PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID = Pattern.compile(LINK_TO_ROOM_ID_REGEXP, Pattern.CASE_INSENSITIVE);
private static final String LINK_TO_ROOM_ALIAS_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX;
public static final Pattern PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS = Pattern.compile(LINK_TO_ROOM_ALIAS_REGEXP, Pattern.CASE_INSENSITIVE);
private static final String LINK_TO_APP_ROOM_ID_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX;
public static final Pattern PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID = Pattern.compile(LINK_TO_APP_ROOM_ID_REGEXP, Pattern.CASE_INSENSITIVE);
private static final String LINK_TO_APP_ROOM_ALIAS_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX;
public static final Pattern PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS = Pattern.compile(LINK_TO_APP_ROOM_ALIAS_REGEXP, Pattern.CASE_INSENSITIVE);
// list of patterns to find some matrix item.
public static final List<Pattern> MATRIX_PATTERNS = Arrays.asList(
MXPatterns.PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID,
MXPatterns.PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS,
MXPatterns.PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID,
MXPatterns.PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS,
MXPatterns.PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER,
MXPatterns.PATTERN_CONTAIN_MATRIX_ALIAS,
MXPatterns.PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER,
MXPatterns.PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER,
MXPatterns.PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER
);
/**
* Tells if a string is a valid user Id.
*
* @param str the string to test
* @return true if the string is a valid user id
*/
public static boolean isUserId(@Nullable final String str) {
return str != null && PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER.matcher(str).matches();
}
/**
* Tells if a string is a valid room id.
*
* @param str the string to test
* @return true if the string is a valid room Id
*/
public static boolean isRoomId(@Nullable final String str) {
return str != null && PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER.matcher(str).matches();
}
/**
* Tells if a string is a valid room alias.
*
* @param str the string to test
* @return true if the string is a valid room alias.
*/
public static boolean isRoomAlias(@Nullable final String str) {
return str != null && PATTERN_CONTAIN_MATRIX_ALIAS.matcher(str).matches();
}
/**
* Tells if a string is a valid event id.
*
* @param str the string to test
* @return true if the string is a valid event id.
*/
public static boolean isEventId(@Nullable final String str) {
return str != null && PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER.matcher(str).matches();
}
/**
* Tells if a string is a valid group id.
*
* @param str the string to test
* @return true if the string is a valid group id.
*/
public static boolean isGroupId(@Nullable final String str) {
return str != null && PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER.matcher(str).matches();
}
}

View file

@ -0,0 +1,738 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy;
import android.os.Looper;
import android.support.annotation.Nullable;
import im.vector.matrix.android.internal.legacy.data.MyUser;
import im.vector.matrix.android.internal.legacy.data.RoomState;
import im.vector.matrix.android.internal.legacy.listeners.IMXEventListener;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
import im.vector.matrix.android.internal.legacy.rest.model.User;
import im.vector.matrix.android.internal.legacy.rest.model.bingrules.BingRule;
import im.vector.matrix.android.internal.legacy.util.Log;
import im.vector.matrix.android.internal.legacy.util.MXOsHandler;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Dispatcher for MXDataHandler
* This class store a list of listener and dispatch event to every listener on the Ui Thread
*/
/* package */ class MxEventDispatcher {
private static final String LOG_TAG = MxEventDispatcher.class.getSimpleName();
private final MXOsHandler mUiHandler;
@Nullable
private IMXEventListener mCryptoEventsListener = null;
private final Set<IMXEventListener> mEventListeners = new HashSet<>();
MxEventDispatcher() {
mUiHandler = new MXOsHandler(Looper.getMainLooper());
}
/* ==========================================================================================
* Public utilities
* ========================================================================================== */
/**
* Set the crypto events listener, or remove it
*
* @param listener the listener or null to remove the listener
*/
public void setCryptoEventsListener(@Nullable IMXEventListener listener) {
mCryptoEventsListener = listener;
}
/**
* Add a listener to the listeners list.
*
* @param listener the listener to add.
*/
public void addListener(IMXEventListener listener) {
mEventListeners.add(listener);
}
/**
* Remove a listener from the listeners list.
*
* @param listener to remove.
*/
public void removeListener(IMXEventListener listener) {
mEventListeners.remove(listener);
}
/**
* Remove any listener
*/
public void clearListeners() {
mEventListeners.clear();
}
/* ==========================================================================================
* Dispatchers
* ========================================================================================== */
public void dispatchOnStoreReady() {
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onStoreReady();
} catch (Exception e) {
Log.e(LOG_TAG, "onStoreReady " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnAccountInfoUpdate(final MyUser myUser) {
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onAccountInfoUpdate(myUser);
} catch (Exception e) {
Log.e(LOG_TAG, "onAccountInfoUpdate " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnPresenceUpdate(final Event event, final User user) {
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onPresenceUpdate(event, user);
} catch (Exception e) {
Log.e(LOG_TAG, "onPresenceUpdate " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnLiveEvent(final Event event, final RoomState roomState) {
if (null != mCryptoEventsListener) {
mCryptoEventsListener.onLiveEvent(event, roomState);
}
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onLiveEvent(event, roomState);
} catch (Exception e) {
Log.e(LOG_TAG, "onLiveEvent " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnLiveEventsChunkProcessed(final String startToken, final String toToken) {
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onLiveEventsChunkProcessed(startToken, toToken);
} catch (Exception e) {
Log.e(LOG_TAG, "onLiveEventsChunkProcessed " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnBingEvent(final Event event, final RoomState roomState, final BingRule bingRule, boolean ignoreEvent) {
if (ignoreEvent) {
return;
}
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onBingEvent(event, roomState, bingRule);
} catch (Exception e) {
Log.e(LOG_TAG, "onBingEvent " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnEventSentStateUpdated(final Event event, boolean ignoreEvent) {
if (ignoreEvent) {
return;
}
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onEventSentStateUpdated(event);
} catch (Exception e) {
Log.e(LOG_TAG, "onEventSentStateUpdated " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnEventSent(final Event event, final String prevEventId, boolean ignoreEvent) {
if (ignoreEvent) {
return;
}
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onEventSent(event, prevEventId);
} catch (Exception e) {
Log.e(LOG_TAG, "onEventSent " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnBingRulesUpdate() {
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onBingRulesUpdate();
} catch (Exception e) {
Log.e(LOG_TAG, "onBingRulesUpdate " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnInitialSyncComplete(final String toToken) {
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onInitialSyncComplete(toToken);
} catch (Exception e) {
Log.e(LOG_TAG, "onInitialSyncComplete " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnCryptoSyncComplete() {
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onCryptoSyncComplete();
} catch (Exception e) {
Log.e(LOG_TAG, "OnCryptoSyncComplete " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnSyncError(final MatrixError matrixError) {
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onSyncError(matrixError);
} catch (Exception e) {
Log.e(LOG_TAG, "onSyncError " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnNewRoom(final String roomId, boolean ignoreEvent) {
if (ignoreEvent) {
return;
}
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onNewRoom(roomId);
} catch (Exception e) {
Log.e(LOG_TAG, "onNewRoom " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnJoinRoom(final String roomId, boolean ignoreEvent) {
if (ignoreEvent) {
return;
}
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onJoinRoom(roomId);
} catch (Exception e) {
Log.e(LOG_TAG, "onJoinRoom " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnRoomInternalUpdate(final String roomId, boolean ignoreEvent) {
if (ignoreEvent) {
return;
}
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onRoomInternalUpdate(roomId);
} catch (Exception e) {
Log.e(LOG_TAG, "onRoomInternalUpdate " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnLeaveRoom(final String roomId, boolean ignoreEvent) {
if (ignoreEvent) {
return;
}
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onLeaveRoom(roomId);
} catch (Exception e) {
Log.e(LOG_TAG, "onLeaveRoom " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnRoomKick(final String roomId, boolean ignoreEvent) {
if (ignoreEvent) {
return;
}
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onRoomKick(roomId);
} catch (Exception e) {
Log.e(LOG_TAG, "onRoomKick " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnReceiptEvent(final String roomId, final List<String> senderIds, boolean ignoreEvent) {
if (ignoreEvent) {
return;
}
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onReceiptEvent(roomId, senderIds);
} catch (Exception e) {
Log.e(LOG_TAG, "onReceiptEvent " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnRoomTagEvent(final String roomId, boolean ignoreEvent) {
if (ignoreEvent) {
return;
}
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onRoomTagEvent(roomId);
} catch (Exception e) {
Log.e(LOG_TAG, "onRoomTagEvent " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnReadMarkerEvent(final String roomId, boolean ignoreEvent) {
if (ignoreEvent) {
return;
}
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onReadMarkerEvent(roomId);
} catch (Exception e) {
Log.e(LOG_TAG, "onReadMarkerEvent " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnRoomFlush(final String roomId, boolean ignoreEvent) {
if (ignoreEvent) {
return;
}
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onRoomFlush(roomId);
} catch (Exception e) {
Log.e(LOG_TAG, "onRoomFlush " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnIgnoredUsersListUpdate() {
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onIgnoredUsersListUpdate();
} catch (Exception e) {
Log.e(LOG_TAG, "onIgnoredUsersListUpdate " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnToDeviceEvent(final Event event, boolean ignoreEvent) {
if (null != mCryptoEventsListener) {
mCryptoEventsListener.onToDeviceEvent(event);
}
if (ignoreEvent) {
return;
}
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onToDeviceEvent(event);
} catch (Exception e) {
Log.e(LOG_TAG, "OnToDeviceEvent " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnDirectMessageChatRoomsListUpdate() {
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onDirectMessageChatRoomsListUpdate();
} catch (Exception e) {
Log.e(LOG_TAG, "onDirectMessageChatRoomsListUpdate " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnEventDecrypted(final Event event) {
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onEventDecrypted(event);
} catch (Exception e) {
Log.e(LOG_TAG, "onDecryptedEvent " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnNewGroupInvitation(final String groupId) {
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onNewGroupInvitation(groupId);
} catch (Exception e) {
Log.e(LOG_TAG, "onNewGroupInvitation " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnJoinGroup(final String groupId) {
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onJoinGroup(groupId);
} catch (Exception e) {
Log.e(LOG_TAG, "onJoinGroup " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnLeaveGroup(final String groupId) {
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onLeaveGroup(groupId);
} catch (Exception e) {
Log.e(LOG_TAG, "onLeaveGroup " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnGroupProfileUpdate(final String groupId) {
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onGroupProfileUpdate(groupId);
} catch (Exception e) {
Log.e(LOG_TAG, "onGroupProfileUpdate " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnGroupRoomsListUpdate(final String groupId) {
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onGroupRoomsListUpdate(groupId);
} catch (Exception e) {
Log.e(LOG_TAG, "onGroupRoomsListUpdate " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnGroupUsersListUpdate(final String groupId) {
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onGroupUsersListUpdate(groupId);
} catch (Exception e) {
Log.e(LOG_TAG, "onGroupUsersListUpdate " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnGroupInvitedUsersListUpdate(final String groupId) {
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onGroupInvitedUsersListUpdate(groupId);
} catch (Exception e) {
Log.e(LOG_TAG, "onGroupInvitedUsersListUpdate " + e.getMessage(), e);
}
}
}
});
}
public void dispatchOnNotificationCountUpdate(final String roomId, boolean ignoreEvent) {
if (ignoreEvent) {
return;
}
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onNotificationCountUpdate(roomId);
} catch (Exception e) {
Log.e(LOG_TAG, "onNotificationCountUpdate " + e.getMessage(), e);
}
}
}
});
}
/* ==========================================================================================
* Private
* ========================================================================================== */
/**
* @return the current MXEvents listeners.
*/
private List<IMXEventListener> getListenersSnapshot() {
List<IMXEventListener> eventListeners;
synchronized (this) {
eventListeners = new ArrayList<>(mEventListeners);
}
return eventListeners;
}
}

View file

@ -0,0 +1,415 @@
/*
* Copyright 2014 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Pair;
import com.google.gson.Gson;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager;
import im.vector.matrix.android.BuildConfig;
import im.vector.matrix.android.internal.legacy.listeners.IMXNetworkEventListener;
import im.vector.matrix.android.internal.legacy.network.NetworkConnectivityReceiver;
import im.vector.matrix.android.internal.legacy.rest.client.MXRestExecutorService;
import im.vector.matrix.android.internal.legacy.rest.model.login.Credentials;
import im.vector.matrix.android.internal.legacy.ssl.CertUtil;
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
import im.vector.matrix.android.internal.legacy.util.Log;
import im.vector.matrix.android.internal.legacy.util.PolymorphicRequestBodyConverter;
import im.vector.matrix.android.internal.legacy.util.UnsentEventsManager;
import okhttp3.Dispatcher;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
/**
* Class for making Matrix API calls.
*/
public class RestClient<T> {
private static final String LOG_TAG = RestClient.class.getSimpleName();
public static final String URI_API_PREFIX_PATH_MEDIA_R0 = "_matrix/media/r0/";
public static final String URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE = "_matrix/media_proxy/unstable/";
public static final String URI_API_PREFIX_PATH = "_matrix/client/";
public static final String URI_API_PREFIX_PATH_R0 = "_matrix/client/r0/";
public static final String URI_API_PREFIX_PATH_UNSTABLE = "_matrix/client/unstable/";
/**
* Prefix used in path of identity server API requests.
*/
public static final String URI_API_PREFIX_IDENTITY = "_matrix/identity/api/v1/";
/**
* List the servers which should be used to define the base url.
*/
public enum EndPointServer {
HOME_SERVER,
IDENTITY_SERVER,
ANTIVIRUS_SERVER
}
protected static final int CONNECTION_TIMEOUT_MS = 30000;
private static final int READ_TIMEOUT_MS = 60000;
private static final int WRITE_TIMEOUT_MS = 60000;
protected Credentials mCredentials;
protected T mApi;
protected Gson gson;
protected UnsentEventsManager mUnsentEventsManager;
protected HomeServerConnectionConfig mHsConfig;
// unitary tests only
public static boolean mUseMXExecutor = false;
// the user agent
private static String sUserAgent = null;
// http client
private OkHttpClient mOkHttpClient = new OkHttpClient();
public RestClient(HomeServerConnectionConfig hsConfig, Class<T> type, String uriPrefix, boolean withNullSerialization) {
this(hsConfig, type, uriPrefix, withNullSerialization, EndPointServer.HOME_SERVER);
}
/**
* Public constructor.
*
* @param hsConfig the home server configuration.
* @param type the REST type
* @param uriPrefix the URL request prefix
* @param withNullSerialization true to serialise class member with null value
* @param useIdentityServer true to use the identity server URL as base request
*/
public RestClient(HomeServerConnectionConfig hsConfig, Class<T> type, String uriPrefix, boolean withNullSerialization, boolean useIdentityServer) {
this(hsConfig, type, uriPrefix, withNullSerialization, useIdentityServer ? EndPointServer.IDENTITY_SERVER : EndPointServer.HOME_SERVER);
}
/**
* Public constructor.
*
* @param hsConfig the home server configuration.
* @param type the REST type
* @param uriPrefix the URL request prefix
* @param withNullSerialization true to serialise class member with null value
* @param endPointServer tell which server is used to define the base url
*/
public RestClient(HomeServerConnectionConfig hsConfig, Class<T> type, String uriPrefix, boolean withNullSerialization, EndPointServer endPointServer) {
// The JSON -> object mapper
gson = JsonUtils.getGson(withNullSerialization);
mHsConfig = hsConfig;
mCredentials = hsConfig.getCredentials();
Interceptor authentInterceptor = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Request.Builder newRequestBuilder = request.newBuilder();
if (null != sUserAgent) {
// set a custom user agent
newRequestBuilder.addHeader("User-Agent", sUserAgent);
}
// Add the access token to all requests if it is set
if ((mCredentials != null) && (mCredentials.accessToken != null)) {
newRequestBuilder.addHeader("Authorization", "Bearer " + mCredentials.accessToken);
}
request = newRequestBuilder.build();
return chain.proceed(request);
}
};
// TODO Remove this, seems so useless
Interceptor connectivityInterceptor = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
if (mUnsentEventsManager != null
&& mUnsentEventsManager.getNetworkConnectivityReceiver() != null
&& !mUnsentEventsManager.getNetworkConnectivityReceiver().isConnected()) {
throw new IOException("Not connected");
}
return chain.proceed(chain.request());
}
};
OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient().newBuilder()
.connectTimeout(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.readTimeout(READ_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.writeTimeout(WRITE_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.addInterceptor(authentInterceptor)
.addInterceptor(connectivityInterceptor);
if (BuildConfig.DEBUG) {
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BASIC);
okHttpClientBuilder
.addInterceptor(loggingInterceptor);
}
if (mUseMXExecutor) {
okHttpClientBuilder.dispatcher(new Dispatcher(new MXRestExecutorService()));
}
try {
Pair<SSLSocketFactory, X509TrustManager> pair = CertUtil.newPinnedSSLSocketFactory(hsConfig);
okHttpClientBuilder.sslSocketFactory(pair.first, pair.second);
okHttpClientBuilder.hostnameVerifier(CertUtil.newHostnameVerifier(hsConfig));
okHttpClientBuilder.connectionSpecs(CertUtil.newConnectionSpecs(hsConfig));
} catch (Exception e) {
Log.e(LOG_TAG, "## RestClient() setSslSocketFactory failed" + e.getMessage(), e);
}
mOkHttpClient = okHttpClientBuilder.build();
final String endPoint = makeEndpoint(hsConfig, uriPrefix, endPointServer);
// Rest adapter for turning API interfaces into actual REST-calling objects
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(endPoint)
.addConverterFactory(PolymorphicRequestBodyConverter.FACTORY)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(mOkHttpClient);
Retrofit retrofit = builder.build();
mApi = retrofit.create(type);
}
@NonNull
private String makeEndpoint(HomeServerConnectionConfig hsConfig, String uriPrefix, EndPointServer endPointServer) {
String baseUrl;
switch (endPointServer) {
case IDENTITY_SERVER:
baseUrl = hsConfig.getIdentityServerUri().toString();
break;
case ANTIVIRUS_SERVER:
baseUrl = hsConfig.getAntiVirusServerUri().toString();
break;
case HOME_SERVER:
default:
baseUrl = hsConfig.getHomeserverUri().toString();
}
baseUrl = sanitizeBaseUrl(baseUrl);
String dynamicPath = sanitizeDynamicPath(uriPrefix);
return baseUrl + dynamicPath;
}
private String sanitizeBaseUrl(String baseUrl) {
if (baseUrl.endsWith("/")) {
return baseUrl;
}
return baseUrl + "/";
}
private String sanitizeDynamicPath(String dynamicPath) {
// remove any trailing http in the uri prefix
if (dynamicPath.startsWith("http://")) {
dynamicPath = dynamicPath.substring("http://".length());
} else if (dynamicPath.startsWith("https://")) {
dynamicPath = dynamicPath.substring("https://".length());
}
return dynamicPath;
}
/**
* Create an user agent with the application version.
* Ex: Riot/0.8.12 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour FDroid; MatrixAndroidSDK 0.9.6)
*
* @param appContext the application context
*/
public static void initUserAgent(Context appContext) {
String appName = "";
String appVersion = "";
if (null != appContext) {
try {
PackageManager pm = appContext.getPackageManager();
ApplicationInfo appInfo = pm.getApplicationInfo(appContext.getApplicationContext().getPackageName(), 0);
appName = pm.getApplicationLabel(appInfo).toString();
PackageInfo pkgInfo = pm.getPackageInfo(appContext.getApplicationContext().getPackageName(), 0);
appVersion = pkgInfo.versionName;
} catch (Exception e) {
Log.e(LOG_TAG, "## initUserAgent() : failed " + e.getMessage(), e);
}
}
sUserAgent = System.getProperty("http.agent");
// cannot retrieve the application version
if (TextUtils.isEmpty(appName) || TextUtils.isEmpty(appVersion)) {
if (null == sUserAgent) {
sUserAgent = "Java" + System.getProperty("java.version");
}
return;
}
// if there is no user agent or cannot parse it
if ((null == sUserAgent) || (sUserAgent.lastIndexOf(")") == -1) || (sUserAgent.indexOf("(") == -1)) {
sUserAgent = appName + "/" + appVersion + "; MatrixAndroidSDK " + BuildConfig.VERSION_NAME + ")";
} else {
// update
sUserAgent = appName + "/" + appVersion + " " +
sUserAgent.substring(sUserAgent.indexOf("("), sUserAgent.lastIndexOf(")") - 1) +
"; MatrixAndroidSDK " + BuildConfig.VERSION_NAME + ")";
}
}
/**
* Get the current user agent
*
* @return the current user agent, or null in case of error or if not initialized yet
*/
@Nullable
public static String getUserAgent() {
return sUserAgent;
}
/**
* Refresh the connection timeouts.
*
* @param networkConnectivityReceiver the network connectivity receiver
*/
private void refreshConnectionTimeout(NetworkConnectivityReceiver networkConnectivityReceiver) {
OkHttpClient.Builder builder = mOkHttpClient.newBuilder();
if (networkConnectivityReceiver.isConnected()) {
float factor = networkConnectivityReceiver.getTimeoutScale();
builder
.connectTimeout((int) (CONNECTION_TIMEOUT_MS * factor), TimeUnit.MILLISECONDS)
.readTimeout((int) (READ_TIMEOUT_MS * factor), TimeUnit.MILLISECONDS)
.writeTimeout((int) (WRITE_TIMEOUT_MS * factor), TimeUnit.MILLISECONDS);
Log.d(LOG_TAG, "## refreshConnectionTimeout() : update setConnectTimeout to " + (CONNECTION_TIMEOUT_MS * factor) + " ms");
Log.d(LOG_TAG, "## refreshConnectionTimeout() : update setReadTimeout to " + (READ_TIMEOUT_MS * factor) + " ms");
Log.d(LOG_TAG, "## refreshConnectionTimeout() : update setWriteTimeout to " + (WRITE_TIMEOUT_MS * factor) + " ms");
} else {
builder.connectTimeout(1, TimeUnit.MILLISECONDS);
Log.d(LOG_TAG, "## refreshConnectionTimeout() : update the requests timeout to 1 ms");
}
// FIXME It has no effect to the rest client
mOkHttpClient = builder.build();
}
/**
* Update the connection timeout
*
* @param aTimeoutMs the connection timeout
*/
protected void setConnectionTimeout(int aTimeoutMs) {
int timeoutMs = aTimeoutMs;
if (null != mUnsentEventsManager) {
NetworkConnectivityReceiver networkConnectivityReceiver = mUnsentEventsManager.getNetworkConnectivityReceiver();
if (null != networkConnectivityReceiver) {
if (networkConnectivityReceiver.isConnected()) {
timeoutMs *= networkConnectivityReceiver.getTimeoutScale();
} else {
timeoutMs = 1000;
}
}
}
if (timeoutMs != mOkHttpClient.connectTimeoutMillis()) {
// FIXME It has no effect to the rest client
mOkHttpClient = mOkHttpClient.newBuilder().connectTimeout(timeoutMs, TimeUnit.MILLISECONDS).build();
}
}
/**
* Set the unsentEvents manager.
*
* @param unsentEventsManager The unsentEvents manager.
*/
public void setUnsentEventsManager(UnsentEventsManager unsentEventsManager) {
mUnsentEventsManager = unsentEventsManager;
final NetworkConnectivityReceiver networkConnectivityReceiver = mUnsentEventsManager.getNetworkConnectivityReceiver();
refreshConnectionTimeout(networkConnectivityReceiver);
networkConnectivityReceiver.addEventListener(new IMXNetworkEventListener() {
@Override
public void onNetworkConnectionUpdate(boolean isConnected) {
Log.d(LOG_TAG, "## setUnsentEventsManager() : update the requests timeout to " + (isConnected ? CONNECTION_TIMEOUT_MS : 1) + " ms");
refreshConnectionTimeout(networkConnectivityReceiver);
}
});
}
/**
* Get the user's credentials. Typically for saving them somewhere persistent.
*
* @return the user credentials
*/
public Credentials getCredentials() {
return mCredentials;
}
/**
* Provide the user's credentials. To be called after login or registration.
*
* @param credentials the user credentials
*/
public void setCredentials(Credentials credentials) {
mCredentials = credentials;
}
/**
* Default protected constructor for unit tests.
*/
protected RestClient() {
}
/**
* Protected setter for injection by unit tests.
*
* @param api the api object
*/
@VisibleForTesting()
protected void setApi(T api) {
mApi = api;
}
}

View file

@ -0,0 +1,797 @@
/*
* Copyright 2014 OpenMarket Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.call;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Environment;
import android.os.Vibrator;
import android.provider.MediaStore;
import im.vector.matrix.android.R;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* This class manages the call sound.
* It is in charge of playing ring tones and managing the audio focus.
*/
public class CallSoundsManager {
private static final String LOG_TAG = CallSoundsManager.class.getSimpleName();
/**
* Track the audio focus update.
*/
public interface OnAudioFocusListener {
/**
* Call back indicating new focus events (ex: {@link AudioManager#AUDIOFOCUS_GAIN},
* {@link AudioManager#AUDIOFOCUS_LOSS}..).
*
* @param aFocusEvent the focus event (see {@link AudioManager.OnAudioFocusChangeListener})
*/
void onFocusChanged(int aFocusEvent);
}
/**
* Track the audio configuration change (like speaker, micro and so on).
*/
public interface OnAudioConfigurationUpdateListener {
void onAudioConfigurationUpdate();
}
/**
* Track the media statuses.
*/
public interface OnMediaListener {
/**
* The media is ready to be played
*/
void onMediaReadyToPlay();
/**
* The media is playing.
*/
void onMediaPlay();
/**
* The media has been played
*/
void onMediaCompleted();
}
private static CallSoundsManager mSharedInstance = null;
private final Context mContext;
/**
* Constructor
*
* @param context the context
*/
private CallSoundsManager(Context context) {
mContext = context;
}
/**
* Provides the shared instance.
*
* @param context the context
* @return the shared instance
*/
public static CallSoundsManager getSharedInstance(Context context) {
if (null == mSharedInstance) {
mSharedInstance = new CallSoundsManager(context.getApplicationContext());
}
return mSharedInstance;
}
//==============================================================================================================
// Audio configuration management
//==============================================================================================================
// audio focus management
private final Set<OnAudioConfigurationUpdateListener> mOnAudioConfigurationUpdateListener = new HashSet<>();
/**
* Add an audio configuration update listener.
*
* @param listener the listener.
*/
public void addAudioConfigurationListener(OnAudioConfigurationUpdateListener listener) {
synchronized (LOG_TAG) {
mOnAudioConfigurationUpdateListener.add(listener);
}
}
/**
* Remove an audio configuration update listener.
*
* @param listener the listener.
*/
public void removeAudioConfigurationListener(OnAudioConfigurationUpdateListener listener) {
synchronized (LOG_TAG) {
mOnAudioConfigurationUpdateListener.remove(listener);
}
}
/**
* Dispatch that the audio configuration has been updated.
*/
private void dispatchAudioConfigurationUpdate() {
synchronized (LOG_TAG) {
// notify listeners
for (OnAudioConfigurationUpdateListener listener : mOnAudioConfigurationUpdateListener) {
try {
listener.onAudioConfigurationUpdate();
} catch (Exception e) {
Log.e(LOG_TAG, "## dispatchAudioConfigurationUpdate() failed " + e.getMessage(), e);
}
}
}
}
//==============================================================================================================
// Focus management
//==============================================================================================================
// audio focus management
private final Set<OnAudioFocusListener> mAudioFocusListeners = new HashSet<>();
private final AudioManager.OnAudioFocusChangeListener mFocusListener = new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int aFocusEvent) {
switch (aFocusEvent) {
case AudioManager.AUDIOFOCUS_GAIN:
Log.d(LOG_TAG, "## OnAudioFocusChangeListener(): AUDIOFOCUS_GAIN");
// TODO resume voip call (ex: ending GSM call)
break;
case AudioManager.AUDIOFOCUS_LOSS:
Log.d(LOG_TAG, "## OnAudioFocusChangeListener(): AUDIOFOCUS_LOSS");
// TODO pause voip call (ex: incoming GSM call)
break;
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
Log.d(LOG_TAG, "## OnAudioFocusChangeListener(): AUDIOFOCUS_GAIN_TRANSIENT");
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
Log.d(LOG_TAG, "## OnAudioFocusChangeListener(): AUDIOFOCUS_LOSS_TRANSIENT");
// TODO pause voip call (ex: incoming GSM call)
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// TODO : continue playing at an attenuated level
Log.d(LOG_TAG, "## OnAudioFocusChangeListener(): AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK");
break;
case AudioManager.AUDIOFOCUS_REQUEST_FAILED:
Log.d(LOG_TAG, "## OnAudioFocusChangeListener(): AUDIOFOCUS_REQUEST_FAILED");
break;
default:
break;
}
synchronized (LOG_TAG) {
// notify listeners
for (OnAudioFocusListener listener : mAudioFocusListeners) {
try {
listener.onFocusChanged(aFocusEvent);
} catch (Exception e) {
Log.e(LOG_TAG, "## onFocusChanged() failed " + e.getMessage(), e);
}
}
}
}
};
/**
* Add a focus listener.
*
* @param focusListener the listener.
*/
public void addFocusListener(OnAudioFocusListener focusListener) {
synchronized (LOG_TAG) {
mAudioFocusListeners.add(focusListener);
}
}
/**
* Remove a focus listener.
*
* @param focusListener the listener.
*/
public void removeFocusListener(OnAudioFocusListener focusListener) {
synchronized (LOG_TAG) {
mAudioFocusListeners.remove(focusListener);
}
}
//==============================================================================================================
// Ringtone management management
//==============================================================================================================
/**
* @return the audio manager
*/
private AudioManager getAudioManager() {
if (null == mAudioManager) {
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
}
return mAudioManager;
}
// audio focus
private boolean mIsFocusGranted = false;
private static final int VIBRATE_DURATION = 500; // milliseconds
private static final int VIBRATE_SLEEP = 1000; // milliseconds
private static final long[] VIBRATE_PATTERN = {0, VIBRATE_DURATION, VIBRATE_SLEEP};
private Ringtone mRingTone;
private boolean mIsRinging;
private MediaPlayer mMediaPlayer = null;
// the audio manager (do not use directly, use getAudioManager())
private AudioManager mAudioManager = null;
// the playing sound
private int mPlayingSound = -1;
/**
* Tells that the device is ringing.
*
* @return true if the device is ringing
*/
public boolean isRinging() {
return mIsRinging;
}
/**
* Getter method.
*
* @return true is focus is granted, false otherwise.
*/
public boolean isFocusGranted() {
return mIsFocusGranted;
}
/**
* Stop any playing sound.
*/
public void stopSounds() {
mIsRinging = false;
if (null != mRingTone) {
mRingTone.stop();
mRingTone = null;
}
if (null != mMediaPlayer) {
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.stop();
}
mMediaPlayer.release();
mMediaPlayer = null;
}
mPlayingSound = -1;
// stop vibrate
enableVibrating(false);
}
/**
* Stop the ringing sound
*/
public void stopRinging() {
Log.d(LOG_TAG, "stopRinging");
stopSounds();
// stop vibrate
enableVibrating(false);
}
/**
* Request a permanent audio focus if the focus was not yet granted.
*/
public void requestAudioFocus() {
if (!mIsFocusGranted) {
int focusResult;
AudioManager audioMgr;
if ((null != (audioMgr = getAudioManager()))) {
// Request permanent audio focus for voice call
focusResult = audioMgr.requestAudioFocus(mFocusListener, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN);
if (AudioManager.AUDIOFOCUS_REQUEST_GRANTED == focusResult) {
mIsFocusGranted = true;
Log.d(LOG_TAG, "## getAudioFocus(): granted");
} else {
mIsFocusGranted = false;
Log.w(LOG_TAG, "## getAudioFocus(): refused - focusResult=" + focusResult);
}
}
dispatchAudioConfigurationUpdate();
} else {
Log.d(LOG_TAG, "## getAudioFocus(): already granted");
}
}
/**
* Release the audio focus if it was granted.
*/
public void releaseAudioFocus() {
if (mIsFocusGranted) {
AudioManager audioManager = getAudioManager();
if ((null != audioManager)) {
// release focus
int abandonResult = audioManager.abandonAudioFocus(mFocusListener);
if (AudioManager.AUDIOFOCUS_REQUEST_GRANTED == abandonResult) {
Log.d(LOG_TAG, "## releaseAudioFocus(): abandonAudioFocus = AUDIOFOCUS_REQUEST_GRANTED");
}
if (AudioManager.AUDIOFOCUS_REQUEST_FAILED == abandonResult) {
Log.d(LOG_TAG, "## releaseAudioFocus(): abandonAudioFocus = AUDIOFOCUS_REQUEST_FAILED");
}
} else {
Log.d(LOG_TAG, "## releaseAudioFocus(): failure - invalid AudioManager");
}
mIsFocusGranted = false;
}
restoreAudioConfig();
dispatchAudioConfigurationUpdate();
}
/**
* Start the ringing sound.
*
* @param resId the ring sound id
* @param filename the filename to save the ringtone
*/
public void startRinging(int resId, String filename) {
Log.v(LOG_TAG, "startRinging");
if (mRingTone != null) {
Log.v(LOG_TAG, "ring tone already ringing");
}
// stop any playing ringtone
stopSounds();
mIsRinging = true;
// use the ringTone to manage sound volume properly
mRingTone = getRingTone(mContext, resId, filename, RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE));
if (mRingTone != null) {
setSpeakerphoneOn(false, true);
mRingTone.play();
} else {
Log.e(LOG_TAG, "startRinging : fail to retrieve RING_TONE_START_RINGING");
}
// start vibrate
enableVibrating(true);
}
/**
* Same than {@link #startRinging(int, String)}}, but do not play sound, nor vibrate.
*/
public void startRingingSilently() {
mIsRinging = true;
}
/**
* Enable the vibrate mode.
*
* @param aIsVibrateEnabled true to force vibrate, false to stop vibrate.
*/
private void enableVibrating(boolean aIsVibrateEnabled) {
Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
if ((null != vibrator) && vibrator.hasVibrator()) {
if (aIsVibrateEnabled) {
vibrator.vibrate(VIBRATE_PATTERN, 0 /*repeat till stop*/);
Log.d(LOG_TAG, "## startVibrating(): Vibrate started");
} else {
vibrator.cancel();
Log.d(LOG_TAG, "## startVibrating(): Vibrate canceled");
}
} else {
Log.w(LOG_TAG, "## startVibrating(): vibrator access failed");
}
}
/**
* Start a sound.
*
* @param resId the sound resource id
* @param isLooping true to loop
* @param listener the listener
*/
public void startSound(int resId, boolean isLooping, final OnMediaListener listener) {
Log.d(LOG_TAG, "startSound");
if (mPlayingSound == resId) {
Log.d(LOG_TAG, "## startSound() : already playing " + resId);
return;
}
stopSounds();
mPlayingSound = resId;
mMediaPlayer = MediaPlayer.create(mContext, resId);
if (null != mMediaPlayer) {
mMediaPlayer.setLooping(isLooping);
if (null != listener) {
listener.onMediaReadyToPlay();
}
mMediaPlayer.start();
if (null != listener) {
listener.onMediaPlay();
}
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
if (null != listener) {
listener.onMediaCompleted();
}
mPlayingSound = -1;
if (null != mMediaPlayer) {
mMediaPlayer.release();
mMediaPlayer = null;
}
}
});
} else {
Log.e(LOG_TAG, "startSound : failed");
}
}
//==============================================================================================================
// resid / filenime to ringtone
//==============================================================================================================
private static final Map<String, Uri> mRingtoneUrlByFileName = new HashMap<>();
/**
* Provide a ringtone uri from a resource and a filename.
*
* @param context the context
* @param resId The audio resource.
* @param filename the audio filename
* @return the ringtone uri
*/
private static Uri getRingToneUri(Context context, int resId, String filename) {
Uri ringToneUri = mRingtoneUrlByFileName.get(filename);
// test if the ring tone has been cached
if (null != ringToneUri) {
// check if the file exists
try {
File ringFile = new File(ringToneUri.toString());
// check if the file exists
if ((null != ringFile) && ringFile.exists() && ringFile.canRead()) {
// provide it
return ringToneUri;
}
} catch (Exception e) {
Log.e(LOG_TAG, "## getRingToneUri() failed " + e.getMessage(), e);
}
}
try {
File directory = new File(Environment.getExternalStorageDirectory(), "/" + context.getApplicationContext().getPackageName().hashCode() + "/Audio/");
// create the directory if it does not exist
if (!directory.exists()) {
directory.mkdirs();
}
File file = new File(directory + "/", filename);
// if the file exists, check if the resource has been created
if (file.exists()) {
Cursor cursor = context.getContentResolver().query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
new String[]{MediaStore.Audio.Media._ID},
MediaStore.Audio.Media.DATA + "=? ",
new String[]{file.getAbsolutePath()}, null);
if ((null != cursor) && cursor.moveToFirst()) {
int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
ringToneUri = Uri.withAppendedPath(Uri.parse("content://media/external/audio/media"), "" + id);
}
if (null != cursor) {
cursor.close();
}
}
// the Uri has been retrieved
if (null == ringToneUri) {
// create the file
if (!file.exists()) {
try {
byte[] readData = new byte[1024];
InputStream fis = context.getResources().openRawResource(resId);
FileOutputStream fos = new FileOutputStream(file);
int i = fis.read(readData);
while (i != -1) {
fos.write(readData, 0, i);
i = fis.read(readData);
}
fos.close();
} catch (Exception e) {
Log.e(LOG_TAG, "## getRingToneUri(): Exception1 Msg=" + e.getMessage(), e);
}
}
// and the resource Uri
ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.DATA, file.getAbsolutePath());
values.put(MediaStore.MediaColumns.TITLE, filename);
values.put(MediaStore.MediaColumns.MIME_TYPE, "audio/ogg");
values.put(MediaStore.MediaColumns.SIZE, file.length());
values.put(MediaStore.Audio.Media.ARTIST, R.string.app_name);
values.put(MediaStore.Audio.Media.IS_RINGTONE, true);
values.put(MediaStore.Audio.Media.IS_NOTIFICATION, true);
values.put(MediaStore.Audio.Media.IS_ALARM, true);
values.put(MediaStore.Audio.Media.IS_MUSIC, true);
ringToneUri = context.getContentResolver().insert(MediaStore.Audio.Media.getContentUriForPath(file.getAbsolutePath()), values);
}
if (null != ringToneUri) {
mRingtoneUrlByFileName.put(filename, ringToneUri);
return ringToneUri;
}
} catch (Exception e) {
Log.e(LOG_TAG, "## getRingToneUri(): Exception2 Msg=" + e.getLocalizedMessage(), e);
}
return null;
}
/**
* Retrieve a ringtone from an uri
*
* @param context the context
* @param ringToneUri the ringtone URI
* @return the ringtone
*/
private static Ringtone uriToRingTone(Context context, Uri ringToneUri) {
if (null != ringToneUri) {
try {
return RingtoneManager.getRingtone(context, ringToneUri);
} catch (Exception e) {
Log.e(LOG_TAG, "## uriToRingTone() failed " + e.getMessage(), e);
}
}
return null;
}
/**
* Provide a ringtone from a resource and a filename.
* The audio file must have a ANDROID_LOOP metatada set to true to loop the sound.
*
* @param context the context
* @param resId The audio resource.
* @param filename the audio filename
* @param defaultRingToneUri the default ring tone
* @return a RingTone, null if the operation fails.
*/
private static Ringtone getRingTone(Context context, int resId, String filename, Uri defaultRingToneUri) {
Ringtone ringtone = uriToRingTone(context, getRingToneUri(context, resId, filename));
if (null == ringtone) {
ringtone = uriToRingTone(context, defaultRingToneUri);
}
Log.d(LOG_TAG, "getRingTone() : resId " + resId + " filename " + filename + " defaultRingToneUri " + defaultRingToneUri + " returns " + ringtone);
return ringtone;
}
//==============================================================================================================
// speakers management
//==============================================================================================================
// save the audio statuses
private Integer mAudioMode = null;
private Boolean mIsSpeakerphoneOn = null;
/**
* Back up the current audio config.
*/
private void backupAudioConfig() {
if (null == mAudioMode) {
AudioManager audioManager = getAudioManager();
mAudioMode = audioManager.getMode();
mIsSpeakerphoneOn = audioManager.isSpeakerphoneOn();
}
}
/**
* Restore the audio config.
*/
private void restoreAudioConfig() {
// ensure that something has been saved
if ((null != mAudioMode) && (null != mIsSpeakerphoneOn)) {
Log.d(LOG_TAG, "## restoreAudioConfig() starts");
AudioManager audioManager = getAudioManager();
if (mAudioMode != audioManager.getMode()) {
Log.d(LOG_TAG, "## restoreAudioConfig() : restore audio mode " + mAudioMode);
audioManager.setMode(mAudioMode);
}
if (mIsSpeakerphoneOn != audioManager.isSpeakerphoneOn()) {
Log.d(LOG_TAG, "## restoreAudioConfig() : restore speaker " + mIsSpeakerphoneOn);
audioManager.setSpeakerphoneOn(mIsSpeakerphoneOn);
}
// stop the bluetooth
if (audioManager.isBluetoothScoOn()) {
Log.d(LOG_TAG, "## restoreAudioConfig() : ends the bluetooth calls");
audioManager.stopBluetoothSco();
audioManager.setBluetoothScoOn(false);
}
mAudioMode = null;
mIsSpeakerphoneOn = null;
Log.d(LOG_TAG, "## restoreAudioConfig() done");
}
}
/**
* Set the speakerphone ON or OFF.
*
* @param isOn true to enable the speaker (ON), false to disable it (OFF)
*/
public void setCallSpeakerphoneOn(boolean isOn) {
setSpeakerphoneOn(true, isOn);
}
/**
* Save the current speaker status and the audio mode, before updating those
* values.
* The audio mode depends on if there is a call in progress.
* If audio mode set to {@link AudioManager#MODE_IN_COMMUNICATION} and
* a media player is in ON, the media player will reduce its audio level.
*
* @param isInCall true when the speaker is updated during call.
* @param isSpeakerOn true to turn on the speaker (false to turn it off)
*/
public void setSpeakerphoneOn(boolean isInCall, boolean isSpeakerOn) {
Log.d(LOG_TAG, "setCallSpeakerphoneOn " + isSpeakerOn);
backupAudioConfig();
try {
AudioManager audioManager = getAudioManager();
int audioMode = isInCall ? AudioManager.MODE_IN_COMMUNICATION : AudioManager.MODE_RINGTONE;
if (audioManager.getMode() != audioMode) {
audioManager.setMode(audioMode);
}
if (!isSpeakerOn) {
try {
if (HeadsetConnectionReceiver.isBTHeadsetPlugged()) {
audioManager.startBluetoothSco();
audioManager.setBluetoothScoOn(true);
} else if (audioManager.isBluetoothScoOn()) {
audioManager.stopBluetoothSco();
audioManager.setBluetoothScoOn(false);
}
} catch (Exception e) {
Log.e(LOG_TAG, "## setSpeakerphoneOn() failed " + e.getMessage(), e);
}
}
if (isSpeakerOn != audioManager.isSpeakerphoneOn()) {
audioManager.setSpeakerphoneOn(isSpeakerOn);
}
} catch (Exception e) {
Log.e(LOG_TAG, "## setSpeakerphoneOn() failed " + e.getMessage(), e);
restoreAudioConfig();
}
dispatchAudioConfigurationUpdate();
}
/**
* Toggle the speaker
*/
public void toggleSpeaker() {
AudioManager audioManager = getAudioManager();
boolean isOn = !audioManager.isSpeakerphoneOn();
audioManager.setSpeakerphoneOn(isOn);
if (!isOn) {
try {
if (HeadsetConnectionReceiver.isBTHeadsetPlugged()) {
audioManager.startBluetoothSco();
audioManager.setBluetoothScoOn(true);
} else if (audioManager.isBluetoothScoOn()) {
audioManager.stopBluetoothSco();
audioManager.setBluetoothScoOn(false);
}
} catch (Exception e) {
Log.e(LOG_TAG, "## toggleSpeaker() failed " + e.getMessage(), e);
}
}
dispatchAudioConfigurationUpdate();
}
/**
* @return true if the speaker is turned on.
*/
public boolean isSpeakerphoneOn() {
return getAudioManager().isSpeakerphoneOn();
}
/**
* Mute the microphone.
*
* @param mute true to mute the microphone
*/
public void setMicrophoneMute(boolean mute) {
getAudioManager().setMicrophoneMute(mute);
dispatchAudioConfigurationUpdate();
}
/**
* @return true if the microphone is mute.
*/
public boolean isMicrophoneMute() {
return getAudioManager().isMicrophoneMute();
}
}

View file

@ -0,0 +1,235 @@
/*
* Copyright 2016 OpenMarket Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.call;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.util.HashSet;
import java.util.Set;
// this class detect if the headset is plugged / unplugged
public class HeadsetConnectionReceiver extends BroadcastReceiver {
private static final String LOG_TAG = HeadsetConnectionReceiver.class.getSimpleName();
private static Boolean mIsHeadsetPlugged = null;
private static HeadsetConnectionReceiver mSharedInstance = null;
/**
* Track the headset update.
*/
public interface OnHeadsetStatusUpdateListener {
/**
* A wire headset has been plugged / unplugged.
*
* @param isPlugged true if the headset is now plugged.
*/
void onWiredHeadsetUpdate(boolean isPlugged);
/**
* A bluetooth headset is connected.
*
* @param isConnected true if the bluetooth headset is connected.
*/
void onBluetoothHeadsetUpdate(boolean isConnected);
}
// listeners
private final Set<OnHeadsetStatusUpdateListener> mListeners = new HashSet<>();
public HeadsetConnectionReceiver() {
}
/**
* @param context the application context
* @return the shared instance
*/
public static HeadsetConnectionReceiver getSharedInstance(Context context) {
if (null == mSharedInstance) {
mSharedInstance = new HeadsetConnectionReceiver();
context.registerReceiver(mSharedInstance, new IntentFilter(Intent.ACTION_HEADSET_PLUG));
context.registerReceiver(mSharedInstance, new IntentFilter(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED));
context.registerReceiver(mSharedInstance, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
context.registerReceiver(mSharedInstance, new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED));
context.registerReceiver(mSharedInstance, new IntentFilter(BluetoothDevice.ACTION_ACL_DISCONNECTED));
}
return mSharedInstance;
}
/**
* Add a listener.
*
* @param listener the listener to add.
*/
public void addListener(OnHeadsetStatusUpdateListener listener) {
synchronized (LOG_TAG) {
mListeners.add(listener);
}
}
/**
* Remove a listener.
*
* @param listener the listener to remove.
*/
public void removeListener(OnHeadsetStatusUpdateListener listener) {
synchronized (LOG_TAG) {
mListeners.remove(listener);
}
}
/**
* Dispatch onBluetoothHeadsetUpdate to the listeners.
*
* @param isConnected true if a bluetooth headset is connected.
*/
private void onBluetoothHeadsetUpdate(boolean isConnected) {
synchronized (LOG_TAG) {
for (OnHeadsetStatusUpdateListener listener : mListeners) {
try {
listener.onBluetoothHeadsetUpdate(isConnected);
} catch (Exception e) {
Log.e(LOG_TAG, "## onBluetoothHeadsetUpdate()) failed " + e.getMessage(), e);
}
}
}
}
/**
* Dispatch onWireHeadsetUpdate to the listeners.
*
* @param isPlugged true if the wire headset is plugged.
*/
private void onWiredHeadsetUpdate(boolean isPlugged) {
synchronized (LOG_TAG) {
for (OnHeadsetStatusUpdateListener listener : mListeners) {
try {
listener.onWiredHeadsetUpdate(isPlugged);
} catch (Exception e) {
Log.e(LOG_TAG, "## onWiredHeadsetUpdate()) failed " + e.getMessage(), e);
}
}
}
}
@Override
public void onReceive(final Context aContext, final Intent aIntent) {
Log.d(LOG_TAG, "## onReceive() : " + aIntent.getExtras());
String action = aIntent.getAction();
if (TextUtils.equals(action, Intent.ACTION_HEADSET_PLUG)
|| TextUtils.equals(action, BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)
|| TextUtils.equals(action, BluetoothAdapter.ACTION_STATE_CHANGED)
|| TextUtils.equals(action, BluetoothDevice.ACTION_ACL_CONNECTED)
|| TextUtils.equals(action, BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
Boolean newState = null;
final boolean isBTHeadsetUpdate;
if (TextUtils.equals(action, Intent.ACTION_HEADSET_PLUG)) {
int state = aIntent.getIntExtra("state", -1);
switch (state) {
case 0:
Log.d(LOG_TAG, "Headset is unplugged");
newState = false;
break;
case 1:
Log.d(LOG_TAG, "Headset is plugged");
newState = true;
break;
default:
Log.d(LOG_TAG, "undefined state");
}
isBTHeadsetUpdate = false;
} else {
int state = BluetoothAdapter.getDefaultAdapter().getProfileConnectionState(BluetoothProfile.HEADSET);
Log.d(LOG_TAG, "bluetooth headset state " + state);
newState = (BluetoothAdapter.STATE_CONNECTED == state);
isBTHeadsetUpdate = mIsHeadsetPlugged != newState;
}
if (newState != mIsHeadsetPlugged) {
mIsHeadsetPlugged = newState;
// wait a little else route to BT headset does not work.
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
if (isBTHeadsetUpdate) {
onBluetoothHeadsetUpdate(mIsHeadsetPlugged);
} else {
onWiredHeadsetUpdate(mIsHeadsetPlugged);
}
}
}, 1000);
}
}
}
private static AudioManager mAudioManager = null;
/**
* @return the audio manager
*/
private static AudioManager getAudioManager(Context context) {
if (null == mAudioManager) {
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
}
return mAudioManager;
}
/**
* @param context the context
* @return true if the headset is plugged
*/
@SuppressLint("Deprecation")
public static boolean isHeadsetPlugged(Context context) {
if (null == mIsHeadsetPlugged) {
AudioManager audioManager = getAudioManager(context);
mIsHeadsetPlugged = isBTHeadsetPlugged() || audioManager.isWiredHeadsetOn();
}
return mIsHeadsetPlugged;
}
/**
* @return true if bluetooth headset is plugged
*/
public static boolean isBTHeadsetPlugged() {
return (BluetoothAdapter.STATE_CONNECTED == BluetoothAdapter.getDefaultAdapter().getProfileConnectionState(BluetoothProfile.HEADSET));
}
}

View file

@ -0,0 +1,337 @@
/*
* Copyright 2015 OpenMarket Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.call;
import android.view.View;
import com.google.gson.JsonObject;
import im.vector.matrix.android.internal.legacy.MXSession;
import im.vector.matrix.android.internal.legacy.data.Room;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
/**
* Audio/video call interface.
* See {@link MXWebRtcCall} and {@link MXChromeCall}.
*/
public interface IMXCall {
// call ending use cases (see {@link #dispatchOnCallEnd}):
int END_CALL_REASON_UNDEFINED = -1;
/**
* the callee has rejected the incoming call
**/
int END_CALL_REASON_PEER_HANG_UP = 0;
/**
* the callee has rejected the incoming call from another device
**/
int END_CALL_REASON_PEER_HANG_UP_ELSEWHERE = 1;
/**
* call ended by the local user himself
**/
int END_CALL_REASON_USER_HIMSELF = 2;
// call state events
// the call is an empty shell nothing has been initialized
String CALL_STATE_CREATED = "IMXCall.CALL_STATE_CREATED";
// the call view is creating and being inserting.
String CALL_STATE_CREATING_CALL_VIEW = "IMXCall.CALL_STATE_CREATING_CALL_VIEW";
// the call view is managed.
// the call can start from now.
String CALL_STATE_READY = "IMXCall.CALL_STATE_READY";
// incoming/outgoing calls : initializing the local audio / video
String CALL_STATE_WAIT_LOCAL_MEDIA = "IMXCall.CALL_STATE_WAIT_LOCAL_MEDIA";
// incoming calls : the local media is retrieved
String CALL_STATE_WAIT_CREATE_OFFER = "IMXCall.CALL_STATE_WAIT_CREATE_OFFER";
// outgoing calls : the call invitation is sent
String CALL_STATE_INVITE_SENT = "IMXCall.CALL_STATE_INVITE_SENT";
// the device is ringing
// incoming calls : after applying the incoming params
// outgoing calls : after getting the m.call.invite echo
String CALL_STATE_RINGING = "IMXCall.CALL_STATE_RINGING";
// incoming calls : create the call answer
String CALL_STATE_CREATE_ANSWER = "IMXCall.CALL_STATE_CREATE_ANSWER";
// the call is connecting
String CALL_STATE_CONNECTING = "IMXCall.CALL_STATE_CONNECTING";
// the call is in progress
String CALL_STATE_CONNECTED = "IMXCall.CALL_STATE_CONNECTED";
// call is ended
String CALL_STATE_ENDED = "IMXCall.CALL_STATE_ENDED";
// error codes
// cannot initialize the camera
String CALL_ERROR_CAMERA_INIT_FAILED = "IMXCall.CALL_ERROR_CAMERA_INIT_FAILED";
// cannot initialize the call.
String CALL_ERROR_CALL_INIT_FAILED = "IMXCall.CALL_ERROR_CALL_INIT_FAILED";
// ICE error
String CALL_ERROR_ICE_FAILED = "IMXCall.CALL_ERROR_ICE_FAILED";
// the user did not respond to the call.
String CALL_ERROR_USER_NOT_RESPONDING = "IMXCall.CALL_ERROR_USER_NOT_RESPONDING";
// creator
/**
* Create the callview
*/
void createCallView();
/**
* The activity is paused.
*/
void onPause();
/**
* The activity is resumed.
*/
void onResume();
// actions (must be done after dispatchOnViewReady()
/**
* Start a call.
*
* @param aLocalVideoPosition position of the local video attendee
*/
void placeCall(VideoLayoutConfiguration aLocalVideoPosition);
/**
* Prepare a call reception.
*
* @param aCallInviteParams the invitation Event content
* @param aCallId the call ID
* @param aLocalVideoPosition position of the local video attendee
*/
void prepareIncomingCall(JsonObject aCallInviteParams, String aCallId, VideoLayoutConfiguration aLocalVideoPosition);
/**
* The call has been detected as an incoming one.
* The application launched the dedicated activity and expects to launch the incoming call.
*
* @param aLocalVideoPosition position of the local video attendee
*/
void launchIncomingCall(VideoLayoutConfiguration aLocalVideoPosition);
/**
* The video will be displayed according to the values set in aConfigurationToApply.
*
* @param aConfigurationToApply the new position to be applied
*/
void updateLocalVideoRendererPosition(VideoLayoutConfiguration aConfigurationToApply);
// events thread
/**
* Manage the call events.
*
* @param event the call event.
*/
void handleCallEvent(Event event);
// user actions
/**
* The call is accepted.
*/
void answer();
/**
* The call has been has answered on another device.
*/
void onAnsweredElsewhere();
/**
* The call is hung up.
*
* @param reason the reason
*/
void hangup(String reason);
/**
* Add a listener to the call manager.
*
* @param callListener the call listener
*/
void addListener(IMXCallListener callListener);
/**
* Remove a listener from the call manager.
*
* @param callListener the call listener
*/
void removeListener(IMXCallListener callListener);
// getters / setters
/**
* @return the callId
*/
String getCallId();
/**
* Set the callId
*
* @param callId the call id
*/
void setCallId(String callId);
/**
* @return the linked room
*/
Room getRoom();
/**
* Set the linked rooms (conference call)
*
* @param room the room
* @param callSignalingRoom the call signaling room.
*/
void setRooms(Room room, Room callSignalingRoom);
/**
* @return the call signaling room
*/
Room getCallSignalingRoom();
/**
* @return the session
*/
MXSession getSession();
/**
* @return true if the call is an incoming call.
*/
boolean isIncoming();
/**
* Set the call type: video or voice
*
* @param isVideo true for video call, false for VoIP
*/
void setIsVideo(boolean isVideo);
/**
* @return true if the call is a video call.
*/
boolean isVideo();
/**
* Defines the call conference status
*
* @param isConference the conference status
*/
void setIsConference(boolean isConference);
/**
* @return true if the call is a conference call.
*/
boolean isConference();
/**
* @return the callstate (must be a CALL_STATE_XX value)
*/
String getCallState();
/**
* @return the callView
*/
View getCallView();
/**
* @return the callView visibility
*/
int getVisibility();
/**
* Set the callview visibility
*
* @param visibility true to make the callview visible
* @return true if the operation succeeds
*/
boolean setVisibility(int visibility);
/**
* @return the call start time in ms since epoch, -1 if not defined.
*/
long getCallStartTime();
/**
* @return the call elapsed time in seconds, -1 if not defined.
*/
long getCallElapsedTime();
/**
* Switch between device cameras. The transmitted stream is modified
* according to the new camera in use.
* If the camera used in the video call is the front one, calling
* switchRearFrontCamera(), will make the rear one to be used, and vice versa.
* If only one camera is available, nothing is done.
*
* @return true if the switch succeed, false otherwise.
*/
boolean switchRearFrontCamera();
/**
* Indicate if a camera switch was performed or not.
* For some reason switching the camera from front to rear and
* vice versa, could not be performed (ie. only one camera is available).
* <p>
* <br>See {@link #switchRearFrontCamera()}.
*
* @return true if camera was switched, false otherwise
*/
boolean isCameraSwitched();
/**
* Indicate if the device supports camera switching.
* <p>See {@link #switchRearFrontCamera()}.
*
* @return true if switch camera is supported, false otherwise
*/
boolean isSwitchCameraSupported();
/**
* Mute/Unmute the recording of the local video attendee. Set isVideoMuted
* to true to enable the recording of the video, if set to false no recording
* is performed.
*
* @param isVideoMuted true to mute the video recording, false to unmute
*/
void muteVideoRecording(boolean isVideoMuted);
/**
* Return the recording mute status of the local video attendee.
* <p>
* <br>See {@link #muteVideoRecording(boolean)}.
*
* @return true if video recording is muted, false otherwise
*/
boolean isVideoRecordingMuted();
}

View file

@ -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);
}

View file

@ -0,0 +1,61 @@
/*
* Copyright 2015 OpenMarket Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.call;
import im.vector.matrix.android.internal.legacy.crypto.data.MXDeviceInfo;
import im.vector.matrix.android.internal.legacy.crypto.data.MXUsersDevicesMap;
/**
* This class manages the calls events.
*/
public interface IMXCallsManagerListener {
/**
* Called when there is an incoming call within the room.
*
* @param call the incoming call
* @param unknownDevices the unknown e2e devices list
*/
void onIncomingCall(IMXCall call, MXUsersDevicesMap<MXDeviceInfo> unknownDevices);
/**
* An outgoing call is started.
*
* @param call the outgoing call
*/
void onOutgoingCall(IMXCall call);
/**
* Called when a called has been hung up
*
* @param call the incoming call
*/
void onCallHangUp(IMXCall call);
/**
* A voip conference started in a room.
*
* @param roomId the room id
*/
void onVoipConferenceStarted(String roomId);
/**
* A voip conference finished in a room.
*
* @param roomId the room id
*/
void onVoipConferenceFinished(String roomId);
}

View file

@ -0,0 +1,706 @@
/*
* Copyright 2015 OpenMarket Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.call;
import android.content.Context;
import android.os.Handler;
import android.text.TextUtils;
import android.view.View;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import im.vector.matrix.android.internal.legacy.MXSession;
import im.vector.matrix.android.internal.legacy.data.Room;
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Timer;
/**
* This class is the default implementation
*/
public class MXCall implements IMXCall {
private static final String LOG_TAG = MXCall.class.getSimpleName();
// defines the call timeout
public static final int CALL_TIMEOUT_MS = 120 * 1000;
/**
* The session
*/
protected MXSession mSession;
/**
* The context
*/
protected Context mContext;
/**
* the turn servers
*/
protected JsonElement mTurnServer;
/**
* The room in which the call is performed.
*/
protected Room mCallingRoom;
/**
* The room in which the call events are sent.
* It might differ from mCallingRoom if it is a conference call.
* For a 1:1 call, it will be equal to mCallingRoom.
*/
protected Room mCallSignalingRoom;
/**
* The call events listeners
*/
private final Set<IMXCallListener> mCallListeners = new HashSet<>();
/**
* the call id
*/
protected String mCallId;
/**
* Tells if it is a video call
*/
protected boolean mIsVideoCall = false;
/**
* Tells if it is an incoming call
*/
protected boolean mIsIncoming = false;
/**
* Tells if it is a conference call.
*/
private boolean mIsConference = false;
/**
* List of events to sends to mCallSignalingRoom
*/
protected final List<Event> mPendingEvents = new ArrayList<>();
/**
* The sending eevent.
*/
private Event mPendingEvent;
/**
* The not responding timer
*/
protected Timer mCallTimeoutTimer;
// call start time
private long mStartTime = -1;
// UI thread handler
final Handler mUIThreadHandler = new Handler();
/**
* Create the call view
*/
public void createCallView() {
}
/**
* The activity is paused.
*/
public void onPause() {
}
/**
* The activity is resumed.
*/
public void onResume() {
}
// actions (must be done after dispatchOnViewReady()
/**
* Start a call.
*/
public void placeCall(VideoLayoutConfiguration aLocalVideoPosition) {
}
/**
* Prepare a call reception.
*
* @param aCallInviteParams the invitation Event content
* @param aCallId the call ID
* @param aLocalVideoPosition position of the local video attendee
*/
public void prepareIncomingCall(JsonObject aCallInviteParams, String aCallId, VideoLayoutConfiguration aLocalVideoPosition) {
setIsIncoming(true);
}
/**
* The call has been detected as an incoming one.
* The application launched the dedicated activity and expects to launch the incoming call.
*
* @param aLocalVideoPosition position of the local video attendee
*/
public void launchIncomingCall(VideoLayoutConfiguration aLocalVideoPosition) {
}
@Override
public void updateLocalVideoRendererPosition(VideoLayoutConfiguration aLocalVideoPosition) {
Log.w(LOG_TAG, "## updateLocalVideoRendererPosition(): not implemented");
}
@Override
public boolean switchRearFrontCamera() {
Log.w(LOG_TAG, "## switchRearFrontCamera(): not implemented");
return false;
}
@Override
public boolean isCameraSwitched() {
Log.w(LOG_TAG, "## isCameraSwitched(): not implemented");
return false;
}
@Override
public boolean isSwitchCameraSupported() {
Log.w(LOG_TAG, "## isSwitchCameraSupported(): not implemented");
return false;
}
// events thread
/**
* Manage the call events.
*
* @param event the call event.
*/
public void handleCallEvent(Event event) {
}
// user actions
/**
* The call is accepted.
*/
public void answer() {
}
/**
* The call has been has answered on another device.
*/
public void onAnsweredElsewhere() {
}
/**
* The call is hung up.
*/
public void hangup(String reason) {
}
// getters / setters
/**
* @return the callId
*/
public String getCallId() {
return mCallId;
}
/**
* Set the callId
*/
public void setCallId(String callId) {
mCallId = callId;
}
/**
* @return the linked room
*/
public Room getRoom() {
return mCallingRoom;
}
/**
* @return the call signaling room
*/
public Room getCallSignalingRoom() {
return mCallSignalingRoom;
}
/**
* Set the linked rooms.
*
* @param room the room where the conference take place
* @param callSignalingRoom the call signaling room.
*/
public void setRooms(Room room, Room callSignalingRoom) {
mCallingRoom = room;
mCallSignalingRoom = callSignalingRoom;
}
/**
* @return the session
*/
public MXSession getSession() {
return mSession;
}
/**
* @return true if the call is an incoming call.
*/
public boolean isIncoming() {
return mIsIncoming;
}
/**
* @param isIncoming true if the call is an incoming one.
*/
private void setIsIncoming(boolean isIncoming) {
mIsIncoming = isIncoming;
}
/**
* Defines the call type
*/
public void setIsVideo(boolean isVideo) {
mIsVideoCall = isVideo;
}
/**
* @return true if the call is a video call.
*/
public boolean isVideo() {
return mIsVideoCall;
}
/**
* Defines the call conference status
*/
public void setIsConference(boolean isConference) {
mIsConference = isConference;
}
/**
* @return true if the call is a conference call.
*/
public boolean isConference() {
return mIsConference;
}
/**
* @return the callstate (must be a CALL_STATE_XX value)
*/
public String getCallState() {
return null;
}
/**
* @return the callView
*/
public View getCallView() {
return null;
}
/**
* @return the callView visibility
*/
public int getVisibility() {
return View.GONE;
}
/**
* Set the callview visibility
*
* @return true if the operation succeeds
*/
public boolean setVisibility(int visibility) {
return false;
}
/**
* @return if the call is ended.
*/
public boolean isCallEnded() {
return TextUtils.equals(CALL_STATE_ENDED, getCallState());
}
/**
* @return the call start time in ms since epoch, -1 if not defined.
*/
public long getCallStartTime() {
return mStartTime;
}
/**
* @return the call elapsed time in seconds, -1 if not defined.
*/
public long getCallElapsedTime() {
if (-1 == mStartTime) {
return -1;
}
return (System.currentTimeMillis() - mStartTime) / 1000;
}
//==============================================================================================================
// call events listener
//==============================================================================================================
/**
* Add a listener.
*
* @param callListener the listener to add
*/
public void addListener(IMXCallListener callListener) {
if (null != callListener) {
synchronized (LOG_TAG) {
mCallListeners.add(callListener);
}
}
}
/**
* Remove a listener
*
* @param callListener the listener to remove
*/
public void removeListener(IMXCallListener callListener) {
if (null != callListener) {
synchronized (LOG_TAG) {
mCallListeners.remove(callListener);
}
}
}
/**
* Remove the listeners
*/
public void clearListeners() {
synchronized (LOG_TAG) {
mCallListeners.clear();
}
}
/**
* @return the call listeners
*/
private Collection<IMXCallListener> getCallListeners() {
Collection<IMXCallListener> listeners;
synchronized (LOG_TAG) {
listeners = new HashSet<>(mCallListeners);
}
return listeners;
}
/**
* Dispatch the onCallViewCreated event to the listeners.
*
* @param callView the call view
*/
protected void dispatchOnCallViewCreated(View callView) {
if (isCallEnded()) {
Log.d(LOG_TAG, "## dispatchOnCallViewCreated(): the call is ended");
return;
}
Log.d(LOG_TAG, "## dispatchOnCallViewCreated()");
Collection<IMXCallListener> listeners = getCallListeners();
for (IMXCallListener listener : listeners) {
try {
listener.onCallViewCreated(callView);
} catch (Exception e) {
Log.e(LOG_TAG, "## dispatchOnCallViewCreated(): Exception Msg=" + e.getMessage(), e);
}
}
}
/**
* Dispatch the onViewReady event to the listeners.
*/
protected void dispatchOnReady() {
if (isCallEnded()) {
Log.d(LOG_TAG, "## dispatchOnReady() : the call is ended");
return;
}
Log.d(LOG_TAG, "## dispatchOnReady()");
Collection<IMXCallListener> listeners = getCallListeners();
for (IMXCallListener listener : listeners) {
try {
listener.onReady();
} catch (Exception e) {
Log.e(LOG_TAG, "## dispatchOnReady(): Exception Msg=" + e.getMessage(), e);
}
}
}
/**
* Dispatch the onCallError event to the listeners.
*
* @param error error message
*/
protected void dispatchOnCallError(String error) {
if (isCallEnded()) {
Log.d(LOG_TAG, "## dispatchOnCallError() : the call is ended");
return;
}
Log.d(LOG_TAG, "## dispatchOnCallError()");
Collection<IMXCallListener> listeners = getCallListeners();
for (IMXCallListener listener : listeners) {
try {
listener.onCallError(error);
} catch (Exception e) {
Log.e(LOG_TAG, "## dispatchOnCallError(): " + e.getMessage(), e);
}
}
}
/**
* Dispatch the onStateDidChange event to the listeners.
*
* @param newState the new state
*/
protected void dispatchOnStateDidChange(String newState) {
Log.d(LOG_TAG, "## dispatchOnCallErrorOnStateDidChange(): " + newState);
// set the call start time
if (TextUtils.equals(CALL_STATE_CONNECTED, newState) && (-1 == mStartTime)) {
mStartTime = System.currentTimeMillis();
}
// the call is ended.
if (TextUtils.equals(CALL_STATE_ENDED, newState)) {
mStartTime = -1;
}
Collection<IMXCallListener> listeners = getCallListeners();
for (IMXCallListener listener : listeners) {
try {
listener.onStateDidChange(newState);
} catch (Exception e) {
Log.e(LOG_TAG, "## dispatchOnStateDidChange(): Exception Msg=" + e.getMessage(), e);
}
}
}
/**
* Dispatch the onCallAnsweredElsewhere event to the listeners.
*/
protected void dispatchAnsweredElsewhere() {
Log.d(LOG_TAG, "## dispatchAnsweredElsewhere()");
Collection<IMXCallListener> listeners = getCallListeners();
for (IMXCallListener listener : listeners) {
try {
listener.onCallAnsweredElsewhere();
} catch (Exception e) {
Log.e(LOG_TAG, "## dispatchAnsweredElsewhere(): Exception Msg=" + e.getMessage(), e);
}
}
}
/**
* Dispatch the onCallEnd event to the listeners.
*
* @param aEndCallReasonId the reason of the call ending
*/
protected void dispatchOnCallEnd(int aEndCallReasonId) {
Log.d(LOG_TAG, "## dispatchOnCallEnd(): endReason=" + aEndCallReasonId);
Collection<IMXCallListener> listeners = getCallListeners();
for (IMXCallListener listener : listeners) {
try {
listener.onCallEnd(aEndCallReasonId);
} catch (Exception e) {
Log.e(LOG_TAG, "## dispatchOnCallEnd(): Exception Msg=" + e.getMessage(), e);
}
}
}
/**
* Send the next pending events
*/
protected void sendNextEvent() {
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
// do not send any new message
if (isCallEnded() && (null != mPendingEvents)) {
mPendingEvents.clear();
}
// ready to send
if ((null == mPendingEvent) && (0 != mPendingEvents.size())) {
mPendingEvent = mPendingEvents.get(0);
mPendingEvents.remove(mPendingEvent);
Log.d(LOG_TAG, "## sendNextEvent() : sending event of type " + mPendingEvent.getType() + " event id " + mPendingEvent.eventId);
mCallSignalingRoom.sendEvent(mPendingEvent, new ApiCallback<Void>() {
@Override
public void onSuccess(Void info) {
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
Log.d(LOG_TAG, "## sendNextEvent() : event " + mPendingEvent.eventId + " is sent");
mPendingEvent = null;
sendNextEvent();
}
});
}
private void commonFailure(String reason) {
Log.d(LOG_TAG, "## sendNextEvent() : event " + mPendingEvent.eventId + " failed to be sent " + reason);
// let try next candidate event
if (TextUtils.equals(mPendingEvent.getType(), Event.EVENT_TYPE_CALL_CANDIDATES)) {
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
mPendingEvent = null;
sendNextEvent();
}
});
} else {
hangup(reason);
}
}
@Override
public void onNetworkError(Exception e) {
commonFailure(e.getLocalizedMessage());
}
@Override
public void onMatrixError(MatrixError e) {
commonFailure(e.getLocalizedMessage());
}
@Override
public void onUnexpectedError(Exception e) {
commonFailure(e.getLocalizedMessage());
}
});
}
}
});
}
/**
* Dispatch the onPreviewSizeChanged event to the listeners.
*
* @param width the preview width
* @param height the preview height
*/
protected void dispatchOnPreviewSizeChanged(int width, int height) {
Log.d(LOG_TAG, "## dispatchOnPreviewSizeChanged(): width =" + width + " - height =" + height);
Collection<IMXCallListener> listeners = getCallListeners();
for (IMXCallListener listener : listeners) {
try {
listener.onPreviewSizeChanged(width, height);
} catch (Exception e) {
Log.e(LOG_TAG, "## dispatchOnPreviewSizeChanged(): Exception Msg=" + e.getMessage(), e);
}
}
}
/**
* send an hang up event
*
* @param reason the reason
*/
protected void sendHangup(String reason) {
JsonObject hangupContent = new JsonObject();
hangupContent.add("version", new JsonPrimitive(0));
hangupContent.add("call_id", new JsonPrimitive(mCallId));
if (!TextUtils.isEmpty(reason)) {
hangupContent.add("reason", new JsonPrimitive(reason));
}
Event event = new Event(Event.EVENT_TYPE_CALL_HANGUP, hangupContent, mSession.getCredentials().userId, mCallSignalingRoom.getRoomId());
// local notification to indicate the end of call
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
dispatchOnCallEnd(END_CALL_REASON_USER_HIMSELF);
}
});
Log.d(LOG_TAG, "## sendHangup(): reason=" + reason);
// send hang up event to the server
mCallSignalingRoom.sendEvent(event, new ApiCallback<Void>() {
@Override
public void onSuccess(Void info) {
Log.d(LOG_TAG, "## sendHangup(): onSuccess");
}
@Override
public void onNetworkError(Exception e) {
Log.e(LOG_TAG, "## sendHangup(): onNetworkError Msg=" + e.getMessage(), e);
}
@Override
public void onMatrixError(MatrixError e) {
Log.e(LOG_TAG, "## sendHangup(): onMatrixError Msg=" + e.getMessage());
}
@Override
public void onUnexpectedError(Exception e) {
Log.e(LOG_TAG, "## sendHangup(): onUnexpectedError Msg=" + e.getMessage(), e);
}
});
}
@Override
public void muteVideoRecording(boolean isVideoMuted) {
Log.w(LOG_TAG, "## muteVideoRecording(): not implemented");
}
@Override
public boolean isVideoRecordingMuted() {
Log.w(LOG_TAG, "## muteVideoRecording(): not implemented - default value = false");
return false;
}
}

View file

@ -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) {
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright 2015 OpenMarket Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.call;
import im.vector.matrix.android.internal.legacy.crypto.data.MXDeviceInfo;
import im.vector.matrix.android.internal.legacy.crypto.data.MXUsersDevicesMap;
/**
* This class is the default implementation of MXCallsManagerListener
*/
public class MXCallsManagerListener implements IMXCallsManagerListener {
@Override
public void onIncomingCall(IMXCall call, MXUsersDevicesMap<MXDeviceInfo> unknownDevices) {
}
@Override
public void onOutgoingCall(IMXCall call) {
}
@Override
public void onCallHangUp(IMXCall call) {
}
@Override
public void onVoipConferenceStarted(String roomId) {
}
@Override
public void onVoipConferenceFinished(String roomId) {
}
}

View file

@ -0,0 +1,687 @@
/*
* Copyright 2015 OpenMarket Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.call;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
import android.text.TextUtils;
import android.view.View;
import android.webkit.JavascriptInterface;
import android.webkit.PermissionRequest;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import im.vector.matrix.android.internal.legacy.MXSession;
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.util.Timer;
import java.util.TimerTask;
public class MXChromeCall extends MXCall {
private static final String LOG_TAG = MXChromeCall.class.getSimpleName();
private WebView mWebView = null;
private CallWebAppInterface mCallWebAppInterface = null;
private boolean mIsIncomingPrepared = false;
private JsonObject mCallInviteParams = null;
private JsonArray mPendingCandidates = new JsonArray();
/**
* @return true if this stack can perform calls.
*/
public static boolean isSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
}
// creator
public MXChromeCall(MXSession session, Context context, JsonElement turnServer) {
if (!isSupported()) {
throw new AssertionError("MXChromeCall : not supported with the current android version");
}
if (null == session) {
throw new AssertionError("MXChromeCall : session cannot be null");
}
if (null == context) {
throw new AssertionError("MXChromeCall : context cannot be null");
}
mCallId = "c" + System.currentTimeMillis();
mSession = session;
mContext = context;
mTurnServer = turnServer;
}
@Override
@SuppressLint("NewApi")
public void createCallView() {
super.createCallView();
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
mWebView = new WebView(mContext);
mWebView.setBackgroundColor(Color.BLACK);
// warn that the webview must be added in an activity/fragment
dispatchOnCallViewCreated(mWebView);
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
mCallWebAppInterface = new CallWebAppInterface();
mWebView.addJavascriptInterface(mCallWebAppInterface, "Android");
WebView.setWebContentsDebuggingEnabled(true);
WebSettings settings = mWebView.getSettings();
// Enable Javascript
settings.setJavaScriptEnabled(true);
// Use WideViewport and Zoom out if there is no viewport defined
settings.setUseWideViewPort(true);
settings.setLoadWithOverviewMode(true);
// Enable pinch to zoom without the zoom buttons
settings.setBuiltInZoomControls(true);
// Allow use of Local Storage
settings.setDomStorageEnabled(true);
settings.setAllowFileAccessFromFileURLs(true);
settings.setAllowUniversalAccessFromFileURLs(true);
settings.setDisplayZoomControls(false);
mWebView.setWebViewClient(new WebViewClient());
// AppRTC requires third party cookies to work
android.webkit.CookieManager cookieManager = android.webkit.CookieManager.getInstance();
cookieManager.setAcceptThirdPartyCookies(mWebView, true);
final String url = "file:///android_asset/www/call.html";
mWebView.loadUrl(url);
mWebView.setWebChromeClient(new WebChromeClient() {
@Override
public void onPermissionRequest(final PermissionRequest request) {
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
request.grant(request.getResources());
}
});
}
});
}
});
}
});
}
/**
* Start a call.
*/
@Override
public void placeCall(VideoLayoutConfiguration aLocalVideoPosition) {
super.placeCall(aLocalVideoPosition);
if (CALL_STATE_READY.equals(getCallState())) {
mIsIncoming = false;
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
mWebView.loadUrl(mIsVideoCall ? "javascript:placeVideoCall()" : "javascript:placeVoiceCall()");
}
});
}
}
/**
* Prepare a call reception.
*
* @param aCallInviteParams the invitation Event content
* @param aCallId the call ID
* @param aLocalVideoPosition position of the local video attendee
*/
@Override
public void prepareIncomingCall(final JsonObject aCallInviteParams, final String aCallId, VideoLayoutConfiguration aLocalVideoPosition) {
Log.d(LOG_TAG, "## prepareIncomingCall : call state " + getCallState());
super.prepareIncomingCall(aCallInviteParams, aCallId, aLocalVideoPosition);
mCallId = aCallId;
if (CALL_STATE_READY.equals(getCallState())) {
mIsIncoming = true;
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
mWebView.loadUrl("javascript:initWithInvite('" + aCallId + "'," + aCallInviteParams.toString() + ")");
mIsIncomingPrepared = true;
mWebView.post(new Runnable() {
@Override
public void run() {
checkPendingCandidates();
}
});
}
});
} else if (CALL_STATE_CREATED.equals(getCallState())) {
mCallInviteParams = aCallInviteParams;
// detect call type from the sdp
try {
JsonObject offer = mCallInviteParams.get("offer").getAsJsonObject();
JsonElement sdp = offer.get("sdp");
String sdpValue = sdp.getAsString();
setIsVideo(sdpValue.contains("m=video"));
} catch (Exception e) {
Log.e(LOG_TAG, "## prepareIncomingCall() ; " + e.getMessage(), e);
}
}
}
/**
* The call has been detected as an incoming one.
* The application launched the dedicated activity and expects to launch the incoming call.
*
* @param aLocalVideoPosition local video position
*/
@Override
public void launchIncomingCall(VideoLayoutConfiguration aLocalVideoPosition) {
super.launchIncomingCall(aLocalVideoPosition);
if (CALL_STATE_READY.equals(getCallState())) {
prepareIncomingCall(mCallInviteParams, mCallId, null);
}
}
/**
* The callee accepts the call.
*
* @param event the event
*/
private void onCallAnswer(final Event event) {
if (!CALL_STATE_CREATED.equals(getCallState()) && (null != mWebView)) {
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
mWebView.loadUrl("javascript:receivedAnswer(" + event.getContent().toString() + ")");
}
});
}
}
/**
* The other call member hangs up the call.
*
* @param event the event
*/
private void onCallHangup(final Event event) {
if (!CALL_STATE_CREATED.equals(getCallState()) && (null != mWebView)) {
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
mWebView.loadUrl("javascript:onHangupReceived(" + event.getContent().toString() + ")");
mWebView.post(new Runnable() {
@Override
public void run() {
dispatchOnCallEnd(END_CALL_REASON_PEER_HANG_UP);
}
});
}
});
}
}
/**
* A new Ice candidate is received
*
* @param candidates the ice candidates
*/
public void onNewCandidates(final JsonElement candidates) {
if (!CALL_STATE_CREATED.equals(getCallState()) && (null != mWebView)) {
mWebView.post(new Runnable() {
@Override
public void run() {
mWebView.loadUrl("javascript:gotRemoteCandidates(" + candidates.toString() + ")");
}
});
}
}
/**
* Add ice candidates
*
* @param candidates ic candidates
*/
private void addCandidates(JsonArray candidates) {
if (mIsIncomingPrepared || !isIncoming()) {
onNewCandidates(candidates);
} else {
synchronized (LOG_TAG) {
mPendingCandidates.addAll(candidates);
}
}
}
/**
* Some Ice candidates could have been received while creating the call view.
* Check if some of them have been defined.
*/
public void checkPendingCandidates() {
synchronized (LOG_TAG) {
onNewCandidates(mPendingCandidates);
mPendingCandidates = new JsonArray();
}
}
// events thread
/**
* Manage the call events.
*
* @param event the call event.
*/
@Override
public void handleCallEvent(Event event) {
super.handleCallEvent(event);
String eventType = event.getType();
if (event.isCallEvent()) {
// event from other member
if (!TextUtils.equals(event.getSender(), mSession.getMyUserId())) {
if (Event.EVENT_TYPE_CALL_ANSWER.equals(eventType) && !mIsIncoming) {
onCallAnswer(event);
} else if (Event.EVENT_TYPE_CALL_CANDIDATES.equals(eventType)) {
JsonArray candidates = event.getContentAsJsonObject().getAsJsonArray("candidates");
addCandidates(candidates);
} else if (Event.EVENT_TYPE_CALL_HANGUP.equals(eventType)) {
onCallHangup(event);
}
} else if (Event.EVENT_TYPE_CALL_INVITE.equals(eventType)) {
// server echo : assume that the other device is ringing
mCallWebAppInterface.mCallState = IMXCall.CALL_STATE_RINGING;
// warn in the UI thread
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
dispatchOnStateDidChange(mCallWebAppInterface.mCallState);
}
});
} else if (Event.EVENT_TYPE_CALL_ANSWER.equals(eventType)) {
// check if the call has not been answer in another device
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
// ring on this side
if (getCallState().equals(IMXCall.CALL_STATE_RINGING)) {
onAnsweredElsewhere();
}
}
});
}
}
}
// user actions
/**
* The call is accepted.
*/
@Override
public void answer() {
super.answer();
if (!CALL_STATE_CREATED.equals(getCallState()) && (null != mWebView)) {
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
mWebView.loadUrl("javascript:answerCall()");
}
});
}
}
/**
* The call is hung up.
*/
@Override
public void hangup(String reason) {
super.hangup(reason);
if (!CALL_STATE_CREATED.equals(getCallState()) && (null != mWebView)) {
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
mWebView.loadUrl("javascript:hangup()");
}
});
} else {
sendHangup(reason);
}
}
// getters / setters
/**
* @return the callstate (must be a CALL_STATE_XX value)
*/
@Override
public String getCallState() {
if (null != mCallWebAppInterface) {
return mCallWebAppInterface.mCallState;
} else {
return CALL_STATE_CREATED;
}
}
/**
* @return the callView
*/
@Override
public View getCallView() {
return mWebView;
}
/**
* @return the callView visibility
*/
@Override
public int getVisibility() {
if (null != mWebView) {
return mWebView.getVisibility();
} else {
return View.GONE;
}
}
/**
* Set the callview visibility
*
* @return true if the operation succeeds
*/
public boolean setVisibility(int visibility) {
if (null != mWebView) {
mWebView.setVisibility(visibility);
return true;
}
return false;
}
@Override
public void onAnsweredElsewhere() {
super.onAnsweredElsewhere();
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
mWebView.loadUrl("javascript:onAnsweredElsewhere()");
}
});
dispatchAnsweredElsewhere();
}
// private class
private class CallWebAppInterface {
public String mCallState = CALL_STATE_CREATING_CALL_VIEW;
private Timer mCallTimeoutTimer = null;
CallWebAppInterface() {
if (null == mCallingRoom) {
throw new AssertionError("MXChromeCall : room cannot be null");
}
}
// JS <-> android calls
@JavascriptInterface
public String wgetCallId() {
return mCallId;
}
@JavascriptInterface
public String wgetRoomId() {
return mCallSignalingRoom.getRoomId();
}
@JavascriptInterface
public String wgetTurnServer() {
if (null != mTurnServer) {
return mTurnServer.toString();
} else {
return null;
}
}
@JavascriptInterface
public void wlog(String message) {
Log.d(LOG_TAG, "WebView Message : " + message);
}
@JavascriptInterface
public void wCallError(String message) {
Log.e(LOG_TAG, "WebView error Message : " + message);
if ("ice_failed".equals(message)) {
dispatchOnCallError(CALL_ERROR_ICE_FAILED);
} else if ("user_media_failed".equals(message)) {
dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
}
}
@JavascriptInterface
public void wOnStateUpdate(String jsstate) {
String nextState = null;
if ("fledgling".equals(jsstate)) {
nextState = CALL_STATE_READY;
} else if ("wait_local_media".equals(jsstate)) {
nextState = CALL_STATE_WAIT_LOCAL_MEDIA;
} else if ("create_offer".equals(jsstate)) {
nextState = CALL_STATE_WAIT_CREATE_OFFER;
} else if ("invite_sent".equals(jsstate)) {
nextState = CALL_STATE_INVITE_SENT;
} else if ("ringing".equals(jsstate)) {
nextState = CALL_STATE_RINGING;
} else if ("create_answer".equals(jsstate)) {
nextState = CALL_STATE_CREATE_ANSWER;
} else if ("connecting".equals(jsstate)) {
nextState = CALL_STATE_CONNECTING;
} else if ("connected".equals(jsstate)) {
nextState = CALL_STATE_CONNECTED;
} else if ("ended".equals(jsstate)) {
nextState = CALL_STATE_ENDED;
}
// is there any state update ?
if ((null != nextState) && !mCallState.equals(nextState)) {
mCallState = nextState;
// warn in the UI thread
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
// call timeout management
if (CALL_STATE_CONNECTING.equals(mCallState) || CALL_STATE_CONNECTING.equals(mCallState)) {
if (null != mCallTimeoutTimer) {
mCallTimeoutTimer.cancel();
mCallTimeoutTimer = null;
}
}
dispatchOnStateDidChange(mCallState);
}
});
}
}
@JavascriptInterface
public void wOnLoaded() {
mCallState = CALL_STATE_READY;
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
dispatchOnReady();
}
});
}
private void sendHangup(final Event event) {
if (null != mCallTimeoutTimer) {
mCallTimeoutTimer.cancel();
mCallTimeoutTimer = null;
}
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
dispatchOnCallEnd(END_CALL_REASON_UNDEFINED);
}
});
mPendingEvents.clear();
mCallSignalingRoom.sendEvent(event, new ApiCallback<Void>() {
@Override
public void onSuccess(Void info) {
}
@Override
public void onNetworkError(Exception e) {
// try again
sendHangup(event);
}
@Override
public void onMatrixError(MatrixError e) {
}
@Override
public void onUnexpectedError(Exception e) {
}
});
}
@JavascriptInterface
public void wSendEvent(final String roomId, final String eventType, final String jsonContent) {
mUIThreadHandler.post(new Runnable() {
@Override
public void run() {
try {
boolean addIt = true;
JsonObject content = (JsonObject) new JsonParser().parse(jsonContent);
// merge candidates
if (TextUtils.equals(eventType, Event.EVENT_TYPE_CALL_CANDIDATES) && (mPendingEvents.size() > 0)) {
try {
Event lastEvent = mPendingEvents.get(mPendingEvents.size() - 1);
if (TextUtils.equals(lastEvent.getType(), Event.EVENT_TYPE_CALL_CANDIDATES)) {
JsonObject lastContent = lastEvent.getContentAsJsonObject();
JsonArray lastContentCandidates = lastContent.get("candidates").getAsJsonArray();
JsonArray newContentCandidates = content.get("candidates").getAsJsonArray();
Log.d(LOG_TAG, "Merge candidates from " + lastContentCandidates.size()
+ " to " + (lastContentCandidates.size() + newContentCandidates.size() + " items."));
lastContentCandidates.addAll(newContentCandidates);
lastContent.remove("candidates");
lastContent.add("candidates", lastContentCandidates);
addIt = false;
}
} catch (Exception e) {
Log.e(LOG_TAG, "## wSendEvent() ; " + e.getMessage(), e);
}
}
if (addIt) {
Event event = new Event(eventType, content, mSession.getCredentials().userId, mCallSignalingRoom.getRoomId());
if (null != event) {
// receive an hangup -> close the window asap
if (TextUtils.equals(eventType, Event.EVENT_TYPE_CALL_HANGUP)) {
sendHangup(event);
} else {
mPendingEvents.add(event);
}
// the calleee has 30s to answer to call
if (TextUtils.equals(eventType, Event.EVENT_TYPE_CALL_INVITE)) {
try {
mCallTimeoutTimer = new Timer();
mCallTimeoutTimer.schedule(new TimerTask() {
@Override
public void run() {
try {
if (getCallState().equals(IMXCall.CALL_STATE_RINGING)
|| getCallState().equals(IMXCall.CALL_STATE_INVITE_SENT)) {
dispatchOnCallError(CALL_ERROR_USER_NOT_RESPONDING);
hangup(null);
}
// cancel the timer
mCallTimeoutTimer.cancel();
mCallTimeoutTimer = null;
} catch (Exception e) {
Log.e(LOG_TAG, "## wSendEvent() ; " + e.getMessage(), e);
}
}
}, CALL_TIMEOUT_MS);
} catch (Throwable throwable) {
if (null != mCallTimeoutTimer) {
mCallTimeoutTimer.cancel();
mCallTimeoutTimer = null;
}
Log.e(LOG_TAG, "## wSendEvent() ; " + throwable.getMessage(), throwable);
}
}
}
}
// send events
sendNextEvent();
} catch (Exception e) {
Log.e(LOG_TAG, "## wSendEvent() ; " + e.getMessage(), e);
}
}
});
}
}
}

View file

@ -0,0 +1,520 @@
package im.vector.matrix.android.internal.legacy.call;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Point;
import android.support.v4.view.ViewCompat;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import com.oney.WebRTCModule.EglUtils;
import com.oney.WebRTCModule.WebRTCView;
import org.webrtc.EglBase;
import org.webrtc.MediaStream;
import org.webrtc.RendererCommon;
import org.webrtc.RendererCommon.RendererEvents;
import org.webrtc.RendererCommon.ScalingType;
import org.webrtc.SurfaceViewRenderer;
import org.webrtc.VideoRenderer;
import org.webrtc.VideoTrack;
import java.lang.reflect.Method;
import java.util.List;
/**
* Use the older implementation of WebRtcView.
* The latest version a stream URL instead of a stream.
* It implies to have a React context.
*/
public class MXWebRtcView extends ViewGroup {
/**
* The scaling type to be utilized by default.
* <p>
* The default value is in accord with
* https://www.w3.org/TR/html5/embedded-content-0.html#the-video-element:
* <p>
* In the absence of style rules to the contrary, video content should be
* rendered inside the element's playback area such that the video content
* is shown centered in the playback area at the largest possible size that
* fits completely within it, with the video content's aspect ratio being
* preserved. Thus, if the aspect ratio of the playback area does not match
* the aspect ratio of the video, the video will be shown letterboxed or
* pillarboxed. Areas of the element's playback area that do not contain the
* video represent nothing.
*/
private static final ScalingType DEFAULT_SCALING_TYPE
= ScalingType.SCALE_ASPECT_FIT;
/**
* {@link View#isInLayout()} as a <tt>Method</tt> to be invoked via
* reflection in order to accommodate its lack of availability before API
* level 18. {@link ViewCompat#isInLayout(View)} is the best solution but I
* could not make it available along with
* {@link ViewCompat#isAttachedToWindow(View)} at the time of this writing.
*/
private static final Method IS_IN_LAYOUT;
private static final String LOG_TAG = MXWebRtcView.class.getSimpleName();
static {
// IS_IN_LAYOUT
Method isInLayout = null;
try {
Method m = MXWebRtcView.class.getMethod("isInLayout");
if (boolean.class.isAssignableFrom(m.getReturnType())) {
isInLayout = m;
}
} catch (NoSuchMethodException e) {
// Fall back to the behavior of ViewCompat#isInLayout(View).
}
IS_IN_LAYOUT = isInLayout;
}
/**
* The height of the last video frame rendered by
* {@link #surfaceViewRenderer}.
*/
private int frameHeight;
/**
* The rotation (degree) of the last video frame rendered by
* {@link #surfaceViewRenderer}.
*/
private int frameRotation;
/**
* The width of the last video frame rendered by
* {@link #surfaceViewRenderer}.
*/
private int frameWidth;
/**
* The {@code Object} which synchronizes the access to the layout-related
* state of this instance such as {@link #frameHeight},
* {@link #frameRotation}, {@link #frameWidth}, and {@link #scalingType}.
*/
private final Object layoutSyncRoot = new Object();
/**
* The indicator which determines whether this {@code WebRTCView} is to
* mirror the video represented by {@link #videoTrack} during its rendering.
*/
private boolean mirror;
/**
* The {@code RendererEvents} which listens to rendering events reported by
* {@link #surfaceViewRenderer}.
*/
private final RendererEvents rendererEvents
= new RendererEvents() {
@Override
public void onFirstFrameRendered() {
}
@Override
public void onFrameResolutionChanged(
int videoWidth, int videoHeight,
int rotation) {
MXWebRtcView.this.onFrameResolutionChanged(
videoWidth, videoHeight,
rotation);
}
};
/**
* The {@code Runnable} representation of
* {@link #requestSurfaceViewRendererLayout()}. Explicitly defined in order
* to allow the use of the latter with {@link #post(Runnable)} without
* initializing new instances on every (method) call.
*/
private final Runnable requestSurfaceViewRendererLayoutRunnable
= new Runnable() {
@Override
public void run() {
requestSurfaceViewRendererLayout();
}
};
/**
* The scaling type this {@code WebRTCView} is to apply to the video
* represented by {@link #videoTrack} during its rendering. An expression of
* the CSS property {@code object-fit} in the terms of WebRTC.
*/
private ScalingType scalingType;
/**
* The {@link View} and {@link VideoRenderer} implementation which
* actually renders {@link #videoTrack} on behalf of this instance.
*/
private final SurfaceViewRenderer surfaceViewRenderer;
/**
* The {@code VideoRenderer}, if any, which renders {@link #videoTrack} on
* this {@code View}.
*/
private VideoRenderer videoRenderer;
/**
* The {@code VideoTrack}, if any, rendered by this {@code MXWebRTCView}.
*/
private VideoTrack videoTrack;
public MXWebRtcView(Context context) {
super(context);
surfaceViewRenderer = new SurfaceViewRenderer(context);
addView(surfaceViewRenderer);
setMirror(false);
setScalingType(DEFAULT_SCALING_TYPE);
}
/**
* Gets the {@code SurfaceViewRenderer} which renders {@link #videoTrack}.
* Explicitly defined and used in order to facilitate switching the instance
* at compile time. For example, reduces the number of modifications
* necessary to switch the implementation from a {@code SurfaceViewRenderer}
* that is a child of a {@code WebRTCView} to {@code WebRTCView} extending
* {@code SurfaceViewRenderer}.
*
* @return The {@code SurfaceViewRenderer} which renders {@code videoTrack}.
*/
private final SurfaceViewRenderer getSurfaceViewRenderer() {
return surfaceViewRenderer;
}
/**
* If this <tt>View</tt> has {@link View#isInLayout()}, invokes it and
* returns its return value; otherwise, returns <tt>false</tt> like
* {@link ViewCompat#isInLayout(View)}.
*
* @return If this <tt>View</tt> has <tt>View#isInLayout()</tt>, invokes it
* and returns its return value; otherwise, returns <tt>false</tt>.
*/
private boolean invokeIsInLayout() {
Method m = IS_IN_LAYOUT;
boolean b = false;
if (m != null) {
try {
b = (boolean) m.invoke(this);
} catch (Throwable e) {
// Fall back to the behavior of ViewCompat#isInLayout(View).
}
}
return b;
}
/**
* {@inheritDoc}
*/
@Override
protected void onAttachedToWindow() {
try {
// Generally, OpenGL is only necessary while this View is attached
// to a window so there is no point in having the whole rendering
// infrastructure hooked up while this View is not attached to a
// window. Additionally, a memory leak was solved in a similar way
// on iOS.
tryAddRendererToVideoTrack();
} finally {
super.onAttachedToWindow();
}
}
/**
* {@inheritDoc}
*/
@Override
protected void onDetachedFromWindow() {
try {
// Generally, OpenGL is only necessary while this View is attached
// to a window so there is no point in having the whole rendering
// infrastructure hooked up while this View is not attached to a
// window. Additionally, a memory leak was solved in a similar way
// on iOS.
removeRendererFromVideoTrack();
} finally {
super.onDetachedFromWindow();
}
}
/**
* Callback fired by {@link #surfaceViewRenderer} when the resolution or
* rotation of the frame it renders has changed.
*
* @param videoWidth The new width of the rendered video frame.
* @param videoHeight The new height of the rendered video frame.
* @param rotation The new rotation of the rendered video frame.
*/
private void onFrameResolutionChanged(int videoWidth,
int videoHeight,
int rotation) {
boolean changed = false;
synchronized (layoutSyncRoot) {
if (frameHeight != videoHeight) {
frameHeight = videoHeight;
changed = true;
}
if (frameRotation != rotation) {
frameRotation = rotation;
changed = true;
}
if (frameWidth != videoWidth) {
frameWidth = videoWidth;
changed = true;
}
}
if (changed) {
// The onFrameResolutionChanged method call executes on the
// surfaceViewRenderer's render Thread.
post(requestSurfaceViewRendererLayoutRunnable);
}
}
/**
* {@inheritDoc}
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int height = b - t;
int width = r - l;
if (height == 0 || width == 0) {
l = t = r = b = 0;
} else {
int frameHeight;
int frameRotation;
int frameWidth;
ScalingType scalingType;
synchronized (layoutSyncRoot) {
frameHeight = this.frameHeight;
frameRotation = this.frameRotation;
frameWidth = this.frameWidth;
scalingType = this.scalingType;
}
SurfaceViewRenderer surfaceViewRenderer = getSurfaceViewRenderer();
switch (scalingType) {
case SCALE_ASPECT_FILL:
// Fill this ViewGroup with surfaceViewRenderer and the latter
// will take care of filling itself with the video similarly to
// the cover value the CSS property object-fit.
r = width;
l = 0;
b = height;
t = 0;
break;
case SCALE_ASPECT_FIT:
default:
// Lay surfaceViewRenderer out inside this ViewGroup in accord
// with the contain value of the CSS property object-fit.
// SurfaceViewRenderer will fill itself with the video similarly
// to the cover or contain value of the CSS property object-fit
// (which will not matter, eventually).
if (frameHeight == 0 || frameWidth == 0) {
l = t = r = b = 0;
} else {
float frameAspectRatio
= (frameRotation % 180 == 0)
? frameWidth / (float) frameHeight
: frameHeight / (float) frameWidth;
Point frameDisplaySize
= RendererCommon.getDisplaySize(
scalingType,
frameAspectRatio,
width, height);
l = (width - frameDisplaySize.x) / 2;
t = (height - frameDisplaySize.y) / 2;
r = l + frameDisplaySize.x;
b = t + frameDisplaySize.y;
}
break;
}
}
surfaceViewRenderer.layout(l, t, r, b);
}
/**
* Stops rendering {@link #videoTrack} and releases the associated acquired
* resources (if rendering is in progress).
*/
private void removeRendererFromVideoTrack() {
if (videoRenderer != null) {
videoTrack.removeRenderer(videoRenderer);
videoRenderer.dispose();
videoRenderer = null;
getSurfaceViewRenderer().release();
// Since this WebRTCView is no longer rendering anything, make sure
// surfaceViewRenderer displays nothing as well.
synchronized (layoutSyncRoot) {
frameHeight = 0;
frameRotation = 0;
frameWidth = 0;
}
requestSurfaceViewRendererLayout();
}
}
/**
* Request that {@link #surfaceViewRenderer} be laid out (as soon as
* possible) because layout-related state either of this instance or of
* {@code surfaceViewRenderer} has changed.
*/
@SuppressLint("WrongCall")
private void requestSurfaceViewRendererLayout() {
// Google/WebRTC just call requestLayout() on surfaceViewRenderer when
// they change the value of its mirror or surfaceType property.
getSurfaceViewRenderer().requestLayout();
// The above is not enough though when the video frame's dimensions or
// rotation change. The following will suffice.
if (!invokeIsInLayout()) {
onLayout(
/* changed */ false,
getLeft(), getTop(), getRight(), getBottom());
}
}
/**
* Sets the indicator which determines whether this {@code WebRTCView} is to
* mirror the video represented by {@link #videoTrack} during its rendering.
*
* @param mirror If this {@code WebRTCView} is to mirror the video
* represented by {@code videoTrack} during its rendering, {@code true};
* otherwise, {@code false}.
*/
public void setMirror(boolean mirror) {
if (this.mirror != mirror) {
this.mirror = mirror;
SurfaceViewRenderer surfaceViewRenderer = getSurfaceViewRenderer();
surfaceViewRenderer.setMirror(mirror);
// SurfaceViewRenderer takes the value of its mirror property into
// account upon its layout.
requestSurfaceViewRendererLayout();
}
}
private void setScalingType(ScalingType scalingType) {
SurfaceViewRenderer surfaceViewRenderer;
synchronized (layoutSyncRoot) {
if (this.scalingType == scalingType) {
return;
}
this.scalingType = scalingType;
surfaceViewRenderer = getSurfaceViewRenderer();
surfaceViewRenderer.setScalingType(scalingType);
}
// Both this instance ant its SurfaceViewRenderer take the value of
// their scalingType properties into account upon their layouts.
requestSurfaceViewRendererLayout();
}
/**
* Sets the {@code MediaStream} to be rendered by this {@code WebRTCView}.
* The implementation renders the first {@link VideoTrack}, if any, of the
* specified {@code mediaStream}.
*
* @param mediaStream The {@code MediaStream} to be rendered by this
* {@code WebRTCView} or {@code null}.
*/
public void setStream(MediaStream mediaStream) {
VideoTrack videoTrack;
if (mediaStream == null) {
videoTrack = null;
} else {
List<VideoTrack> videoTracks = mediaStream.videoTracks;
videoTrack = videoTracks.isEmpty() ? null : videoTracks.get(0);
}
setVideoTrack(videoTrack);
}
/**
* Sets the {@code VideoTrack} to be rendered by this {@code WebRTCView}.
*
* @param videoTrack The {@code VideoTrack} to be rendered by this
* {@code WebRTCView} or {@code null}.
*/
private void setVideoTrack(VideoTrack videoTrack) {
VideoTrack oldValue = this.videoTrack;
if (oldValue != videoTrack) {
if (oldValue != null) {
removeRendererFromVideoTrack();
}
this.videoTrack = videoTrack;
if (videoTrack != null) {
tryAddRendererToVideoTrack();
}
}
}
/**
* Sets the z-order of this {@link WebRTCView} in the stacking space of all
* {@code WebRTCView}s. For more details, refer to the documentation of the
* {@code zOrder} property of the JavaScript counterpart of
* {@code WebRTCView} i.e. {@code RTCView}.
*
* @param zOrder The z-order to set on this {@code WebRTCView}.
*/
public void setZOrder(int zOrder) {
SurfaceViewRenderer surfaceViewRenderer = getSurfaceViewRenderer();
switch (zOrder) {
case 0:
surfaceViewRenderer.setZOrderMediaOverlay(false);
break;
case 1:
surfaceViewRenderer.setZOrderMediaOverlay(true);
break;
case 2:
surfaceViewRenderer.setZOrderOnTop(true);
break;
}
}
/**
* Starts rendering {@link #videoTrack} if rendering is not in progress and
* all preconditions for the start of rendering are met.
*/
private void tryAddRendererToVideoTrack() {
if (videoRenderer == null
&& videoTrack != null
&& ViewCompat.isAttachedToWindow(this)) {
EglBase.Context sharedContext = EglUtils.getRootEglBaseContext();
if (sharedContext == null) {
// If SurfaceViewRenderer#init() is invoked, it will throw a
// RuntimeException which will very likely kill the application.
Log.e(LOG_TAG, "Failed to render a VideoTrack!");
return;
}
SurfaceViewRenderer surfaceViewRenderer = getSurfaceViewRenderer();
surfaceViewRenderer.init(sharedContext, rendererEvents);
videoRenderer = new VideoRenderer(surfaceViewRenderer);
videoTrack.addRenderer(videoRenderer);
}
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,136 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.crypto;
import android.text.TextUtils;
import im.vector.matrix.android.internal.legacy.crypto.algorithms.IMXDecrypting;
import im.vector.matrix.android.internal.legacy.crypto.algorithms.IMXEncrypting;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MXCryptoAlgorithms {
private static final String LOG_TAG = MXCryptoAlgorithms.class.getSimpleName();
/**
* Matrix algorithm tag for olm.
*/
public static final String MXCRYPTO_ALGORITHM_OLM = "m.olm.v1.curve25519-aes-sha2";
/**
* Matrix algorithm tag for megolm.
*/
public static final String MXCRYPTO_ALGORITHM_MEGOLM = "m.megolm.v1.aes-sha2";
// encryptors map
private final Map<String, Class<IMXEncrypting>> mEncryptors;
// decryptors map
private final Map<String, Class<IMXDecrypting>> mDecryptors;
// shared instance
private static MXCryptoAlgorithms mSharedInstance = null;
/**
* @return the shared instance
*/
public static MXCryptoAlgorithms sharedAlgorithms() {
if (null == mSharedInstance) {
mSharedInstance = new MXCryptoAlgorithms();
}
return mSharedInstance;
}
/**
* Constructor
*/
private MXCryptoAlgorithms() {
// encryptos
mEncryptors = new HashMap<>();
try {
mEncryptors.put(MXCRYPTO_ALGORITHM_MEGOLM,
(Class<IMXEncrypting>) Class.forName("org.matrix.androidsdk.crypto.algorithms.megolm.MXMegolmEncryption"));
} catch (Exception e) {
Log.e(LOG_TAG, "## MXCryptoAlgorithms() : fails to add MXCRYPTO_ALGORITHM_MEGOLM " + e.getMessage(), e);
}
try {
mEncryptors.put(MXCRYPTO_ALGORITHM_OLM,
(Class<IMXEncrypting>) Class.forName("org.matrix.androidsdk.crypto.algorithms.olm.MXOlmEncryption"));
} catch (Exception e) {
Log.e(LOG_TAG, "## MXCryptoAlgorithms() : fails to add MXCRYPTO_ALGORITHM_OLM " + e.getMessage(), e);
}
mDecryptors = new HashMap<>();
try {
mDecryptors.put(MXCRYPTO_ALGORITHM_MEGOLM,
(Class<IMXDecrypting>) Class.forName("org.matrix.androidsdk.crypto.algorithms.megolm.MXMegolmDecryption"));
} catch (Exception e) {
Log.e(LOG_TAG, "## MXCryptoAlgorithms() : fails to add MXCRYPTO_ALGORITHM_MEGOLM " + e.getMessage(), e);
}
try {
mDecryptors.put(MXCRYPTO_ALGORITHM_OLM,
(Class<IMXDecrypting>) Class.forName("org.matrix.androidsdk.crypto.algorithms.olm.MXOlmDecryption"));
} catch (Exception e) {
Log.e(LOG_TAG, "## MXCryptoAlgorithms() : fails to add MXCRYPTO_ALGORITHM_OLM " + e.getMessage(), e);
}
}
/**
* Get the class implementing encryption for the provided algorithm.
*
* @param algorithm the algorithm tag.
* @return A class implementing 'IMXEncrypting'.
*/
public Class<IMXEncrypting> encryptorClassForAlgorithm(String algorithm) {
if (!TextUtils.isEmpty(algorithm)) {
return mEncryptors.get(algorithm);
} else {
return null;
}
}
/**
* Get the class implementing decryption for the provided algorithm.
*
* @param algorithm the algorithm tag.
* @return A class implementing 'IMXDecrypting'.
*/
public Class<IMXDecrypting> decryptorClassForAlgorithm(String algorithm) {
if (!TextUtils.isEmpty(algorithm)) {
return mDecryptors.get(algorithm);
} else {
return null;
}
}
/**
* @return The list of registered algorithms.
*/
public List<String> supportedAlgorithms() {
return new ArrayList<>(mEncryptors.keySet());
}
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -0,0 +1,835 @@
/*
* Copyright 2017 Vector Creations Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.crypto;
import android.text.TextUtils;
import im.vector.matrix.android.internal.legacy.MXPatterns;
import im.vector.matrix.android.internal.legacy.MXSession;
import im.vector.matrix.android.internal.legacy.crypto.data.MXDeviceInfo;
import im.vector.matrix.android.internal.legacy.crypto.data.MXUsersDevicesMap;
import im.vector.matrix.android.internal.legacy.data.cryptostore.IMXCryptoStore;
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
import im.vector.matrix.android.internal.legacy.rest.model.crypto.KeysQueryResponse;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class MXDeviceList {
private static final String LOG_TAG = MXDeviceList.class.getSimpleName();
/**
* State transition diagram for DeviceList.deviceTrackingStatus
* <p>
* |
* stopTrackingDeviceList V
* +---------------------> NOT_TRACKED
* | |
* +<--------------------+ | startTrackingDeviceList
* | | V
* | +-------------> PENDING_DOWNLOAD <--------------------+-+
* | | ^ | | |
* | | restart download | | start download | | invalidateUserDeviceList
* | | client failed | | | |
* | | | V | |
* | +------------ DOWNLOAD_IN_PROGRESS -------------------+ |
* | | | |
* +<-------------------+ | download successful |
* ^ V |
* +----------------------- UP_TO_DATE ------------------------+
**/
public static final int TRACKING_STATUS_NOT_TRACKED = -1;
public static final int TRACKING_STATUS_PENDING_DOWNLOAD = 1;
public static final int TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2;
public static final int TRACKING_STATUS_UP_TO_DATE = 3;
public static final int TRACKING_STATUS_UNREACHABLE_SERVER = 4;
// keys in progress
private final Set<String> mUserKeyDownloadsInProgress = new HashSet<>();
// HS not ready for retry
private final Set<String> mNotReadyToRetryHS = new HashSet<>();
// indexed by UserId
private final Map<String, String> mPendingDownloadKeysRequestToken = new HashMap<>();
// download keys queue
class DownloadKeysPromise {
// list of remain pending device keys
final List<String> mPendingUserIdsList;
// the unfiltered user ids list
final List<String> mUserIdsList;
// the request callback
final ApiCallback<MXUsersDevicesMap<MXDeviceInfo>> mCallback;
/**
* Creator
*
* @param userIds the user ids list
* @param callback the asynchronous callback
*/
DownloadKeysPromise(List<String> userIds, ApiCallback<MXUsersDevicesMap<MXDeviceInfo>> callback) {
mPendingUserIdsList = new ArrayList<>(userIds);
mUserIdsList = new ArrayList<>(userIds);
mCallback = callback;
}
}
// pending queues list
private final List<DownloadKeysPromise> mDownloadKeysQueues = new ArrayList<>();
private final MXCrypto mxCrypto;
private final MXSession mxSession;
private final IMXCryptoStore mCryptoStore;
// tells if there is a download keys request in progress
private boolean mIsDownloadingKeys = false;
/**
* Constructor
*
* @param session the session
* @param crypto the crypto session
*/
public MXDeviceList(MXSession session, MXCrypto crypto) {
mxSession = session;
mxCrypto = crypto;
mCryptoStore = crypto.getCryptoStore();
boolean isUpdated = false;
Map<String, Integer> deviceTrackingStatuses = mCryptoStore.getDeviceTrackingStatuses();
for (String userId : deviceTrackingStatuses.keySet()) {
int status = deviceTrackingStatuses.get(userId);
if ((TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == status) || (TRACKING_STATUS_UNREACHABLE_SERVER == status)) {
// if a download was in progress when we got shut down, it isn't any more.
deviceTrackingStatuses.put(userId, TRACKING_STATUS_PENDING_DOWNLOAD);
isUpdated = true;
}
}
if (isUpdated) {
mCryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses);
}
}
/**
* Tells if the key downloads should be tried
*
* @param userId the userId
* @return true if the keys download can be retrieved
*/
private boolean canRetryKeysDownload(String userId) {
boolean res = false;
if (!TextUtils.isEmpty(userId) && userId.contains(":")) {
try {
synchronized (mNotReadyToRetryHS) {
res = !mNotReadyToRetryHS.contains(userId.substring(userId.lastIndexOf(":") + 1));
}
} catch (Exception e) {
Log.e(LOG_TAG, "## canRetryKeysDownload() failed : " + e.getMessage(), e);
}
}
return res;
}
/**
* Add a download keys promise
*
* @param userIds the user ids list
* @param callback the asynchronous callback
* @return the filtered user ids list i.e the one which require a remote request
*/
private List<String> addDownloadKeysPromise(List<String> userIds, ApiCallback<MXUsersDevicesMap<MXDeviceInfo>> callback) {
if (null != userIds) {
List<String> filteredUserIds = new ArrayList<>();
List<String> invalidUserIds = new ArrayList<>();
for (String userId : userIds) {
if (MXPatterns.isUserId(userId)) {
filteredUserIds.add(userId);
} else {
Log.e(LOG_TAG, "## userId " + userId + "is not a valid user id");
invalidUserIds.add(userId);
}
}
synchronized (mUserKeyDownloadsInProgress) {
filteredUserIds.removeAll(mUserKeyDownloadsInProgress);
mUserKeyDownloadsInProgress.addAll(userIds);
// got some email addresses instead of matrix ids
mUserKeyDownloadsInProgress.removeAll(invalidUserIds);
userIds.removeAll(invalidUserIds);
}
mDownloadKeysQueues.add(new DownloadKeysPromise(userIds, callback));
return filteredUserIds;
} else {
return null;
}
}
/**
* Clear the unavailable server lists
*/
private void clearUnavailableServersList() {
synchronized (mNotReadyToRetryHS) {
mNotReadyToRetryHS.clear();
}
}
/**
* Mark the cached device list for the given user outdated
* flag the given user for device-list tracking, if they are not already.
*
* @param userIds the user ids list
*/
public void startTrackingDeviceList(List<String> userIds) {
if (null != userIds) {
boolean isUpdated = false;
Map<String, Integer> deviceTrackingStatuses = mCryptoStore.getDeviceTrackingStatuses();
for (String userId : userIds) {
if (!deviceTrackingStatuses.containsKey(userId) || (TRACKING_STATUS_NOT_TRACKED == deviceTrackingStatuses.get(userId))) {
Log.d(LOG_TAG, "## startTrackingDeviceList() : Now tracking device list for " + userId);
deviceTrackingStatuses.put(userId, TRACKING_STATUS_PENDING_DOWNLOAD);
isUpdated = true;
}
}
if (isUpdated) {
mCryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses);
}
}
}
/**
* Update the devices list statuses
*
* @param changed the user ids list which have new devices
* @param left the user ids list which left a room
*/
public void handleDeviceListsChanges(List<String> changed, List<String> left) {
boolean isUpdated = false;
Map<String, Integer> deviceTrackingStatuses = mCryptoStore.getDeviceTrackingStatuses();
if ((null != changed) && (0 != changed.size())) {
clearUnavailableServersList();
for (String userId : changed) {
if (deviceTrackingStatuses.containsKey(userId)) {
Log.d(LOG_TAG, "## invalidateUserDeviceList() : Marking device list outdated for " + userId);
deviceTrackingStatuses.put(userId, TRACKING_STATUS_PENDING_DOWNLOAD);
isUpdated = true;
}
}
}
if ((null != left) && (0 != left.size())) {
clearUnavailableServersList();
for (String userId : left) {
if (deviceTrackingStatuses.containsKey(userId)) {
Log.d(LOG_TAG, "## invalidateUserDeviceList() : No longer tracking device list for " + userId);
deviceTrackingStatuses.put(userId, TRACKING_STATUS_NOT_TRACKED);
isUpdated = true;
}
}
}
if (isUpdated) {
mCryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses);
}
}
/**
* This will flag each user whose devices we are tracking as in need of an
* + update
*/
public void invalidateAllDeviceLists() {
handleDeviceListsChanges(new ArrayList<>(mCryptoStore.getDeviceTrackingStatuses().keySet()), null);
}
/**
* The keys download failed
*
* @param userIds the user ids list
*/
private void onKeysDownloadFailed(final List<String> userIds) {
if (null != userIds) {
synchronized (mUserKeyDownloadsInProgress) {
Map<String, Integer> deviceTrackingStatuses = mCryptoStore.getDeviceTrackingStatuses();
for (String userId : userIds) {
mUserKeyDownloadsInProgress.remove(userId);
deviceTrackingStatuses.put(userId, TRACKING_STATUS_PENDING_DOWNLOAD);
}
mCryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses);
}
}
mIsDownloadingKeys = false;
}
/**
* The keys download succeeded.
*
* @param userIds the userIds list
* @param failures the failure map.
*/
private void onKeysDownloadSucceed(List<String> userIds, Map<String, Map<String, Object>> failures) {
if (null != failures) {
Set<String> keys = failures.keySet();
for (String k : keys) {
Map<String, Object> value = failures.get(k);
if (value.containsKey("status")) {
Object statusCodeAsVoid = value.get("status");
int statusCode = 0;
if (statusCodeAsVoid instanceof Double) {
statusCode = ((Double) statusCodeAsVoid).intValue();
} else if (statusCodeAsVoid instanceof Integer) {
statusCode = ((Integer) statusCodeAsVoid).intValue();
}
if (statusCode == 503) {
synchronized (mNotReadyToRetryHS) {
mNotReadyToRetryHS.add(k);
}
}
}
}
}
Map<String, Integer> deviceTrackingStatuses = mCryptoStore.getDeviceTrackingStatuses();
if (null != userIds) {
if (mDownloadKeysQueues.size() > 0) {
List<DownloadKeysPromise> promisesToRemove = new ArrayList<>();
for (DownloadKeysPromise promise : mDownloadKeysQueues) {
promise.mPendingUserIdsList.removeAll(userIds);
if (promise.mPendingUserIdsList.size() == 0) {
// private members
final MXUsersDevicesMap<MXDeviceInfo> usersDevicesInfoMap = new MXUsersDevicesMap<>();
for (String userId : promise.mUserIdsList) {
Map<String, MXDeviceInfo> devices = mCryptoStore.getUserDevices(userId);
if (null == devices) {
if (canRetryKeysDownload(userId)) {
deviceTrackingStatuses.put(userId, TRACKING_STATUS_PENDING_DOWNLOAD);
Log.e(LOG_TAG, "failed to retry the devices of " + userId + " : retry later");
} else {
if (deviceTrackingStatuses.containsKey(userId)
&& (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == deviceTrackingStatuses.get(userId))) {
deviceTrackingStatuses.put(userId, TRACKING_STATUS_UNREACHABLE_SERVER);
Log.e(LOG_TAG, "failed to retry the devices of " + userId + " : the HS is not available");
}
}
} else {
if (deviceTrackingStatuses.containsKey(userId)
&& (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == deviceTrackingStatuses.get(userId))) {
// we didn't get any new invalidations since this download started:
// this user's device list is now up to date.
deviceTrackingStatuses.put(userId, TRACKING_STATUS_UP_TO_DATE);
Log.d(LOG_TAG, "Device list for " + userId + " now up to date");
}
// And the response result
usersDevicesInfoMap.setObjects(devices, userId);
}
}
if (!mxCrypto.hasBeenReleased()) {
final ApiCallback<MXUsersDevicesMap<MXDeviceInfo>> callback = promise.mCallback;
if (null != callback) {
mxCrypto.getUIHandler().post(new Runnable() {
@Override
public void run() {
callback.onSuccess(usersDevicesInfoMap);
}
});
}
}
promisesToRemove.add(promise);
}
}
mDownloadKeysQueues.removeAll(promisesToRemove);
}
for (String userId : userIds) {
mUserKeyDownloadsInProgress.remove(userId);
}
mCryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses);
}
mIsDownloadingKeys = false;
}
/**
* Download the device keys for a list of users and stores the keys in the MXStore.
* It must be called in getEncryptingThreadHandler() thread.
* The callback is called in the UI thread.
*
* @param userIds The users to fetch.
* @param forceDownload Always download the keys even if cached.
* @param callback the asynchronous callback
*/
public void downloadKeys(List<String> userIds, boolean forceDownload, final ApiCallback<MXUsersDevicesMap<MXDeviceInfo>> callback) {
Log.d(LOG_TAG, "## downloadKeys() : forceDownload " + forceDownload + " : " + userIds);
// Map from userid -> deviceid -> DeviceInfo
final MXUsersDevicesMap<MXDeviceInfo> stored = new MXUsersDevicesMap<>();
// List of user ids we need to download keys for
final List<String> downloadUsers = new ArrayList<>();
if (null != userIds) {
if (forceDownload) {
downloadUsers.addAll(userIds);
} else {
for (String userId : userIds) {
Integer status = mCryptoStore.getDeviceTrackingStatus(userId, TRACKING_STATUS_NOT_TRACKED);
// downloading keys ->the keys download won't be triggered twice but the callback requires the dedicated keys
// not yet retrieved
if (mUserKeyDownloadsInProgress.contains(userId)
|| ((TRACKING_STATUS_UP_TO_DATE != status) && (TRACKING_STATUS_UNREACHABLE_SERVER != status))) {
downloadUsers.add(userId);
} else {
Map<String, MXDeviceInfo> devices = mCryptoStore.getUserDevices(userId);
// should always be true
if (null != devices) {
stored.setObjects(devices, userId);
} else {
downloadUsers.add(userId);
}
}
}
}
}
if (0 == downloadUsers.size()) {
Log.d(LOG_TAG, "## downloadKeys() : no new user device");
if (null != callback) {
mxCrypto.getUIHandler().post(new Runnable() {
@Override
public void run() {
callback.onSuccess(stored);
}
});
}
} else {
Log.d(LOG_TAG, "## downloadKeys() : starts");
final long t0 = System.currentTimeMillis();
doKeyDownloadForUsers(downloadUsers, new ApiCallback<MXUsersDevicesMap<MXDeviceInfo>>() {
public void onSuccess(MXUsersDevicesMap<MXDeviceInfo> usersDevicesInfoMap) {
Log.d(LOG_TAG, "## downloadKeys() : doKeyDownloadForUsers succeeds after " + (System.currentTimeMillis() - t0) + " ms");
usersDevicesInfoMap.addEntriesFromMap(stored);
if (null != callback) {
callback.onSuccess(usersDevicesInfoMap);
}
}
@Override
public void onNetworkError(Exception e) {
Log.e(LOG_TAG, "## downloadKeys() : doKeyDownloadForUsers onNetworkError " + e.getMessage(), e);
if (null != callback) {
callback.onNetworkError(e);
}
}
@Override
public void onMatrixError(MatrixError e) {
Log.e(LOG_TAG, "## downloadKeys() : doKeyDownloadForUsers onMatrixError " + e.getMessage());
if (null != callback) {
callback.onMatrixError(e);
}
}
@Override
public void onUnexpectedError(Exception e) {
Log.e(LOG_TAG, "## downloadKeys() : doKeyDownloadForUsers onUnexpectedError " + e.getMessage(), e);
if (null != callback) {
callback.onUnexpectedError(e);
}
}
});
}
}
/**
* Download the devices keys for a set of users.
* It must be called in getEncryptingThreadHandler() thread.
* The callback is called in the UI thread.
*
* @param downloadUsers the user ids list
* @param callback the asynchronous callback
*/
private void doKeyDownloadForUsers(final List<String> downloadUsers, final ApiCallback<MXUsersDevicesMap<MXDeviceInfo>> callback) {
Log.d(LOG_TAG, "## doKeyDownloadForUsers() : doKeyDownloadForUsers " + downloadUsers);
// get the user ids which did not already trigger a keys download
final List<String> filteredUsers = addDownloadKeysPromise(downloadUsers, callback);
// if there is no new keys request
if (0 == filteredUsers.size()) {
// trigger nothing
return;
}
// sanity check
if ((null == mxSession.getDataHandler()) || (null == mxSession.getDataHandler().getStore())) {
return;
}
mIsDownloadingKeys = true;
// track the race condition while sending requests
// we defines a tag for each request
// and test if the response is the latest request one
final String downloadToken = filteredUsers.hashCode() + " " + System.currentTimeMillis();
for (String userId : filteredUsers) {
mPendingDownloadKeysRequestToken.put(userId, downloadToken);
}
mxSession.getCryptoRestClient()
.downloadKeysForUsers(filteredUsers, mxSession.getDataHandler().getStore().getEventStreamToken(), new ApiCallback<KeysQueryResponse>() {
@Override
public void onSuccess(final KeysQueryResponse keysQueryResponse) {
mxCrypto.getEncryptingThreadHandler().post(new Runnable() {
@Override
public void run() {
Log.d(LOG_TAG, "## doKeyDownloadForUsers() : Got keys for " + filteredUsers.size() + " users");
MXDeviceInfo myDevice = mxCrypto.getMyDevice();
IMXCryptoStore cryptoStore = mxCrypto.getCryptoStore();
List<String> userIdsList = new ArrayList<>(filteredUsers);
for (String userId : userIdsList) {
// test if the response is the latest request one
if (!TextUtils.equals(mPendingDownloadKeysRequestToken.get(userId), downloadToken)) {
Log.e(LOG_TAG, "## doKeyDownloadForUsers() : Another update in the queue for "
+ userId + " not marking up-to-date");
filteredUsers.remove(userId);
} else {
Map<String, MXDeviceInfo> devices = keysQueryResponse.deviceKeys.get(userId);
Log.d(LOG_TAG, "## doKeyDownloadForUsers() : Got keys for " + userId + " : " + devices);
if (null != devices) {
Map<String, MXDeviceInfo> mutableDevices = new HashMap<>(devices);
List<String> deviceIds = new ArrayList<>(mutableDevices.keySet());
for (String deviceId : deviceIds) {
// the user has been logged out
if (null == cryptoStore) {
break;
}
// Get the potential previously store device keys for this device
MXDeviceInfo previouslyStoredDeviceKeys = cryptoStore.getUserDevice(deviceId, userId);
MXDeviceInfo deviceInfo = mutableDevices.get(deviceId);
// in some race conditions (like unit tests)
// the self device must be seen as verified
if (TextUtils.equals(deviceInfo.deviceId, myDevice.deviceId)
&& TextUtils.equals(userId, myDevice.userId)) {
deviceInfo.mVerified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED;
}
// Validate received keys
if (!validateDeviceKeys(deviceInfo, userId, deviceId, previouslyStoredDeviceKeys)) {
// New device keys are not valid. Do not store them
mutableDevices.remove(deviceId);
if (null != previouslyStoredDeviceKeys) {
// But keep old validated ones if any
mutableDevices.put(deviceId, previouslyStoredDeviceKeys);
}
} else if (null != previouslyStoredDeviceKeys) {
// The verified status is not sync'ed with hs.
// This is a client side information, valid only for this client.
// So, transfer its previous value
mutableDevices.get(deviceId).mVerified = previouslyStoredDeviceKeys.mVerified;
}
}
// Update the store
// Note that devices which aren't in the response will be removed from the stores
cryptoStore.storeUserDevices(userId, mutableDevices);
}
// the response is the latest request one
mPendingDownloadKeysRequestToken.remove(userId);
}
}
onKeysDownloadSucceed(filteredUsers, keysQueryResponse.failures);
}
});
}
private void onFailed() {
mxCrypto.getEncryptingThreadHandler().post(new Runnable() {
@Override
public void run() {
List<String> userIdsList = new ArrayList<>(filteredUsers);
// test if the response is the latest request one
for (String userId : userIdsList) {
if (!TextUtils.equals(mPendingDownloadKeysRequestToken.get(userId), downloadToken)) {
Log.e(LOG_TAG, "## doKeyDownloadForUsers() : Another update in the queue for " + userId + " not marking up-to-date");
filteredUsers.remove(userId);
} else {
// the response is the latest request one
mPendingDownloadKeysRequestToken.remove(userId);
}
}
onKeysDownloadFailed(filteredUsers);
}
});
}
@Override
public void onNetworkError(Exception e) {
Log.e(LOG_TAG, "##doKeyDownloadForUsers() : onNetworkError " + e.getMessage(), e);
onFailed();
if (null != callback) {
callback.onNetworkError(e);
}
}
@Override
public void onMatrixError(MatrixError e) {
Log.e(LOG_TAG, "##doKeyDownloadForUsers() : onMatrixError " + e.getMessage());
onFailed();
if (null != callback) {
callback.onMatrixError(e);
}
}
@Override
public void onUnexpectedError(Exception e) {
Log.e(LOG_TAG, "##doKeyDownloadForUsers() : onUnexpectedError " + e.getMessage(), e);
onFailed();
if (null != callback) {
callback.onUnexpectedError(e);
}
}
});
}
/**
* Validate device keys.
* This method must called on getEncryptingThreadHandler() thread.
*
* @param deviceKeys the device keys to validate.
* @param userId the id of the user of the device.
* @param deviceId the id of the device.
* @param previouslyStoredDeviceKeys the device keys we received before for this device
* @return true if succeeds
*/
private boolean validateDeviceKeys(MXDeviceInfo deviceKeys, String userId, String deviceId, MXDeviceInfo previouslyStoredDeviceKeys) {
if (null == deviceKeys) {
Log.e(LOG_TAG, "## validateDeviceKeys() : deviceKeys is null from " + userId + ":" + deviceId);
return false;
}
if (null == deviceKeys.keys) {
Log.e(LOG_TAG, "## validateDeviceKeys() : deviceKeys.keys is null from " + userId + ":" + deviceId);
return false;
}
if (null == deviceKeys.signatures) {
Log.e(LOG_TAG, "## validateDeviceKeys() : deviceKeys.signatures is null from " + userId + ":" + deviceId);
return false;
}
// Check that the user_id and device_id in the received deviceKeys are correct
if (!TextUtils.equals(deviceKeys.userId, userId)) {
Log.e(LOG_TAG, "## validateDeviceKeys() : Mismatched user_id " + deviceKeys.userId + " from " + userId + ":" + deviceId);
return false;
}
if (!TextUtils.equals(deviceKeys.deviceId, deviceId)) {
Log.e(LOG_TAG, "## validateDeviceKeys() : Mismatched device_id " + deviceKeys.deviceId + " from " + userId + ":" + deviceId);
return false;
}
String signKeyId = "ed25519:" + deviceKeys.deviceId;
String signKey = deviceKeys.keys.get(signKeyId);
if (null == signKey) {
Log.e(LOG_TAG, "## validateDeviceKeys() : Device " + userId + ":" + deviceKeys.deviceId + " has no ed25519 key");
return false;
}
Map<String, String> signatureMap = deviceKeys.signatures.get(userId);
if (null == signatureMap) {
Log.e(LOG_TAG, "## validateDeviceKeys() : Device " + userId + ":" + deviceKeys.deviceId + " has no map for " + userId);
return false;
}
String signature = signatureMap.get(signKeyId);
if (null == signature) {
Log.e(LOG_TAG, "## validateDeviceKeys() : Device " + userId + ":" + deviceKeys.deviceId + " is not signed");
return false;
}
boolean isVerified = false;
String errorMessage = null;
try {
mxCrypto.getOlmDevice().verifySignature(signKey, deviceKeys.signalableJSONDictionary(), signature);
isVerified = true;
} catch (Exception e) {
errorMessage = e.getMessage();
}
if (!isVerified) {
Log.e(LOG_TAG, "## validateDeviceKeys() : Unable to verify signature on device " + userId + ":"
+ deviceKeys.deviceId + " with error " + errorMessage);
return false;
}
if (null != previouslyStoredDeviceKeys) {
if (!TextUtils.equals(previouslyStoredDeviceKeys.fingerprint(), signKey)) {
// This should only happen if the list has been MITMed; we are
// best off sticking with the original keys.
//
// Should we warn the user about it somehow?
Log.e(LOG_TAG, "## validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":"
+ deviceKeys.deviceId + " has changed : "
+ previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey);
Log.e(LOG_TAG, "## validateDeviceKeys() : " + previouslyStoredDeviceKeys + " -> " + deviceKeys);
Log.e(LOG_TAG, "## validateDeviceKeys() : " + previouslyStoredDeviceKeys.keys + " -> " + deviceKeys.keys);
return false;
}
}
return true;
}
/**
* Start device queries for any users who sent us an m.new_device recently
* This method must be called on getEncryptingThreadHandler() thread.
*/
public void refreshOutdatedDeviceLists() {
final List<String> users = new ArrayList<>();
Map<String, Integer> deviceTrackingStatuses = mCryptoStore.getDeviceTrackingStatuses();
for (String userId : deviceTrackingStatuses.keySet()) {
if (TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses.get(userId)) {
users.add(userId);
}
}
if (users.size() == 0) {
return;
}
if (mIsDownloadingKeys) {
// request already in progress - do nothing. (We will automatically
// make another request if there are more users with outdated
// device lists when the current request completes).
return;
}
// update the statuses
for (String userId : users) {
Integer status = deviceTrackingStatuses.get(userId);
if ((null != status) && (TRACKING_STATUS_PENDING_DOWNLOAD == status)) {
deviceTrackingStatuses.put(userId, TRACKING_STATUS_DOWNLOAD_IN_PROGRESS);
}
}
mCryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses);
doKeyDownloadForUsers(users, new ApiCallback<MXUsersDevicesMap<MXDeviceInfo>>() {
@Override
public void onSuccess(final MXUsersDevicesMap<MXDeviceInfo> response) {
mxCrypto.getEncryptingThreadHandler().post(new Runnable() {
@Override
public void run() {
Log.d(LOG_TAG, "## refreshOutdatedDeviceLists() : done");
}
});
}
private void onError(String error) {
Log.e(LOG_TAG, "## refreshOutdatedDeviceLists() : ERROR updating device keys for users " + users + " : " + error);
}
@Override
public void onNetworkError(final Exception e) {
onError(e.getMessage());
}
@Override
public void onMatrixError(final MatrixError e) {
onError(e.getMessage());
}
@Override
public void onUnexpectedError(final Exception e) {
onError(e.getMessage());
}
});
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.crypto;
import com.google.gson.JsonElement;
import java.util.ArrayList;
import java.util.List;
/**
* The result of a (successful) call to decryptEvent.
*/
public class MXEventDecryptionResult {
/**
* The plaintext payload for the event (typically containing "type" and "content" fields).
*/
public JsonElement mClearEvent;
/**
* Key owned by the sender of this event.
* See MXEvent.senderKey.
*/
public String mSenderCurve25519Key;
/**
* Ed25519 key claimed by the sender of this event.
* See MXEvent.claimedEd25519Key.
*/
public String mClaimedEd25519Key;
/**
* List of curve25519 keys involved in telling us about the senderCurve25519Key and
* claimedEd25519Key. See MXEvent.forwardingCurve25519KeyChain.
*/
public List<String> mForwardingCurve25519KeyChain = new ArrayList<>();
}

View file

@ -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;
}
}

View file

@ -0,0 +1,830 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.crypto;
import android.text.TextUtils;
import com.google.gson.JsonParser;
import im.vector.matrix.android.internal.legacy.crypto.algorithms.MXDecryptionResult;
import im.vector.matrix.android.internal.legacy.crypto.data.MXOlmInboundGroupSession2;
import im.vector.matrix.android.internal.legacy.data.cryptostore.IMXCryptoStore;
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
import im.vector.matrix.android.internal.legacy.util.Log;
import org.matrix.olm.OlmAccount;
import org.matrix.olm.OlmInboundGroupSession;
import org.matrix.olm.OlmMessage;
import org.matrix.olm.OlmOutboundGroupSession;
import org.matrix.olm.OlmSession;
import org.matrix.olm.OlmUtility;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class MXOlmDevice {
private static final String LOG_TAG = MXOlmDevice.class.getSimpleName();
// Curve25519 key for the account.
private String mDeviceCurve25519Key;
// Ed25519 key for the account.
private String mDeviceEd25519Key;
// The store where crypto data is saved.
private final IMXCryptoStore mStore;
// The OLMKit account instance.
private OlmAccount mOlmAccount;
// The OLMKit utility instance.
private OlmUtility mOlmUtility;
// The outbound group session.
// They are not stored in 'store' to avoid to remember to which devices we sent the session key.
// Plus, in cryptography, it is good to refresh sessions from time to time.
// The key is the session id, the value the outbound group session.
private final Map<String, OlmOutboundGroupSession> mOutboundGroupSessionStore;
// Store a set of decrypted message indexes for each group session.
// This partially mitigates a replay attack where a MITM resends a group
// message into the room.
//
// The Matrix SDK exposes events through MXEventTimelines. A developer can open several
// timelines from a same room so that a message can be decrypted several times but from
// a different timeline.
// So, store these message indexes per timeline id.
//
// The first level keys are timeline ids.
// The second level keys are strings of form "<senderKey>|<session_id>|<message_index>"
// Values are true.
private final Map<String, Map<String, Boolean>> mInboundGroupSessionMessageIndexes;
/**
* inboundGroupSessionWithId error
*/
private MXCryptoError mInboundGroupSessionWithIdError = null;
/**
* Constructor
*
* @param store the used store
*/
public MXOlmDevice(IMXCryptoStore store) {
mStore = store;
// Retrieve the account from the store
mOlmAccount = mStore.getAccount();
if (null == mOlmAccount) {
Log.d(LOG_TAG, "MXOlmDevice : create a new olm account");
// Else, create it
try {
mOlmAccount = new OlmAccount();
mStore.storeAccount(mOlmAccount);
} catch (Exception e) {
Log.e(LOG_TAG, "MXOlmDevice : cannot initialize mOlmAccount " + e.getMessage(), e);
}
} else {
Log.d(LOG_TAG, "MXOlmDevice : use an existing account");
}
try {
mOlmUtility = new OlmUtility();
} catch (Exception e) {
Log.e(LOG_TAG, "## MXOlmDevice : OlmUtility failed with error " + e.getMessage(), e);
mOlmUtility = null;
}
mOutboundGroupSessionStore = new HashMap<>();
try {
mDeviceCurve25519Key = mOlmAccount.identityKeys().get(OlmAccount.JSON_KEY_IDENTITY_KEY);
} catch (Exception e) {
Log.e(LOG_TAG, "## MXOlmDevice : cannot find " + OlmAccount.JSON_KEY_IDENTITY_KEY + " with error " + e.getMessage(), e);
}
try {
mDeviceEd25519Key = mOlmAccount.identityKeys().get(OlmAccount.JSON_KEY_FINGER_PRINT_KEY);
} catch (Exception e) {
Log.e(LOG_TAG, "## MXOlmDevice : cannot find " + OlmAccount.JSON_KEY_FINGER_PRINT_KEY + " with error " + e.getMessage(), e);
}
mInboundGroupSessionMessageIndexes = new HashMap<>();
}
/**
* Release the instance
*/
public void release() {
if (null != mOlmAccount) {
mOlmAccount.releaseAccount();
}
}
/**
* @return the Curve25519 key for the account.
*/
public String getDeviceCurve25519Key() {
return mDeviceCurve25519Key;
}
/**
* @return the Ed25519 key for the account.
*/
public String getDeviceEd25519Key() {
return mDeviceEd25519Key;
}
/**
* Signs a message with the ed25519 key for this account.
*
* @param message the message to be signed.
* @return the base64-encoded signature.
*/
private String signMessage(String message) {
try {
return mOlmAccount.signMessage(message);
} catch (Exception e) {
Log.e(LOG_TAG, "## signMessage() : failed " + e.getMessage(), e);
}
return null;
}
/**
* Signs a JSON dictionary with the ed25519 key for this account.
* The signature is done on canonical version of the JSON.
*
* @param JSONDictionary the JSON to be signed.
* @return the base64-encoded signature
*/
public String signJSON(Map<String, Object> JSONDictionary) {
return signMessage(JsonUtils.getCanonicalizedJsonString(JSONDictionary));
}
/**
* @return The current (unused, unpublished) one-time keys for this account.
*/
public Map<String, Map<String, String>> getOneTimeKeys() {
try {
return mOlmAccount.oneTimeKeys();
} catch (Exception e) {
Log.e(LOG_TAG, "## getOneTimeKeys() : failed " + e.getMessage(), e);
}
return null;
}
/**
* @return The maximum number of one-time keys the olm account can store.
*/
public long getMaxNumberOfOneTimeKeys() {
if (null != mOlmAccount) {
return mOlmAccount.maxOneTimeKeys();
} else {
return -1;
}
}
/**
* Marks all of the one-time keys as published.
*/
public void markKeysAsPublished() {
try {
mOlmAccount.markOneTimeKeysAsPublished();
mStore.storeAccount(mOlmAccount);
} catch (Exception e) {
Log.e(LOG_TAG, "## markKeysAsPublished() : failed " + e.getMessage(), e);
}
}
/**
* Generate some new one-time keys
*
* @param numKeys number of keys to generate
*/
public void generateOneTimeKeys(int numKeys) {
try {
mOlmAccount.generateOneTimeKeys(numKeys);
mStore.storeAccount(mOlmAccount);
} catch (Exception e) {
Log.e(LOG_TAG, "## generateOneTimeKeys() : failed " + e.getMessage(), e);
}
}
/**
* Generate a new outbound session.
* The new session will be stored in the MXStore.
*
* @param theirIdentityKey the remote user's Curve25519 identity key
* @param theirOneTimeKey the remote user's one-time Curve25519 key
* @return the session id for the outbound session. @TODO OLMSession?
*/
public String createOutboundSession(String theirIdentityKey, String theirOneTimeKey) {
Log.d(LOG_TAG, "## createOutboundSession() ; theirIdentityKey " + theirIdentityKey + " theirOneTimeKey " + theirOneTimeKey);
OlmSession olmSession = null;
try {
olmSession = new OlmSession();
olmSession.initOutboundSession(mOlmAccount, theirIdentityKey, theirOneTimeKey);
mStore.storeSession(olmSession, theirIdentityKey);
String sessionIdentifier = olmSession.sessionIdentifier();
Log.d(LOG_TAG, "## createOutboundSession() ; olmSession.sessionIdentifier: " + sessionIdentifier);
return sessionIdentifier;
} catch (Exception e) {
Log.e(LOG_TAG, "## createOutboundSession() failed ; " + e.getMessage(), e);
if (null != olmSession) {
olmSession.releaseSession();
}
}
return null;
}
/**
* Generate a new inbound session, given an incoming message.
*
* @param theirDeviceIdentityKey the remote user's Curve25519 identity key.
* @param messageType the message_type field from the received message (must be 0).
* @param ciphertext base64-encoded body from the received message.
* @return {{payload: string, session_id: string}} decrypted payload, andsession id of new session.
*/
public Map<String, String> createInboundSession(String theirDeviceIdentityKey, int messageType, String ciphertext) {
Log.d(LOG_TAG, "## createInboundSession() : theirIdentityKey: " + theirDeviceIdentityKey);
OlmSession olmSession = null;
try {
try {
olmSession = new OlmSession();
olmSession.initInboundSessionFrom(mOlmAccount, theirDeviceIdentityKey, ciphertext);
} catch (Exception e) {
Log.e(LOG_TAG, "## createInboundSession() : the session creation failed " + e.getMessage(), e);
return null;
}
Log.d(LOG_TAG, "## createInboundSession() : sessionId: " + olmSession.sessionIdentifier());
try {
mOlmAccount.removeOneTimeKeys(olmSession);
mStore.storeAccount(mOlmAccount);
} catch (Exception e) {
Log.e(LOG_TAG, "## createInboundSession() : removeOneTimeKeys failed " + e.getMessage(), e);
}
Log.d(LOG_TAG, "## createInboundSession() : ciphertext: " + ciphertext);
try {
Log.d(LOG_TAG, "## createInboundSession() :ciphertext: SHA256:" + mOlmUtility.sha256(URLEncoder.encode(ciphertext, "utf-8")));
} catch (Exception e) {
Log.e(LOG_TAG, "## createInboundSession() :ciphertext: cannot encode ciphertext", e);
}
OlmMessage olmMessage = new OlmMessage();
olmMessage.mCipherText = ciphertext;
olmMessage.mType = messageType;
String payloadString = null;
try {
payloadString = olmSession.decryptMessage(olmMessage);
mStore.storeSession(olmSession, theirDeviceIdentityKey);
} catch (Exception e) {
Log.e(LOG_TAG, "## createInboundSession() : decryptMessage failed " + e.getMessage(), e);
}
Map<String, String> res = new HashMap<>();
if (!TextUtils.isEmpty(payloadString)) {
res.put("payload", payloadString);
}
String sessionIdentifier = olmSession.sessionIdentifier();
if (!TextUtils.isEmpty(sessionIdentifier)) {
res.put("session_id", sessionIdentifier);
}
return res;
} catch (Exception e) {
Log.e(LOG_TAG, "## createInboundSession() : OlmSession creation failed " + e.getMessage(), e);
if (null != olmSession) {
olmSession.releaseSession();
}
}
return null;
}
/**
* Get a list of known session IDs for the given device.
*
* @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
* @return a list of known session ids for the device.
*/
public Set<String> getSessionIds(String theirDeviceIdentityKey) {
Map<String, OlmSession> map = mStore.getDeviceSessions(theirDeviceIdentityKey);
if (null != map) {
return map.keySet();
}
return null;
}
/**
* Get the right olm session id for encrypting messages to the given identity key.
*
* @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
* @return the session id, or nil if no established session.
*/
public String getSessionId(String theirDeviceIdentityKey) {
String sessionId = null;
Set<String> sessionIds = getSessionIds(theirDeviceIdentityKey);
if ((null != sessionIds) && (0 != sessionIds.size())) {
List<String> sessionIdsList = new ArrayList<>(sessionIds);
Collections.sort(sessionIdsList);
sessionId = sessionIdsList.get(0);
}
return sessionId;
}
/**
* Encrypt an outgoing message using an existing session.
*
* @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
* @param sessionId the id of the active session
* @param payloadString the payload to be encrypted and sent
* @return the cipher text
*/
public Map<String, Object> encryptMessage(String theirDeviceIdentityKey, String sessionId, String payloadString) {
Map<String, Object> res = null;
OlmMessage olmMessage;
OlmSession olmSession = getSessionForDevice(theirDeviceIdentityKey, sessionId);
if (null != olmSession) {
try {
Log.d(LOG_TAG, "## encryptMessage() : olmSession.sessionIdentifier: " + olmSession.sessionIdentifier());
//Log.d(LOG_TAG, "## encryptMessage() : payloadString: " + payloadString);
olmMessage = olmSession.encryptMessage(payloadString);
mStore.storeSession(olmSession, theirDeviceIdentityKey);
res = new HashMap<>();
res.put("body", olmMessage.mCipherText);
res.put("type", olmMessage.mType);
} catch (Exception e) {
Log.e(LOG_TAG, "## encryptMessage() : failed " + e.getMessage(), e);
}
}
return res;
}
/**
* Decrypt an incoming message using an existing session.
*
* @param ciphertext the base64-encoded body from the received message.
* @param messageType message_type field from the received message.
* @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
* @param sessionId the id of the active session.
* @return the decrypted payload.
*/
public String decryptMessage(String ciphertext, int messageType, String sessionId, String theirDeviceIdentityKey) {
String payloadString = null;
OlmSession olmSession = getSessionForDevice(theirDeviceIdentityKey, sessionId);
if (null != olmSession) {
OlmMessage olmMessage = new OlmMessage();
olmMessage.mCipherText = ciphertext;
olmMessage.mType = messageType;
try {
payloadString = olmSession.decryptMessage(olmMessage);
mStore.storeSession(olmSession, theirDeviceIdentityKey);
} catch (Exception e) {
Log.e(LOG_TAG, "## decryptMessage() : decryptMessage failed " + e.getMessage(), e);
}
}
return payloadString;
}
/**
* Determine if an incoming messages is a prekey message matching an existing session.
*
* @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
* @param sessionId the id of the active session.
* @param messageType message_type field from the received message.
* @param ciphertext the base64-encoded body from the received message.
* @return YES if the received message is a prekey message which matchesthe given session.
*/
public boolean matchesSession(String theirDeviceIdentityKey, String sessionId, int messageType, String ciphertext) {
if (messageType != 0) {
return false;
}
OlmSession olmSession = getSessionForDevice(theirDeviceIdentityKey, sessionId);
return (null != olmSession) && olmSession.matchesInboundSession(ciphertext);
}
// Outbound group session
/**
* Generate a new outbound group session.
*
* @return the session id for the outbound session.
*/
public String createOutboundGroupSession() {
OlmOutboundGroupSession session = null;
try {
session = new OlmOutboundGroupSession();
mOutboundGroupSessionStore.put(session.sessionIdentifier(), session);
return session.sessionIdentifier();
} catch (Exception e) {
Log.e(LOG_TAG, "createOutboundGroupSession " + e.getMessage(), e);
if (null != session) {
session.releaseSession();
}
}
return null;
}
/**
* Get the current session key of an outbound group session.
*
* @param sessionId the id of the outbound group session.
* @return the base64-encoded secret key.
*/
public String getSessionKey(String sessionId) {
if (!TextUtils.isEmpty(sessionId)) {
try {
return mOutboundGroupSessionStore.get(sessionId).sessionKey();
} catch (Exception e) {
Log.e(LOG_TAG, "## getSessionKey() : failed " + e.getMessage(), e);
}
}
return null;
}
/**
* Get the current message index of an outbound group session.
*
* @param sessionId the id of the outbound group session.
* @return the current chain index.
*/
public int getMessageIndex(String sessionId) {
if (!TextUtils.isEmpty(sessionId)) {
return mOutboundGroupSessionStore.get(sessionId).messageIndex();
}
return 0;
}
/**
* Encrypt an outgoing message with an outbound group session.
*
* @param sessionId the id of the outbound group session.
* @param payloadString the payload to be encrypted and sent.
* @return ciphertext
*/
public String encryptGroupMessage(String sessionId, String payloadString) {
if (!TextUtils.isEmpty(sessionId) && !TextUtils.isEmpty(payloadString)) {
try {
return mOutboundGroupSessionStore.get(sessionId).encryptMessage(payloadString);
} catch (Exception e) {
Log.e(LOG_TAG, "## encryptGroupMessage() : failed " + e.getMessage(), e);
}
}
return null;
}
// Inbound group session
/**
* Add an inbound group session to the session store.
*
* @param sessionId the session identifier.
* @param sessionKey base64-encoded secret key.
* @param roomId the id of the room in which this session will be used.
* @param senderKey the base64-encoded curve25519 key of the sender.
* @param forwardingCurve25519KeyChain Devices involved in forwarding this session to us.
* @param keysClaimed Other keys the sender claims.
* @param exportFormat true if the megolm keys are in export format
* @return true if the operation succeeds.
*/
public boolean addInboundGroupSession(String sessionId,
String sessionKey,
String roomId,
String senderKey,
List<String> forwardingCurve25519KeyChain,
Map<String, String> keysClaimed,
boolean exportFormat) {
if (null != getInboundGroupSession(sessionId, senderKey, roomId)) {
// If we already have this session, consider updating it
Log.e(LOG_TAG, "## addInboundGroupSession() : Update for megolm session " + senderKey + "/" + sessionId);
// For now we just ignore updates. TODO: implement something here
return false;
}
MXOlmInboundGroupSession2 session = new MXOlmInboundGroupSession2(sessionKey, exportFormat);
// sanity check
if (null == session.mSession) {
Log.e(LOG_TAG, "## addInboundGroupSession : invalid session");
return false;
}
try {
if (!TextUtils.equals(session.mSession.sessionIdentifier(), sessionId)) {
Log.e(LOG_TAG, "## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: " + senderKey);
return false;
}
} catch (Exception e) {
Log.e(LOG_TAG, "## addInboundGroupSession : sessionIdentifier') failed " + e.getMessage(), e);
return false;
}
session.mSenderKey = senderKey;
session.mRoomId = roomId;
session.mKeysClaimed = keysClaimed;
session.mForwardingCurve25519KeyChain = forwardingCurve25519KeyChain;
mStore.storeInboundGroupSession(session);
return true;
}
/**
* Import an inbound group session to the session store.
*
* @param exportedSessionMap the exported session map
* @return the imported session if the operation succeeds.
*/
public MXOlmInboundGroupSession2 importInboundGroupSession(Map<String, Object> exportedSessionMap) {
String sessionId = (String) exportedSessionMap.get("session_id");
String senderKey = (String) exportedSessionMap.get("sender_key");
String roomId = (String) exportedSessionMap.get("room_id");
if (null != getInboundGroupSession(sessionId, senderKey, roomId)) {
// If we already have this session, consider updating it
Log.e(LOG_TAG, "## importInboundGroupSession() : Update for megolm session " + senderKey + "/" + sessionId);
// For now we just ignore updates. TODO: implement something here
return null;
}
MXOlmInboundGroupSession2 session = null;
try {
session = new MXOlmInboundGroupSession2(exportedSessionMap);
} catch (Exception e) {
Log.e(LOG_TAG, "## importInboundGroupSession() : Update for megolm session " + senderKey + "/" + sessionId, e);
}
// sanity check
if ((null == session) || (null == session.mSession)) {
Log.e(LOG_TAG, "## importInboundGroupSession : invalid session");
return null;
}
try {
if (!TextUtils.equals(session.mSession.sessionIdentifier(), sessionId)) {
Log.e(LOG_TAG, "## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: " + senderKey);
return null;
}
} catch (Exception e) {
Log.e(LOG_TAG, "## importInboundGroupSession : sessionIdentifier') failed " + e.getMessage(), e);
return null;
}
mStore.storeInboundGroupSession(session);
return session;
}
/**
* Remove an inbound group session
*
* @param sessionId the session identifier.
* @param sessionKey base64-encoded secret key.
*/
public void removeInboundGroupSession(String sessionId, String sessionKey) {
if ((null != sessionId) && (null != sessionKey)) {
mStore.removeInboundGroupSession(sessionId, sessionKey);
}
}
/**
* Decrypt a received message with an inbound group session.
*
* @param body the base64-encoded body of the encrypted message.
* @param roomId theroom in which the message was received.
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @param sessionId the session identifier.
* @param senderKey the base64-encoded curve25519 key of the sender.
* @return the decrypting result. Nil if the sessionId is unknown.
*/
public MXDecryptionResult decryptGroupMessage(String body,
String roomId,
String timeline,
String sessionId,
String senderKey) throws MXDecryptionException {
MXDecryptionResult result = new MXDecryptionResult();
MXOlmInboundGroupSession2 session = getInboundGroupSession(sessionId, senderKey, roomId);
if (null != session) {
// Check that the room id matches the original one for the session. This stops
// the HS pretending a message was targeting a different room.
if (TextUtils.equals(roomId, session.mRoomId)) {
String errorMessage = "";
OlmInboundGroupSession.DecryptMessageResult decryptResult = null;
try {
decryptResult = session.mSession.decryptMessage(body);
} catch (Exception e) {
Log.e(LOG_TAG, "## decryptGroupMessage () : decryptMessage failed " + e.getMessage(), e);
errorMessage = e.getMessage();
}
if (null != decryptResult) {
if (null != timeline) {
if (!mInboundGroupSessionMessageIndexes.containsKey(timeline)) {
mInboundGroupSessionMessageIndexes.put(timeline, new HashMap<String, Boolean>());
}
String messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex;
if (null != mInboundGroupSessionMessageIndexes.get(timeline).get(messageIndexKey)) {
String reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex);
Log.e(LOG_TAG, "## decryptGroupMessage() : " + reason);
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.DUPLICATED_MESSAGE_INDEX_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, reason));
}
mInboundGroupSessionMessageIndexes.get(timeline).put(messageIndexKey, true);
}
mStore.storeInboundGroupSession(session);
try {
JsonParser parser = new JsonParser();
result.mPayload = parser.parse(JsonUtils.convertFromUTF8(decryptResult.mDecryptedMessage));
} catch (Exception e) {
Log.e(LOG_TAG, "## decryptGroupMessage() : RLEncoder.encode failed " + e.getMessage(), e);
return null;
}
if (null == result.mPayload) {
Log.e(LOG_TAG, "## decryptGroupMessage() : fails to parse the payload");
return null;
}
result.mKeysClaimed = session.mKeysClaimed;
result.mSenderKey = senderKey;
result.mForwardingCurve25519KeyChain = session.mForwardingCurve25519KeyChain;
} else {
Log.e(LOG_TAG, "## decryptGroupMessage() : failed to decode the message");
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.OLM_ERROR_CODE, errorMessage, null));
}
} else {
String reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.mRoomId);
Log.e(LOG_TAG, "## decryptGroupMessage() : " + reason);
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, reason));
}
} else {
Log.e(LOG_TAG, "## decryptGroupMessage() : Cannot retrieve inbound group session " + sessionId);
throw new MXDecryptionException(mInboundGroupSessionWithIdError);
}
return result;
}
/**
* Reset replay attack data for the given timeline.
*
* @param timeline the id of the timeline.
*/
public void resetReplayAttackCheckInTimeline(String timeline) {
if (null != timeline) {
mInboundGroupSessionMessageIndexes.remove(timeline);
}
}
// Utilities
/**
* Verify an ed25519 signature on a JSON object.
*
* @param key the ed25519 key.
* @param JSONDictinary the JSON object which was signed.
* @param signature the base64-encoded signature to be checked.
* @throws Exception the exception
*/
public void verifySignature(String key, Map<String, Object> JSONDictinary, String signature) throws Exception {
// Check signature on the canonical version of the JSON
mOlmUtility.verifyEd25519Signature(signature, key, JsonUtils.getCanonicalizedJsonString(JSONDictinary));
}
/**
* Calculate the SHA-256 hash of the input and encodes it as base64.
*
* @param message the message to hash.
* @return the base64-encoded hash value.
*/
public String sha256(String message) {
return mOlmUtility.sha256(JsonUtils.convertToUTF8(message));
}
/**
* Search an OlmSession
*
* @param theirDeviceIdentityKey the device key
* @param sessionId the session Id
* @return the olm session
*/
private OlmSession getSessionForDevice(String theirDeviceIdentityKey, String sessionId) {
// sanity check
if (!TextUtils.isEmpty(theirDeviceIdentityKey) && !TextUtils.isEmpty(sessionId)) {
Map<String, OlmSession> map = mStore.getDeviceSessions(theirDeviceIdentityKey);
if (null != map) {
return map.get(sessionId);
}
}
return null;
}
/**
* Extract an InboundGroupSession from the session store and do some check.
* mInboundGroupSessionWithIdError describes the failure reason.
*
* @param roomId the room where the sesion is used.
* @param sessionId the session identifier.
* @param senderKey the base64-encoded curve25519 key of the sender.
* @return the inbound group session.
*/
public MXOlmInboundGroupSession2 getInboundGroupSession(String sessionId, String senderKey, String roomId) {
mInboundGroupSessionWithIdError = null;
MXOlmInboundGroupSession2 session = mStore.getInboundGroupSession(sessionId, senderKey);
if (null != session) {
// Check that the room id matches the original one for the session. This stops
// the HS pretending a message was targeting a different room.
if (!TextUtils.equals(roomId, session.mRoomId)) {
String errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.mRoomId);
Log.e(LOG_TAG, "## getInboundGroupSession() : " + errorDescription);
mInboundGroupSessionWithIdError = new MXCryptoError(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, errorDescription);
}
} else {
Log.e(LOG_TAG, "## getInboundGroupSession() : Cannot retrieve inbound group session " + sessionId);
mInboundGroupSessionWithIdError = new MXCryptoError(MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE,
MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON, null);
}
return session;
}
/**
* Determine if we have the keys for a given megolm session.
*
* @param roomId room in which the message was received
* @param senderKey base64-encoded curve25519 key of the sender
* @param sessionId session identifier
* @return true if the unbound session keys are known.
*/
public boolean hasInboundSessionKeys(String roomId, String senderKey, String sessionId) {
return null != getInboundGroupSession(sessionId, senderKey, roomId);
}
}

View file

@ -0,0 +1,371 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.crypto;
import android.os.Handler;
import im.vector.matrix.android.internal.legacy.MXSession;
import im.vector.matrix.android.internal.legacy.crypto.data.MXUsersDevicesMap;
import im.vector.matrix.android.internal.legacy.data.cryptostore.IMXCryptoStore;
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
import im.vector.matrix.android.internal.legacy.rest.model.crypto.RoomKeyRequest;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
public class MXOutgoingRoomKeyRequestManager {
private static final String LOG_TAG = MXOutgoingRoomKeyRequestManager.class.getSimpleName();
private static final int SEND_KEY_REQUESTS_DELAY_MS = 500;
// the linked session
private MXSession mSession;
// working handler (should not be the UI thread)
private Handler mWorkingHandler;
// store
private IMXCryptoStore mCryptoStore;
// running
public boolean mClientRunning;
// transaction counter
private int mTxnCtr;
// sanity check to ensure that we don't end up with two concurrent runs
// of mSendOutgoingRoomKeyRequestsTimer
private boolean mSendOutgoingRoomKeyRequestsRunning;
/**
* Constructor
*
* @param session the session
* @param crypto the crypto engine
*/
public MXOutgoingRoomKeyRequestManager(MXSession session, MXCrypto crypto) {
mSession = session;
mWorkingHandler = crypto.getEncryptingThreadHandler();
mCryptoStore = crypto.getCryptoStore();
}
/**
* Called when the client is started. Sets background processes running.
*/
public void start() {
mClientRunning = true;
startTimer();
}
/**
* Called when the client is stopped. Stops any running background processes.
*/
public void stop() {
mClientRunning = false;
}
/**
* Make up a new transaction id
*
* @return {string} a new, unique, transaction id
*/
private String makeTxnId() {
return "m" + System.currentTimeMillis() + "." + mTxnCtr++;
}
/**
* Send off a room key request, if we haven't already done so.
* <p>
* The `requestBody` is compared (with a deep-equality check) against
* previous queued or sent requests and if it matches, no change is made.
* Otherwise, a request is added to the pending list, and a job is started
* in the background to send it.
*
* @param requestBody requestBody
* @param recipients recipients
*/
public void sendRoomKeyRequest(final Map<String, String> requestBody, final List<Map<String, String>> recipients) {
mWorkingHandler.post(new Runnable() {
@Override
public void run() {
OutgoingRoomKeyRequest req = mCryptoStore.getOrAddOutgoingRoomKeyRequest(
new OutgoingRoomKeyRequest(requestBody, recipients, makeTxnId(), OutgoingRoomKeyRequest.RequestState.UNSENT));
if (req.mState == OutgoingRoomKeyRequest.RequestState.UNSENT) {
startTimer();
}
}
});
}
/**
* Cancel room key requests, if any match the given details
*
* @param requestBody requestBody
*/
public void cancelRoomKeyRequest(final Map<String, String> requestBody) {
cancelRoomKeyRequest(requestBody, false);
}
/**
* Cancel room key requests, if any match the given details, and resend
*
* @param requestBody requestBody
*/
public void resendRoomKeyRequest(final Map<String, String> requestBody) {
cancelRoomKeyRequest(requestBody, true);
}
/**
* Cancel room key requests, if any match the given details, and resend
*
* @param requestBody requestBody
* @param andResend true to resend the key request
*/
private void cancelRoomKeyRequest(final Map<String, String> requestBody, boolean andResend) {
OutgoingRoomKeyRequest req = mCryptoStore.getOutgoingRoomKeyRequest(requestBody);
if (null == req) {
// no request was made for this key
return;
}
if (req.mState == OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING
|| req.mState == OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND) {
// nothing to do here
} else if ((req.mState == OutgoingRoomKeyRequest.RequestState.UNSENT)
|| (req.mState == OutgoingRoomKeyRequest.RequestState.FAILED)) {
Log.d(LOG_TAG, "## cancelRoomKeyRequest() : deleting unnecessary room key request for " + requestBody);
mCryptoStore.deleteOutgoingRoomKeyRequest(req.mRequestId);
} else if (req.mState == OutgoingRoomKeyRequest.RequestState.SENT) {
if (andResend) {
req.mState = OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND;
} else {
req.mState = OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING;
}
req.mCancellationTxnId = makeTxnId();
mCryptoStore.updateOutgoingRoomKeyRequest(req);
sendOutgoingRoomKeyRequestCancellation(req);
}
}
/**
* Start the background timer to send queued requests, if the timer isn't already running.
*/
private void startTimer() {
mWorkingHandler.post(new Runnable() {
@Override
public void run() {
if (mSendOutgoingRoomKeyRequestsRunning) {
return;
}
mWorkingHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (mSendOutgoingRoomKeyRequestsRunning) {
Log.d(LOG_TAG, "## startTimer() : RoomKeyRequestSend already in progress!");
return;
}
mSendOutgoingRoomKeyRequestsRunning = true;
sendOutgoingRoomKeyRequests();
}
}, SEND_KEY_REQUESTS_DELAY_MS);
}
});
}
// look for and send any queued requests. Runs itself recursively until
// there are no more requests, or there is an error (in which case, the
// timer will be restarted before the promise resolves).
private void sendOutgoingRoomKeyRequests() {
if (!mClientRunning) {
mSendOutgoingRoomKeyRequestsRunning = false;
return;
}
Log.d(LOG_TAG, "## sendOutgoingRoomKeyRequests() : Looking for queued outgoing room key requests");
OutgoingRoomKeyRequest outgoingRoomKeyRequest = mCryptoStore.getOutgoingRoomKeyRequestByState(
new HashSet<>(Arrays.asList(OutgoingRoomKeyRequest.RequestState.UNSENT,
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING,
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND)));
if (null == outgoingRoomKeyRequest) {
Log.e(LOG_TAG, "## sendOutgoingRoomKeyRequests() : No more outgoing room key requests");
mSendOutgoingRoomKeyRequestsRunning = false;
return;
}
if (OutgoingRoomKeyRequest.RequestState.UNSENT == outgoingRoomKeyRequest.mState) {
sendOutgoingRoomKeyRequest(outgoingRoomKeyRequest);
} else {
sendOutgoingRoomKeyRequestCancellation(outgoingRoomKeyRequest);
}
}
/**
* Send the outgoing key request.
*
* @param request the request
*/
private void sendOutgoingRoomKeyRequest(final OutgoingRoomKeyRequest request) {
Log.d(LOG_TAG, "## sendOutgoingRoomKeyRequest() : Requesting keys " + request.mRequestBody
+ " from " + request.mRecipients + " id " + request.mRequestId);
Map<String, Object> requestMessage = new HashMap<>();
requestMessage.put("action", "request");
requestMessage.put("requesting_device_id", mCryptoStore.getDeviceId());
requestMessage.put("request_id", request.mRequestId);
requestMessage.put("body", request.mRequestBody);
sendMessageToDevices(requestMessage, request.mRecipients, request.mRequestId, new ApiCallback<Void>() {
private void onDone(final OutgoingRoomKeyRequest.RequestState state) {
mWorkingHandler.post(new Runnable() {
@Override
public void run() {
if (request.mState != OutgoingRoomKeyRequest.RequestState.UNSENT) {
Log.d(LOG_TAG, "## sendOutgoingRoomKeyRequest() : Cannot update room key request from UNSENT as it was already updated to "
+ request.mState);
} else {
request.mState = state;
mCryptoStore.updateOutgoingRoomKeyRequest(request);
}
mSendOutgoingRoomKeyRequestsRunning = false;
startTimer();
}
});
}
@Override
public void onSuccess(Void info) {
Log.d(LOG_TAG, "## sendOutgoingRoomKeyRequest succeed");
onDone(OutgoingRoomKeyRequest.RequestState.SENT);
}
@Override
public void onNetworkError(Exception e) {
Log.e(LOG_TAG, "## sendOutgoingRoomKeyRequest failed " + e.getMessage(), e);
onDone(OutgoingRoomKeyRequest.RequestState.FAILED);
}
@Override
public void onMatrixError(MatrixError e) {
Log.e(LOG_TAG, "## sendOutgoingRoomKeyRequest failed " + e.getMessage());
onDone(OutgoingRoomKeyRequest.RequestState.FAILED);
}
@Override
public void onUnexpectedError(Exception e) {
Log.e(LOG_TAG, "## sendOutgoingRoomKeyRequest failed " + e.getMessage(), e);
onDone(OutgoingRoomKeyRequest.RequestState.FAILED);
}
});
}
/**
* Given a RoomKeyRequest, cancel it and delete the request record
*
* @param request the request
*/
private void sendOutgoingRoomKeyRequestCancellation(final OutgoingRoomKeyRequest request) {
Log.d(LOG_TAG, "## sendOutgoingRoomKeyRequestCancellation() : Sending cancellation for key request for " + request.mRequestBody
+ " to " + request.mRecipients
+ " cancellation id " + request.mCancellationTxnId);
Map<String, Object> requestMessageMap = new HashMap<>();
requestMessageMap.put("action", RoomKeyRequest.ACTION_REQUEST_CANCELLATION);
requestMessageMap.put("requesting_device_id", mCryptoStore.getDeviceId());
requestMessageMap.put("request_id", request.mCancellationTxnId);
sendMessageToDevices(requestMessageMap, request.mRecipients, request.mCancellationTxnId, new ApiCallback<Void>() {
private void onDone() {
mWorkingHandler.post(new Runnable() {
@Override
public void run() {
mCryptoStore.deleteOutgoingRoomKeyRequest(request.mRequestId);
mSendOutgoingRoomKeyRequestsRunning = false;
startTimer();
}
});
}
@Override
public void onSuccess(Void info) {
Log.d(LOG_TAG, "## sendOutgoingRoomKeyRequestCancellation() : done");
boolean resend = request.mState == OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND;
onDone();
// Resend the request with a new ID
if (resend) {
sendRoomKeyRequest(request.mRequestBody, request.mRecipients);
}
}
@Override
public void onNetworkError(Exception e) {
Log.e(LOG_TAG, "## sendOutgoingRoomKeyRequestCancellation failed " + e.getMessage(), e);
onDone();
}
@Override
public void onMatrixError(MatrixError e) {
Log.e(LOG_TAG, "## sendOutgoingRoomKeyRequestCancellation failed " + e.getMessage());
onDone();
}
@Override
public void onUnexpectedError(Exception e) {
Log.e(LOG_TAG, "## sendOutgoingRoomKeyRequestCancellation failed " + e.getMessage(), e);
onDone();
}
});
}
/**
* Send a RoomKeyRequest to a list of recipients
*
* @param message the message
* @param recipients the recipients.
* @param transactionId the transaction id
* @param callback the asynchronous callback.
*/
private void sendMessageToDevices(final Map<String, Object> message,
List<Map<String, String>> recipients,
String transactionId,
final ApiCallback<Void> callback) {
MXUsersDevicesMap<Map<String, Object>> contentMap = new MXUsersDevicesMap<>();
for (Map<String, String> recipient : recipients) {
contentMap.setObject(message, recipient.get("userId"), recipient.get("deviceId"));
}
mSession.getCryptoRestClient().sendToDevice(Event.EVENT_TYPE_ROOM_KEY_REQUEST, contentMap, transactionId, callback);
}
}

View file

@ -0,0 +1,124 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.crypto;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
/**
* Represents an outgoing room key request
*/
public class OutgoingRoomKeyRequest implements Serializable {
/**
* possible states for a room key request
*
* The state machine looks like:
*
* |
* V
* UNSENT -----------------------------+
* | |
* | (send successful) | (cancellation requested)
* V |
* SENT |
* |-------------------------------- | --------------+
* | | |
* | | | (cancellation requested with intent
* | | | to resend a new request)
* | (cancellation requested) | |
* V | V
* CANCELLATION_PENDING | CANCELLATION_PENDING_AND_WILL_RESEND
* | | |
* | (cancellation sent) | | (cancellation sent. Create new request
* | | | in the UNSENT state)
* V | |
* (deleted) <---------------------------+----------------+
*/
public enum RequestState {
/**
* request not yet sent
*/
UNSENT,
/**
* request sent, awaiting reply
*/
SENT,
/**
* reply received, cancellation not yet sent
*/
CANCELLATION_PENDING,
/**
* Cancellation not yet sent, once sent, a new request will be done
*/
CANCELLATION_PENDING_AND_WILL_RESEND,
/**
* sending failed
*/
FAILED
}
// Unique id for this request. Used for both
// an id within the request for later pairing with a cancellation, and for
// the transaction id when sending the to_device messages to our local
public String mRequestId;
// transaction id for the cancellation, if any
public String mCancellationTxnId;
// list of recipients for the request
public List<Map<String, String>> mRecipients;
// RequestBody
public Map<String, String> mRequestBody;
// current state of this request
public RequestState mState;
public OutgoingRoomKeyRequest(Map<String, String> requestBody, List<Map<String, String>> recipients, String requestId, RequestState state) {
mRequestBody = requestBody;
mRecipients = recipients;
mRequestId = requestId;
mState = state;
}
/**
* @return the room id
*/
public String getRoomId() {
if (null != mRequestBody) {
return mRequestBody.get("room_id");
}
return null;
}
/**
* @return the session id
*/
public String getSessionId() {
if (null != mRequestBody) {
return mRequestBody.get("session_id");
}
return null;
}
}

View file

@ -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);
}

View file

@ -0,0 +1,49 @@
/*
* Copyright 2015 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.crypto.algorithms;
import com.google.gson.JsonElement;
import im.vector.matrix.android.internal.legacy.MXSession;
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
import java.util.List;
/**
* An interface for encrypting data
*/
public interface IMXEncrypting {
/**
* Init
*
* @param matrixSession the related 'MXSession'.
* @param roomId the id of the room we will be sending to.
*/
void initWithMatrixSession(MXSession matrixSession, String roomId);
/**
* Encrypt an event content according to the configuration of the room.
*
* @param eventContent the content of the event.
* @param eventType the type of the event.
* @param userIds the room members the event will be sent to.
* @param callback the asynchronous callback
*/
void encryptEventContent(JsonElement eventContent, String eventType, List<String> userIds, ApiCallback<JsonElement> callback);
}

View file

@ -0,0 +1,48 @@
/*
* Copyright 2016 OpenMarket Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.crypto.algorithms;
import com.google.gson.JsonElement;
import java.util.List;
import java.util.Map;
/**
* This class represents the decryption result.
*/
public class MXDecryptionResult {
/**
* The decrypted payload (with properties 'type', 'content')
*/
public JsonElement mPayload;
/**
* keys that the sender of the event claims ownership of:
* map from key type to base64-encoded key.
*/
public Map<String, String> mKeysClaimed;
/**
* The curve25519 key that the sender of the event is known to have ownership of.
*/
public String mSenderKey;
/**
* Devices which forwarded this session to us (normally empty).
*/
public List<String> mForwardingCurve25519KeyChain;
}

View file

@ -0,0 +1,468 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.crypto.algorithms.megolm;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import im.vector.matrix.android.internal.legacy.MXSession;
import im.vector.matrix.android.internal.legacy.crypto.IncomingRoomKeyRequest;
import im.vector.matrix.android.internal.legacy.crypto.MXCryptoError;
import im.vector.matrix.android.internal.legacy.crypto.MXDecryptionException;
import im.vector.matrix.android.internal.legacy.crypto.MXEventDecryptionResult;
import im.vector.matrix.android.internal.legacy.crypto.MXOlmDevice;
import im.vector.matrix.android.internal.legacy.crypto.algorithms.IMXDecrypting;
import im.vector.matrix.android.internal.legacy.crypto.algorithms.MXDecryptionResult;
import im.vector.matrix.android.internal.legacy.crypto.data.MXDeviceInfo;
import im.vector.matrix.android.internal.legacy.crypto.data.MXOlmInboundGroupSession2;
import im.vector.matrix.android.internal.legacy.crypto.data.MXOlmSessionResult;
import im.vector.matrix.android.internal.legacy.crypto.data.MXUsersDevicesMap;
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
import im.vector.matrix.android.internal.legacy.rest.model.crypto.EncryptedEventContent;
import im.vector.matrix.android.internal.legacy.rest.model.crypto.ForwardedRoomKeyContent;
import im.vector.matrix.android.internal.legacy.rest.model.crypto.RoomKeyContent;
import im.vector.matrix.android.internal.legacy.rest.model.crypto.RoomKeyRequestBody;
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class MXMegolmDecryption implements IMXDecrypting {
private static final String LOG_TAG = MXMegolmDecryption.class.getSimpleName();
/**
* The olm device interface
*/
private MXOlmDevice mOlmDevice;
// the matrix session
private MXSession mSession;
/**
* Events which we couldn't decrypt due to unknown sessions / indexes: map from
* senderKey|sessionId to timelines to list of MatrixEvents.
*/
private Map<String, /* senderKey|sessionId */
Map<String /* timelineId */, List<Event>>> mPendingEvents;
/**
* Init the object fields
*
* @param matrixSession the matrix session
*/
@Override
public void initWithMatrixSession(MXSession matrixSession) {
mSession = matrixSession;
mOlmDevice = matrixSession.getCrypto().getOlmDevice();
mPendingEvents = new HashMap<>();
}
@Override
@Nullable
public MXEventDecryptionResult decryptEvent(Event event, String timeline) throws MXDecryptionException {
return decryptEvent(event, timeline, true);
}
@Nullable
private MXEventDecryptionResult decryptEvent(Event event, String timeline, boolean requestKeysOnFail) throws MXDecryptionException {
// sanity check
if (null == event) {
Log.e(LOG_TAG, "## decryptEvent() : null event");
return null;
}
EncryptedEventContent encryptedEventContent = JsonUtils.toEncryptedEventContent(event.getWireContent().getAsJsonObject());
String senderKey = encryptedEventContent.sender_key;
String ciphertext = encryptedEventContent.ciphertext;
String sessionId = encryptedEventContent.session_id;
if (TextUtils.isEmpty(senderKey) || TextUtils.isEmpty(sessionId) || TextUtils.isEmpty(ciphertext)) {
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.MISSING_FIELDS_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_FIELDS_REASON));
}
MXEventDecryptionResult eventDecryptionResult = null;
MXCryptoError cryptoError = null;
MXDecryptionResult decryptGroupMessageResult = null;
try {
decryptGroupMessageResult = mOlmDevice.decryptGroupMessage(ciphertext, event.roomId, timeline, sessionId, senderKey);
} catch (MXDecryptionException e) {
cryptoError = e.getCryptoError();
}
// the decryption succeeds
if ((null != decryptGroupMessageResult) && (null != decryptGroupMessageResult.mPayload) && (null == cryptoError)) {
eventDecryptionResult = new MXEventDecryptionResult();
eventDecryptionResult.mClearEvent = decryptGroupMessageResult.mPayload;
eventDecryptionResult.mSenderCurve25519Key = decryptGroupMessageResult.mSenderKey;
if (null != decryptGroupMessageResult.mKeysClaimed) {
eventDecryptionResult.mClaimedEd25519Key = decryptGroupMessageResult.mKeysClaimed.get("ed25519");
}
eventDecryptionResult.mForwardingCurve25519KeyChain = decryptGroupMessageResult.mForwardingCurve25519KeyChain;
} else if (null != cryptoError) {
if (cryptoError.isOlmError()) {
if (TextUtils.equals("UNKNOWN_MESSAGE_INDEX", cryptoError.error)) {
addEventToPendingList(event, timeline);
if (requestKeysOnFail) {
requestKeysForEvent(event);
}
}
String reason = String.format(MXCryptoError.OLM_REASON, cryptoError.error);
String detailedReason = String.format(MXCryptoError.DETAILLED_OLM_REASON, ciphertext, cryptoError.error);
throw new MXDecryptionException(new MXCryptoError(
MXCryptoError.OLM_ERROR_CODE,
reason,
detailedReason));
} else if (TextUtils.equals(cryptoError.errcode, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE)) {
addEventToPendingList(event, timeline);
if (requestKeysOnFail) {
requestKeysForEvent(event);
}
}
throw new MXDecryptionException(cryptoError);
}
return eventDecryptionResult;
}
/**
* Helper for the real decryptEvent and for _retryDecryption. If
* requestKeysOnFail is true, we'll send an m.room_key_request when we fail
* to decrypt the event due to missing megolm keys.
*
* @param event the event
*/
private void requestKeysForEvent(Event event) {
String sender = event.getSender();
EncryptedEventContent wireContent = JsonUtils.toEncryptedEventContent(event.getWireContent());
List<Map<String, String>> recipients = new ArrayList<>();
Map<String, String> selfMap = new HashMap<>();
selfMap.put("userId", mSession.getMyUserId());
selfMap.put("deviceId", "*");
recipients.add(selfMap);
if (!TextUtils.equals(sender, mSession.getMyUserId())) {
Map<String, String> senderMap = new HashMap<>();
senderMap.put("userId", sender);
senderMap.put("deviceId", wireContent.device_id);
recipients.add(senderMap);
}
Map<String, String> requestBody = new HashMap<>();
requestBody.put("room_id", event.roomId);
requestBody.put("algorithm", wireContent.algorithm);
requestBody.put("sender_key", wireContent.sender_key);
requestBody.put("session_id", wireContent.session_id);
mSession.getCrypto().requestRoomKey(requestBody, recipients);
}
/**
* Add an event to the list of those we couldn't decrypt the first time we
* saw them.
*
* @param event the event to try to decrypt later
* @param timelineId the timeline identifier
*/
private void addEventToPendingList(Event event, String timelineId) {
EncryptedEventContent encryptedEventContent = JsonUtils.toEncryptedEventContent(event.getWireContent().getAsJsonObject());
String senderKey = encryptedEventContent.sender_key;
String sessionId = encryptedEventContent.session_id;
String k = senderKey + "|" + sessionId;
// avoid undefined timelineId
if (TextUtils.isEmpty(timelineId)) {
timelineId = "";
}
if (!mPendingEvents.containsKey(k)) {
mPendingEvents.put(k, new HashMap<String, List<Event>>());
}
if (!mPendingEvents.get(k).containsKey(timelineId)) {
mPendingEvents.get(k).put(timelineId, new ArrayList<Event>());
}
if (mPendingEvents.get(k).get(timelineId).indexOf(event) < 0) {
Log.d(LOG_TAG, "## addEventToPendingList() : add Event " + event.eventId + " in room id " + event.roomId);
mPendingEvents.get(k).get(timelineId).add(event);
}
}
/**
* Handle a key event.
*
* @param roomKeyEvent the key event.
*/
@Override
public void onRoomKeyEvent(Event roomKeyEvent) {
boolean exportFormat = false;
RoomKeyContent roomKeyContent = JsonUtils.toRoomKeyContent(roomKeyEvent.getContentAsJsonObject());
String roomId = roomKeyContent.room_id;
String sessionId = roomKeyContent.session_id;
String sessionKey = roomKeyContent.session_key;
String senderKey = roomKeyEvent.senderKey();
Map<String, String> keysClaimed = new HashMap<>();
List<String> forwarding_curve25519_key_chain = null;
if (TextUtils.isEmpty(roomId) || TextUtils.isEmpty(sessionId) || TextUtils.isEmpty(sessionKey)) {
Log.e(LOG_TAG, "## onRoomKeyEvent() : Key event is missing fields");
return;
}
if (TextUtils.equals(roomKeyEvent.getType(), Event.EVENT_TYPE_FORWARDED_ROOM_KEY)) {
Log.d(LOG_TAG, "## onRoomKeyEvent(), forward adding key : roomId " + roomId + " sessionId " + sessionId
+ " sessionKey " + sessionKey); // from " + event);
ForwardedRoomKeyContent forwardedRoomKeyContent = JsonUtils.toForwardedRoomKeyContent(roomKeyEvent.getContentAsJsonObject());
if (null == forwardedRoomKeyContent.forwarding_curve25519_key_chain) {
forwarding_curve25519_key_chain = new ArrayList<>();
} else {
forwarding_curve25519_key_chain = new ArrayList<>(forwardedRoomKeyContent.forwarding_curve25519_key_chain);
}
forwarding_curve25519_key_chain.add(senderKey);
exportFormat = true;
senderKey = forwardedRoomKeyContent.sender_key;
if (null == senderKey) {
Log.e(LOG_TAG, "## onRoomKeyEvent() : forwarded_room_key event is missing sender_key field");
return;
}
String ed25519Key = forwardedRoomKeyContent.sender_claimed_ed25519_key;
if (null == ed25519Key) {
Log.e(LOG_TAG, "## forwarded_room_key_event is missing sender_claimed_ed25519_key field");
return;
}
keysClaimed.put("ed25519", ed25519Key);
} else {
Log.d(LOG_TAG, "## onRoomKeyEvent(), Adding key : roomId " + roomId + " sessionId " + sessionId
+ " sessionKey " + sessionKey); // from " + event);
if (null == senderKey) {
Log.e(LOG_TAG, "## onRoomKeyEvent() : key event has no sender key (not encrypted?)");
return;
}
// inherit the claimed ed25519 key from the setup message
keysClaimed = roomKeyEvent.getKeysClaimed();
}
mOlmDevice.addInboundGroupSession(sessionId, sessionKey, roomId, senderKey, forwarding_curve25519_key_chain, keysClaimed, exportFormat);
Map<String, String> content = new HashMap<>();
content.put("algorithm", roomKeyContent.algorithm);
content.put("room_id", roomKeyContent.room_id);
content.put("session_id", roomKeyContent.session_id);
content.put("sender_key", senderKey);
mSession.getCrypto().cancelRoomKeyRequest(content);
onNewSession(senderKey, sessionId);
}
/**
* Check if the some messages can be decrypted with a new session
*
* @param senderKey the session sender key
* @param sessionId the session id
*/
public void onNewSession(String senderKey, String sessionId) {
String k = senderKey + "|" + sessionId;
Map<String, List<Event>> pending = mPendingEvents.get(k);
if (null != pending) {
// Have another go at decrypting events sent with this session.
mPendingEvents.remove(k);
Set<String> timelineIds = pending.keySet();
for (String timelineId : timelineIds) {
List<Event> events = pending.get(timelineId);
for (Event event : events) {
MXEventDecryptionResult result = null;
try {
result = decryptEvent(event, TextUtils.isEmpty(timelineId) ? null : timelineId);
} catch (MXDecryptionException e) {
Log.e(LOG_TAG, "## onNewSession() : Still can't decrypt " + event.eventId + ". Error " + e.getMessage(), e);
event.setCryptoError(e.getCryptoError());
}
if (null != result) {
final Event fEvent = event;
final MXEventDecryptionResult fResut = result;
mSession.getCrypto().getUIHandler().post(new Runnable() {
@Override
public void run() {
fEvent.setClearData(fResut);
mSession.getDataHandler().onEventDecrypted(fEvent);
}
});
Log.d(LOG_TAG, "## onNewSession() : successful re-decryption of " + event.eventId);
}
}
}
}
}
@Override
public boolean hasKeysForKeyRequest(IncomingRoomKeyRequest request) {
return (null != request)
&& (null != request.mRequestBody)
&& mOlmDevice.hasInboundSessionKeys(request.mRequestBody.room_id, request.mRequestBody.sender_key, request.mRequestBody.session_id);
}
@Override
public void shareKeysWithDevice(final IncomingRoomKeyRequest request) {
// sanity checks
if ((null == request) || (null == request.mRequestBody)) {
return;
}
final String userId = request.mUserId;
mSession.getCrypto().getDeviceList().downloadKeys(Arrays.asList(userId), false, new ApiCallback<MXUsersDevicesMap<MXDeviceInfo>>() {
@Override
public void onSuccess(MXUsersDevicesMap<MXDeviceInfo> devicesMap) {
final String deviceId = request.mDeviceId;
final MXDeviceInfo deviceInfo = mSession.getCrypto().mCryptoStore.getUserDevice(deviceId, userId);
if (null != deviceInfo) {
final RoomKeyRequestBody body = request.mRequestBody;
Map<String, List<MXDeviceInfo>> devicesByUser = new HashMap<>();
devicesByUser.put(userId, new ArrayList<>(Arrays.asList(deviceInfo)));
mSession.getCrypto().ensureOlmSessionsForDevices(devicesByUser, new ApiCallback<MXUsersDevicesMap<MXOlmSessionResult>>() {
@Override
public void onSuccess(MXUsersDevicesMap<MXOlmSessionResult> map) {
MXOlmSessionResult olmSessionResult = map.getObject(deviceId, userId);
if ((null == olmSessionResult) || (null == olmSessionResult.mSessionId)) {
// no session with this device, probably because there
// were no one-time keys.
//
// ensureOlmSessionsForUsers has already done the logging,
// so just skip it.
return;
}
Log.d(LOG_TAG, "## shareKeysWithDevice() : sharing keys for session " + body.sender_key + "|" + body.session_id
+ " with device " + userId + ":" + deviceId);
MXOlmInboundGroupSession2 inboundGroupSession = mSession.getCrypto()
.getOlmDevice().getInboundGroupSession(body.session_id, body.sender_key, body.room_id);
Map<String, Object> payloadJson = new HashMap<>();
payloadJson.put("type", Event.EVENT_TYPE_FORWARDED_ROOM_KEY);
payloadJson.put("content", inboundGroupSession.exportKeys());
Map<String, Object> encodedPayload = mSession.getCrypto().encryptMessage(payloadJson, Arrays.asList(deviceInfo));
MXUsersDevicesMap<Map<String, Object>> sendToDeviceMap = new MXUsersDevicesMap<>();
sendToDeviceMap.setObject(encodedPayload, userId, deviceId);
Log.d(LOG_TAG, "## shareKeysWithDevice() : sending to " + userId + ":" + deviceId);
mSession.getCryptoRestClient().sendToDevice(Event.EVENT_TYPE_MESSAGE_ENCRYPTED, sendToDeviceMap, new ApiCallback<Void>() {
@Override
public void onSuccess(Void info) {
Log.d(LOG_TAG, "## shareKeysWithDevice() : sent to " + userId + ":" + deviceId);
}
@Override
public void onNetworkError(Exception e) {
Log.e(LOG_TAG, "## shareKeysWithDevice() : sendToDevice " + userId + ":" + deviceId + " failed " + e.getMessage(), e);
}
@Override
public void onMatrixError(MatrixError e) {
Log.e(LOG_TAG, "## shareKeysWithDevice() : sendToDevice " + userId + ":" + deviceId + " failed " + e.getMessage());
}
@Override
public void onUnexpectedError(Exception e) {
Log.e(LOG_TAG, "## shareKeysWithDevice() : sendToDevice " + userId + ":" + deviceId + " failed " + e.getMessage(), e);
}
});
}
@Override
public void onNetworkError(Exception e) {
Log.e(LOG_TAG, "## shareKeysWithDevice() : ensureOlmSessionsForDevices " + userId + ":" + deviceId + " failed "
+ e.getMessage(), e);
}
@Override
public void onMatrixError(MatrixError e) {
Log.e(LOG_TAG, "## shareKeysWithDevice() : ensureOlmSessionsForDevices " + userId + ":" + deviceId + " failed " + e.getMessage());
}
@Override
public void onUnexpectedError(Exception e) {
Log.e(LOG_TAG, "## shareKeysWithDevice() : ensureOlmSessionsForDevices " + userId + ":" + deviceId + " failed "
+ e.getMessage(), e);
}
});
} else {
Log.e(LOG_TAG, "## shareKeysWithDevice() : ensureOlmSessionsForDevices " + userId + ":" + deviceId + " not found");
}
}
@Override
public void onNetworkError(Exception e) {
Log.e(LOG_TAG, "## shareKeysWithDevice() : downloadKeys " + userId + " failed " + e.getMessage(), e);
}
@Override
public void onMatrixError(MatrixError e) {
Log.e(LOG_TAG, "## shareKeysWithDevice() : downloadKeys " + userId + " failed " + e.getMessage());
}
@Override
public void onUnexpectedError(Exception e) {
Log.e(LOG_TAG, "## shareKeysWithDevice() : downloadKeys " + userId + " failed " + e.getMessage(), e);
}
});
}
}

View file

@ -0,0 +1,714 @@
/*
* Copyright 2015 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.crypto.algorithms.megolm;
import android.text.TextUtils;
import com.google.gson.JsonElement;
import im.vector.matrix.android.internal.legacy.MXSession;
import im.vector.matrix.android.internal.legacy.crypto.MXCrypto;
import im.vector.matrix.android.internal.legacy.crypto.MXCryptoAlgorithms;
import im.vector.matrix.android.internal.legacy.crypto.MXCryptoError;
import im.vector.matrix.android.internal.legacy.crypto.MXOlmDevice;
import im.vector.matrix.android.internal.legacy.crypto.algorithms.IMXEncrypting;
import im.vector.matrix.android.internal.legacy.crypto.data.MXDeviceInfo;
import im.vector.matrix.android.internal.legacy.crypto.data.MXOlmSessionResult;
import im.vector.matrix.android.internal.legacy.crypto.data.MXQueuedEncryption;
import im.vector.matrix.android.internal.legacy.crypto.data.MXUsersDevicesMap;
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
import im.vector.matrix.android.internal.legacy.rest.callback.SimpleApiCallback;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MXMegolmEncryption implements IMXEncrypting {
private static final String LOG_TAG = MXMegolmEncryption.class.getSimpleName();
private MXSession mSession;
private MXCrypto mCrypto;
// The id of the room we will be sending to.
private String mRoomId;
private String mDeviceId;
// OutboundSessionInfo. Null if we haven't yet started setting one up. Note
// that even if this is non-null, it may not be ready for use (in which
// case outboundSession.shareOperation will be non-null.)
private MXOutboundSessionInfo mOutboundSession;
// true when there is an HTTP operation in progress
private boolean mShareOperationIsProgress;
private final List<MXQueuedEncryption> mPendingEncryptions = new ArrayList<>();
// Session rotation periods
private int mSessionRotationPeriodMsgs;
private int mSessionRotationPeriodMs;
@Override
public void initWithMatrixSession(MXSession matrixSession, String roomId) {
mSession = matrixSession;
mCrypto = matrixSession.getCrypto();
mRoomId = roomId;
mDeviceId = matrixSession.getCredentials().deviceId;
// Default rotation periods
// TODO: Make it configurable via parameters
mSessionRotationPeriodMsgs = 100;
mSessionRotationPeriodMs = 7 * 24 * 3600 * 1000;
}
/**
* @return a snapshot of the pending encryptions
*/
private List<MXQueuedEncryption> getPendingEncryptions() {
List<MXQueuedEncryption> list = new ArrayList<>();
synchronized (mPendingEncryptions) {
list.addAll(mPendingEncryptions);
}
return list;
}
@Override
public void encryptEventContent(final JsonElement eventContent,
final String eventType,
final List<String> userIds,
final ApiCallback<JsonElement> callback) {
// Queue the encryption request
// It will be processed when everything is set up
MXQueuedEncryption queuedEncryption = new MXQueuedEncryption();
queuedEncryption.mEventContent = eventContent;
queuedEncryption.mEventType = eventType;
queuedEncryption.mApiCallback = callback;
synchronized (mPendingEncryptions) {
mPendingEncryptions.add(queuedEncryption);
}
final long t0 = System.currentTimeMillis();
Log.d(LOG_TAG, "## encryptEventContent () starts");
getDevicesInRoom(userIds, new ApiCallback<MXUsersDevicesMap<MXDeviceInfo>>() {
/**
* A network error has been received while encrypting
* @param e the exception
*/
private void dispatchNetworkError(Exception e) {
Log.e(LOG_TAG, "## encryptEventContent() : onNetworkError " + e.getMessage(), e);
List<MXQueuedEncryption> queuedEncryptions = getPendingEncryptions();
for (MXQueuedEncryption queuedEncryption : queuedEncryptions) {
queuedEncryption.mApiCallback.onNetworkError(e);
}
synchronized (mPendingEncryptions) {
mPendingEncryptions.removeAll(queuedEncryptions);
}
}
/**
* A matrix error has been received while encrypting
* @param e the exception
*/
private void dispatchMatrixError(MatrixError e) {
Log.e(LOG_TAG, "## encryptEventContent() : onMatrixError " + e.getMessage());
List<MXQueuedEncryption> queuedEncryptions = getPendingEncryptions();
for (MXQueuedEncryption queuedEncryption : queuedEncryptions) {
queuedEncryption.mApiCallback.onMatrixError(e);
}
synchronized (mPendingEncryptions) {
mPendingEncryptions.removeAll(queuedEncryptions);
}
}
/**
* An unexpected error has been received while encrypting
* @param e the exception
*/
private void dispatchUnexpectedError(Exception e) {
Log.e(LOG_TAG, "## onUnexpectedError() : onMatrixError " + e.getMessage(), e);
List<MXQueuedEncryption> queuedEncryptions = getPendingEncryptions();
for (MXQueuedEncryption queuedEncryption : queuedEncryptions) {
queuedEncryption.mApiCallback.onUnexpectedError(e);
}
synchronized (mPendingEncryptions) {
mPendingEncryptions.removeAll(queuedEncryptions);
}
}
@Override
public void onSuccess(MXUsersDevicesMap<MXDeviceInfo> devicesInRoom) {
ensureOutboundSession(devicesInRoom, new ApiCallback<MXOutboundSessionInfo>() {
@Override
public void onSuccess(final MXOutboundSessionInfo session) {
mCrypto.getEncryptingThreadHandler().post(new Runnable() {
@Override
public void run() {
Log.d(LOG_TAG, "## encryptEventContent () processPendingEncryptions after " + (System.currentTimeMillis() - t0) + "ms");
processPendingEncryptions(session);
}
});
}
@Override
public void onNetworkError(Exception e) {
dispatchNetworkError(e);
}
@Override
public void onMatrixError(MatrixError e) {
dispatchMatrixError(e);
}
@Override
public void onUnexpectedError(Exception e) {
dispatchUnexpectedError(e);
}
});
}
@Override
public void onNetworkError(Exception e) {
dispatchNetworkError(e);
}
@Override
public void onMatrixError(MatrixError e) {
dispatchMatrixError(e);
}
@Override
public void onUnexpectedError(Exception e) {
dispatchUnexpectedError(e);
}
});
}
/**
* Prepare a new session.
*
* @return the session description
*/
private MXOutboundSessionInfo prepareNewSessionInRoom() {
MXOlmDevice olmDevice = mCrypto.getOlmDevice();
final String sessionId = olmDevice.createOutboundGroupSession();
Map<String, String> keysClaimedMap = new HashMap<>();
keysClaimedMap.put("ed25519", olmDevice.getDeviceEd25519Key());
olmDevice.addInboundGroupSession(sessionId, olmDevice.getSessionKey(sessionId), mRoomId, olmDevice.getDeviceCurve25519Key(),
new ArrayList<String>(), keysClaimedMap, false);
return new MXOutboundSessionInfo(sessionId);
}
/**
* Ensure the outbound session
*
* @param devicesInRoom the devices list
* @param callback the asynchronous callback.
*/
private void ensureOutboundSession(MXUsersDevicesMap<MXDeviceInfo> devicesInRoom, final ApiCallback<MXOutboundSessionInfo> callback) {
MXOutboundSessionInfo session = mOutboundSession;
if ((null == session)
// Need to make a brand new session?
|| session.needsRotation(mSessionRotationPeriodMsgs, mSessionRotationPeriodMs)
// Determine if we have shared with anyone we shouldn't have
|| session.sharedWithTooManyDevices(devicesInRoom)) {
mOutboundSession = session = prepareNewSessionInRoom();
}
if (mShareOperationIsProgress) {
Log.d(LOG_TAG, "## ensureOutboundSessionInRoom() : already in progress");
// Key share already in progress
return;
}
final MXOutboundSessionInfo fSession = session;
Map<String, /* userId */List<MXDeviceInfo>> shareMap = new HashMap<>();
List<String> userIds = devicesInRoom.getUserIds();
for (String userId : userIds) {
List<String> deviceIds = devicesInRoom.getUserDeviceIds(userId);
for (String deviceId : deviceIds) {
MXDeviceInfo deviceInfo = devicesInRoom.getObject(deviceId, userId);
if (null == fSession.mSharedWithDevices.getObject(deviceId, userId)) {
if (!shareMap.containsKey(userId)) {
shareMap.put(userId, new ArrayList<MXDeviceInfo>());
}
shareMap.get(userId).add(deviceInfo);
}
}
}
shareKey(fSession, shareMap, new ApiCallback<Void>() {
@Override
public void onSuccess(Void anything) {
mShareOperationIsProgress = false;
if (null != callback) {
callback.onSuccess(fSession);
}
}
@Override
public void onNetworkError(final Exception e) {
Log.e(LOG_TAG, "## ensureOutboundSessionInRoom() : shareKey onNetworkError " + e.getMessage(), e);
if (null != callback) {
callback.onNetworkError(e);
}
mShareOperationIsProgress = false;
}
@Override
public void onMatrixError(final MatrixError e) {
Log.e(LOG_TAG, "## ensureOutboundSessionInRoom() : shareKey onMatrixError " + e.getMessage());
if (null != callback) {
callback.onMatrixError(e);
}
mShareOperationIsProgress = false;
}
@Override
public void onUnexpectedError(final Exception e) {
Log.e(LOG_TAG, "## ensureOutboundSessionInRoom() : shareKey onUnexpectedError " + e.getMessage(), e);
if (null != callback) {
callback.onUnexpectedError(e);
}
mShareOperationIsProgress = false;
}
});
}
/**
* Share the device key to a list of users
*
* @param session the session info
* @param devicesByUsers the devices map
* @param callback the asynchronous callback
*/
private void shareKey(final MXOutboundSessionInfo session,
final Map<String, List<MXDeviceInfo>> devicesByUsers,
final ApiCallback<Void> callback) {
// nothing to send, the task is done
if (0 == devicesByUsers.size()) {
Log.d(LOG_TAG, "## shareKey() : nothing more to do");
if (null != callback) {
mCrypto.getUIHandler().post(new Runnable() {
@Override
public void run() {
callback.onSuccess(null);
}
});
}
return;
}
// reduce the map size to avoid request timeout when there are too devices (Users size * devices per user)
Map<String, List<MXDeviceInfo>> subMap = new HashMap<>();
final List<String> userIds = new ArrayList<>();
int devicesCount = 0;
for (String userId : devicesByUsers.keySet()) {
List<MXDeviceInfo> devicesList = devicesByUsers.get(userId);
userIds.add(userId);
subMap.put(userId, devicesList);
devicesCount += devicesList.size();
if (devicesCount > 100) {
break;
}
}
Log.d(LOG_TAG, "## shareKey() ; userId " + userIds);
shareUserDevicesKey(session, subMap, new ApiCallback<Void>() {
@Override
public void onSuccess(Void info) {
mCrypto.getEncryptingThreadHandler().post(new Runnable() {
@Override
public void run() {
for (String userId : userIds) {
devicesByUsers.remove(userId);
}
shareKey(session, devicesByUsers, callback);
}
});
}
@Override
public void onNetworkError(Exception e) {
Log.e(LOG_TAG, "## shareKey() ; userIds " + userIds + " failed " + e.getMessage(), e);
if (null != callback) {
callback.onNetworkError(e);
}
}
@Override
public void onMatrixError(MatrixError e) {
Log.e(LOG_TAG, "## shareKey() ; userIds " + userIds + " failed " + e.getMessage());
if (null != callback) {
callback.onMatrixError(e);
}
}
@Override
public void onUnexpectedError(Exception e) {
Log.e(LOG_TAG, "## shareKey() ; userIds " + userIds + " failed " + e.getMessage(), e);
if (null != callback) {
callback.onUnexpectedError(e);
}
}
});
}
/**
* Share the device keys of a an user
*
* @param session the session info
* @param devicesByUser the devices map
* @param callback the asynchronous callback
*/
private void shareUserDevicesKey(final MXOutboundSessionInfo session,
final Map<String, List<MXDeviceInfo>> devicesByUser,
final ApiCallback<Void> callback) {
final String sessionKey = mCrypto.getOlmDevice().getSessionKey(session.mSessionId);
final int chainIndex = mCrypto.getOlmDevice().getMessageIndex(session.mSessionId);
Map<String, Object> submap = new HashMap<>();
submap.put("algorithm", MXCryptoAlgorithms.MXCRYPTO_ALGORITHM_MEGOLM);
submap.put("room_id", mRoomId);
submap.put("session_id", session.mSessionId);
submap.put("session_key", sessionKey);
submap.put("chain_index", chainIndex);
final Map<String, Object> payload = new HashMap<>();
payload.put("type", Event.EVENT_TYPE_ROOM_KEY);
payload.put("content", submap);
final long t0 = System.currentTimeMillis();
Log.d(LOG_TAG, "## shareUserDevicesKey() : starts");
mCrypto.ensureOlmSessionsForDevices(devicesByUser, new ApiCallback<MXUsersDevicesMap<MXOlmSessionResult>>() {
@Override
public void onSuccess(final MXUsersDevicesMap<MXOlmSessionResult> results) {
mCrypto.getEncryptingThreadHandler().post(new Runnable() {
@Override
public void run() {
Log.d(LOG_TAG, "## shareUserDevicesKey() : ensureOlmSessionsForDevices succeeds after " + (System.currentTimeMillis() - t0) + " ms");
MXUsersDevicesMap<Map<String, Object>> contentMap = new MXUsersDevicesMap<>();
boolean haveTargets = false;
List<String> userIds = results.getUserIds();
for (String userId : userIds) {
List<MXDeviceInfo> devicesToShareWith = devicesByUser.get(userId);
for (MXDeviceInfo deviceInfo : devicesToShareWith) {
String deviceID = deviceInfo.deviceId;
MXOlmSessionResult sessionResult = results.getObject(deviceID, userId);
if ((null == sessionResult) || (null == sessionResult.mSessionId)) {
// no session with this device, probably because there
// were no one-time keys.
//
// we could send them a to_device message anyway, as a
// signal that they have missed out on the key sharing
// message because of the lack of keys, but there's not
// much point in that really; it will mostly serve to clog
// up to_device inboxes.
//
// ensureOlmSessionsForUsers has already done the logging,
// so just skip it.
continue;
}
Log.d(LOG_TAG, "## shareUserDevicesKey() : Sharing keys with device " + userId + ":" + deviceID);
//noinspection ArraysAsListWithZeroOrOneArgument,ArraysAsListWithZeroOrOneArgument
contentMap.setObject(mCrypto.encryptMessage(payload, Arrays.asList(sessionResult.mDevice)), userId, deviceID);
haveTargets = true;
}
}
if (haveTargets && !mCrypto.hasBeenReleased()) {
final long t0 = System.currentTimeMillis();
Log.d(LOG_TAG, "## shareUserDevicesKey() : has target");
mSession.getCryptoRestClient().sendToDevice(Event.EVENT_TYPE_MESSAGE_ENCRYPTED, contentMap, new ApiCallback<Void>() {
@Override
public void onSuccess(Void info) {
mCrypto.getEncryptingThreadHandler().post(new Runnable() {
@Override
public void run() {
Log.d(LOG_TAG, "## shareUserDevicesKey() : sendToDevice succeeds after "
+ (System.currentTimeMillis() - t0) + " ms");
// Add the devices we have shared with to session.sharedWithDevices.
// we deliberately iterate over devicesByUser (ie, the devices we
// attempted to share with) rather than the contentMap (those we did
// share with), because we don't want to try to claim a one-time-key
// for dead devices on every message.
for (String userId : devicesByUser.keySet()) {
List<MXDeviceInfo> devicesToShareWith = devicesByUser.get(userId);
for (MXDeviceInfo deviceInfo : devicesToShareWith) {
session.mSharedWithDevices.setObject(chainIndex, userId, deviceInfo.deviceId);
}
}
mCrypto.getUIHandler().post(new Runnable() {
@Override
public void run() {
if (null != callback) {
callback.onSuccess(null);
}
}
});
}
});
}
@Override
public void onNetworkError(Exception e) {
Log.e(LOG_TAG, "## shareUserDevicesKey() : sendToDevice onNetworkError " + e.getMessage(), e);
if (null != callback) {
callback.onNetworkError(e);
}
}
@Override
public void onMatrixError(MatrixError e) {
Log.e(LOG_TAG, "## shareUserDevicesKey() : sendToDevice onMatrixError " + e.getMessage());
if (null != callback) {
callback.onMatrixError(e);
}
}
@Override
public void onUnexpectedError(Exception e) {
Log.e(LOG_TAG, "## shareUserDevicesKey() : sendToDevice onUnexpectedError " + e.getMessage(), e);
if (null != callback) {
callback.onUnexpectedError(e);
}
}
});
} else {
Log.d(LOG_TAG, "## shareUserDevicesKey() : no need to sharekey");
if (null != callback) {
mCrypto.getUIHandler().post(new Runnable() {
@Override
public void run() {
callback.onSuccess(null);
}
});
}
}
}
});
}
@Override
public void onNetworkError(Exception e) {
Log.e(LOG_TAG, "## shareUserDevicesKey() : ensureOlmSessionsForDevices failed " + e.getMessage(), e);
if (null != callback) {
callback.onNetworkError(e);
}
}
@Override
public void onMatrixError(MatrixError e) {
Log.e(LOG_TAG, "## shareUserDevicesKey() : ensureOlmSessionsForDevices failed " + e.getMessage());
if (null != callback) {
callback.onMatrixError(e);
}
}
@Override
public void onUnexpectedError(Exception e) {
Log.e(LOG_TAG, "## shareUserDevicesKey() : ensureOlmSessionsForDevices failed " + e.getMessage(), e);
if (null != callback) {
callback.onUnexpectedError(e);
}
}
});
}
/**
* process the pending encryptions
*/
private void processPendingEncryptions(MXOutboundSessionInfo session) {
if (null != session) {
List<MXQueuedEncryption> queuedEncryptions = getPendingEncryptions();
// Everything is in place, encrypt all pending events
for (MXQueuedEncryption queuedEncryption : queuedEncryptions) {
Map<String, Object> payloadJson = new HashMap<>();
payloadJson.put("room_id", mRoomId);
payloadJson.put("type", queuedEncryption.mEventType);
payloadJson.put("content", queuedEncryption.mEventContent);
String payloadString = JsonUtils.convertToUTF8(JsonUtils.canonicalize(JsonUtils.getGson(false).toJsonTree(payloadJson)).toString());
String ciphertext = mCrypto.getOlmDevice().encryptGroupMessage(session.mSessionId, payloadString);
final Map<String, Object> map = new HashMap<>();
map.put("algorithm", MXCryptoAlgorithms.MXCRYPTO_ALGORITHM_MEGOLM);
map.put("sender_key", mCrypto.getOlmDevice().getDeviceCurve25519Key());
map.put("ciphertext", ciphertext);
map.put("session_id", session.mSessionId);
// Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key.
map.put("device_id", mDeviceId);
final MXQueuedEncryption fQueuedEncryption = queuedEncryption;
mCrypto.getUIHandler().post(new Runnable() {
@Override
public void run() {
fQueuedEncryption.mApiCallback.onSuccess(JsonUtils.getGson(false).toJsonTree(map));
}
});
session.mUseCount++;
}
synchronized (mPendingEncryptions) {
mPendingEncryptions.removeAll(queuedEncryptions);
}
}
}
/**
* Get the list of devices which can encrypt data to.
* This method must be called in getDecryptingThreadHandler() thread.
*
* @param userIds the user ids whose devices must be checked.
* @param callback the asynchronous callback
*/
private void getDevicesInRoom(final List<String> userIds, final ApiCallback<MXUsersDevicesMap<MXDeviceInfo>> callback) {
// We are happy to use a cached version here: we assume that if we already
// have a list of the user's devices, then we already share an e2e room
// with them, which means that they will have announced any new devices via
// an m.new_device.
mCrypto.getDeviceList().downloadKeys(userIds, false, new SimpleApiCallback<MXUsersDevicesMap<MXDeviceInfo>>(callback) {
@Override
public void onSuccess(final MXUsersDevicesMap<MXDeviceInfo> devices) {
mCrypto.getEncryptingThreadHandler().post(new Runnable() {
@Override
public void run() {
boolean encryptToVerifiedDevicesOnly = mCrypto.getGlobalBlacklistUnverifiedDevices()
|| mCrypto.isRoomBlacklistUnverifiedDevices(mRoomId);
final MXUsersDevicesMap<MXDeviceInfo> devicesInRoom = new MXUsersDevicesMap<>();
final MXUsersDevicesMap<MXDeviceInfo> unknownDevices = new MXUsersDevicesMap<>();
List<String> userIds = devices.getUserIds();
for (String userId : userIds) {
List<String> deviceIds = devices.getUserDeviceIds(userId);
for (String deviceId : deviceIds) {
MXDeviceInfo deviceInfo = devices.getObject(deviceId, userId);
if (mCrypto.warnOnUnknownDevices() && deviceInfo.isUnknown()) {
// The device is not yet known by the user
unknownDevices.setObject(deviceInfo, userId, deviceId);
continue;
}
if (deviceInfo.isBlocked()) {
// Remove any blocked devices
continue;
}
if (!deviceInfo.isVerified() && encryptToVerifiedDevicesOnly) {
continue;
}
if (TextUtils.equals(deviceInfo.identityKey(), mCrypto.getOlmDevice().getDeviceCurve25519Key())) {
// Don't bother sending to ourself
continue;
}
devicesInRoom.setObject(deviceInfo, userId, deviceId);
}
}
mCrypto.getUIHandler().post(new Runnable() {
@Override
public void run() {
// Check if any of these devices are not yet known to the user.
// if so, warn the user so they can verify or ignore.
if (0 != unknownDevices.getMap().size()) {
callback.onMatrixError(new MXCryptoError(MXCryptoError.UNKNOWN_DEVICES_CODE,
MXCryptoError.UNABLE_TO_ENCRYPT, MXCryptoError.UNKNOWN_DEVICES_REASON, unknownDevices));
} else {
callback.onSuccess(devicesInRoom);
}
}
});
}
});
}
});
}
}

View file

@ -0,0 +1,89 @@
/*
* Copyright 2015 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.crypto.algorithms.megolm;
import im.vector.matrix.android.internal.legacy.crypto.data.MXDeviceInfo;
import im.vector.matrix.android.internal.legacy.util.Log;
import im.vector.matrix.android.internal.legacy.crypto.data.MXUsersDevicesMap;
import java.util.List;
public class MXOutboundSessionInfo {
private static final String LOG_TAG = MXOutboundSessionInfo.class.getSimpleName();
// When the session was created
private final long mCreationTime;
// The id of the session
public final String mSessionId;
// Number of times this session has been used
public int mUseCount;
// Devices with which we have shared the session key
// userId -> {deviceId -> msgindex}
public final MXUsersDevicesMap<Integer> mSharedWithDevices;
// constructor
public MXOutboundSessionInfo(String sessionId) {
mSessionId = sessionId;
mSharedWithDevices = new MXUsersDevicesMap<>();
mCreationTime = System.currentTimeMillis();
mUseCount = 0;
}
public boolean needsRotation(int rotationPeriodMsgs, int rotationPeriodMs) {
boolean needsRotation = false;
long sessionLifetime = System.currentTimeMillis() - mCreationTime;
if ((mUseCount >= rotationPeriodMsgs) || (sessionLifetime >= rotationPeriodMs)) {
Log.d(LOG_TAG, "## needsRotation() : Rotating megolm session after " + mUseCount + ", " + sessionLifetime + "ms");
needsRotation = true;
}
return needsRotation;
}
/**
* Determine if this session has been shared with devices which it shouldn't have been.
*
* @param devicesInRoom the devices map
* @return true if we have shared the session with devices which aren't in devicesInRoom.
*/
public boolean sharedWithTooManyDevices(MXUsersDevicesMap<MXDeviceInfo> devicesInRoom) {
List<String> userIds = mSharedWithDevices.getUserIds();
for (String userId : userIds) {
if (null == devicesInRoom.getUserDeviceIds(userId)) {
Log.d(LOG_TAG, "## sharedWithTooManyDevices() : Starting new session because we shared with " + userId);
return true;
}
List<String> deviceIds = mSharedWithDevices.getUserDeviceIds(userId);
for (String deviceId : deviceIds) {
if (null == devicesInRoom.getObject(deviceId, userId)) {
Log.d(LOG_TAG, "## sharedWithTooManyDevices() : Starting new session because we shared with " + userId + ":" + deviceId);
return true;
}
}
}
return false;
}
}

View file

@ -0,0 +1,272 @@
/*
* Copyright 2015 OpenMarket Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.crypto.algorithms.olm;
import android.text.TextUtils;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import im.vector.matrix.android.internal.legacy.MXSession;
import im.vector.matrix.android.internal.legacy.crypto.IncomingRoomKeyRequest;
import im.vector.matrix.android.internal.legacy.crypto.MXCryptoError;
import im.vector.matrix.android.internal.legacy.crypto.MXDecryptionException;
import im.vector.matrix.android.internal.legacy.crypto.MXEventDecryptionResult;
import im.vector.matrix.android.internal.legacy.crypto.MXOlmDevice;
import im.vector.matrix.android.internal.legacy.crypto.algorithms.IMXDecrypting;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import im.vector.matrix.android.internal.legacy.rest.model.crypto.OlmEventContent;
import im.vector.matrix.android.internal.legacy.rest.model.crypto.OlmPayloadContent;
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* An interface for encrypting data
*/
public class MXOlmDecryption implements IMXDecrypting {
private static final String LOG_TAG = "MXOlmDecryption";
// The olm device interface
private MXOlmDevice mOlmDevice;
// the matrix session
private MXSession mSession;
@Override
public void initWithMatrixSession(MXSession matrixSession) {
mSession = matrixSession;
mOlmDevice = matrixSession.getCrypto().getOlmDevice();
}
@Override
public MXEventDecryptionResult decryptEvent(Event event, String timeline) throws MXDecryptionException {
// sanity check
if (null == event) {
Log.e(LOG_TAG, "## decryptEvent() : null event");
return null;
}
OlmEventContent olmEventContent = JsonUtils.toOlmEventContent(event.getWireContent().getAsJsonObject());
String deviceKey = olmEventContent.sender_key;
Map<String, Object> ciphertext = olmEventContent.ciphertext;
if (null == ciphertext) {
Log.e(LOG_TAG, "## decryptEvent() : missing cipher text");
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.MISSING_CIPHER_TEXT_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON));
}
if (!ciphertext.containsKey(mOlmDevice.getDeviceCurve25519Key())) {
Log.e(LOG_TAG, "## decryptEvent() : our device " + mOlmDevice.getDeviceCurve25519Key()
+ " is not included in recipients. Event " + event.getContentAsJsonObject());
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.NOT_INCLUDE_IN_RECIPIENTS_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON));
}
// The message for myUser
Map<String, Object> message = (Map<String, Object>) ciphertext.get(mOlmDevice.getDeviceCurve25519Key());
String payloadString = decryptMessage(message, deviceKey);
if (null == payloadString) {
Log.e(LOG_TAG, "## decryptEvent() Failed to decrypt Olm event (id= " + event.eventId + " ) from " + deviceKey);
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.BAD_ENCRYPTED_MESSAGE_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON));
}
JsonElement payload = new JsonParser().parse(JsonUtils.convertFromUTF8(payloadString));
if (null == payload) {
Log.e(LOG_TAG, "## decryptEvent failed : null payload");
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.UNABLE_TO_DECRYPT_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON));
}
OlmPayloadContent olmPayloadContent = JsonUtils.toOlmPayloadContent(payload);
if (TextUtils.isEmpty(olmPayloadContent.recipient)) {
String reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient");
Log.e(LOG_TAG, "## decryptEvent() : " + reason);
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.MISSING_PROPERTY_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, reason));
}
if (!TextUtils.equals(olmPayloadContent.recipient, mSession.getMyUserId())) {
Log.e(LOG_TAG, "## decryptEvent() : Event " + event.eventId + ": Intended recipient " + olmPayloadContent.recipient
+ " does not match our id " + mSession.getMyUserId());
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.BAD_RECIPIENT_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient)));
}
if (null == olmPayloadContent.recipient_keys) {
Log.e(LOG_TAG, "## decryptEvent() : Olm event (id=" + event.eventId
+ ") contains no " + "'recipient_keys' property; cannot prevent unknown-key attack");
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.MISSING_PROPERTY_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys")));
}
String ed25519 = olmPayloadContent.recipient_keys.get("ed25519");
if (!TextUtils.equals(ed25519, mOlmDevice.getDeviceEd25519Key())) {
Log.e(LOG_TAG, "## decryptEvent() : Event " + event.eventId + ": Intended recipient ed25519 key " + ed25519 + " did not match ours");
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.BAD_RECIPIENT_KEY_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.BAD_RECIPIENT_KEY_REASON));
}
if (TextUtils.isEmpty(olmPayloadContent.sender)) {
Log.e(LOG_TAG, "## decryptEvent() : Olm event (id=" + event.eventId
+ ") contains no 'sender' property; cannot prevent unknown-key attack");
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.MISSING_PROPERTY_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender")));
}
if (!TextUtils.equals(olmPayloadContent.sender, event.getSender())) {
Log.e(LOG_TAG, "Event " + event.eventId + ": original sender " + olmPayloadContent.sender
+ " does not match reported sender " + event.getSender());
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.FORWARDED_MESSAGE_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender)));
}
if (!TextUtils.equals(olmPayloadContent.room_id, event.roomId)) {
Log.e(LOG_TAG, "## decryptEvent() : Event " + event.eventId + ": original room " + olmPayloadContent.room_id
+ " does not match reported room " + event.roomId);
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.BAD_ROOM_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.room_id)));
}
if (null == olmPayloadContent.keys) {
Log.e(LOG_TAG, "## decryptEvent failed : null keys");
throw new MXDecryptionException(new MXCryptoError(MXCryptoError.UNABLE_TO_DECRYPT_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON));
}
MXEventDecryptionResult result = new MXEventDecryptionResult();
result.mClearEvent = payload;
result.mSenderCurve25519Key = deviceKey;
result.mClaimedEd25519Key = olmPayloadContent.keys.get("ed25519");
return result;
}
@Override
public void onRoomKeyEvent(Event event) {
// No impact for olm
}
@Override
public void onNewSession(String senderKey, String sessionId) {
// No impact for olm
}
@Override
public boolean hasKeysForKeyRequest(IncomingRoomKeyRequest request) {
return false;
}
@Override
public void shareKeysWithDevice(IncomingRoomKeyRequest request) {
}
/**
* Attempt to decrypt an Olm message.
*
* @param theirDeviceIdentityKey the Curve25519 identity key of the sender.
* @param message message object, with 'type' and 'body' fields.
* @return payload, if decrypted successfully.
*/
private String decryptMessage(Map<String, Object> message, String theirDeviceIdentityKey) {
Set<String> sessionIdsSet = mOlmDevice.getSessionIds(theirDeviceIdentityKey);
List<String> sessionIds;
if (null == sessionIdsSet) {
sessionIds = new ArrayList<>();
} else {
sessionIds = new ArrayList<>(sessionIdsSet);
}
String messageBody = (String) message.get("body");
Integer messageType = null;
Object typeAsVoid = message.get("type");
if (null != typeAsVoid) {
if (typeAsVoid instanceof Double) {
messageType = new Integer(((Double) typeAsVoid).intValue());
} else if (typeAsVoid instanceof Integer) {
messageType = (Integer) typeAsVoid;
} else if (typeAsVoid instanceof Long) {
messageType = new Integer(((Long) typeAsVoid).intValue());
}
}
if ((null == messageBody) || (null == messageType)) {
return null;
}
// Try each session in turn
// decryptionErrors = {};
for (String sessionId : sessionIds) {
String payload = mOlmDevice.decryptMessage(messageBody, messageType, sessionId, theirDeviceIdentityKey);
if (null != payload) {
Log.d(LOG_TAG, "## decryptMessage() : Decrypted Olm message from " + theirDeviceIdentityKey + " with session " + sessionId);
return payload;
} else {
boolean foundSession = mOlmDevice.matchesSession(theirDeviceIdentityKey, sessionId, messageType, messageBody);
if (foundSession) {
// Decryption failed, but it was a prekey message matching this
// session, so it should have worked.
Log.e(LOG_TAG, "## decryptMessage() : Error decrypting prekey message with existing session id " + sessionId + ":TODO");
return null;
}
}
}
if (messageType != 0) {
// not a prekey message, so it should have matched an existing session, but it
// didn't work.
if (sessionIds.size() == 0) {
Log.e(LOG_TAG, "## decryptMessage() : No existing sessions");
} else {
Log.e(LOG_TAG, "## decryptMessage() : Error decrypting non-prekey message with existing sessions");
}
return null;
}
// prekey message which doesn't match any existing sessions: make a new
// session.
Map<String, String> res = mOlmDevice.createInboundSession(theirDeviceIdentityKey, messageType, messageBody);
if (null == res) {
Log.e(LOG_TAG, "## decryptMessage() : Error decrypting non-prekey message with existing sessions");
return null;
}
Log.d(LOG_TAG, "## decryptMessage() : Created new inbound Olm session get id " + res.get("session_id") + " with " + theirDeviceIdentityKey);
return res.get("payload");
}
}

View file

@ -0,0 +1,126 @@
/*
* Copyright 2015 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.crypto.algorithms.olm;
import android.text.TextUtils;
import com.google.gson.JsonElement;
import im.vector.matrix.android.internal.legacy.MXSession;
import im.vector.matrix.android.internal.legacy.crypto.MXCrypto;
import im.vector.matrix.android.internal.legacy.crypto.algorithms.IMXEncrypting;
import im.vector.matrix.android.internal.legacy.crypto.data.MXDeviceInfo;
import im.vector.matrix.android.internal.legacy.crypto.data.MXOlmSessionResult;
import im.vector.matrix.android.internal.legacy.crypto.data.MXUsersDevicesMap;
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
import im.vector.matrix.android.internal.legacy.rest.callback.SimpleApiCallback;
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MXOlmEncryption implements IMXEncrypting {
private MXCrypto mCrypto;
private String mRoomId;
@Override
public void initWithMatrixSession(MXSession matrixSession, String roomId) {
mCrypto = matrixSession.getCrypto();
mRoomId = roomId;
}
/**
* @return the stored device keys for a user.
*/
private List<MXDeviceInfo> getUserDevices(final String userId) {
Map<String, MXDeviceInfo> map = mCrypto.getCryptoStore().getUserDevices(userId);
return (null != map) ? new ArrayList<>(map.values()) : new ArrayList<MXDeviceInfo>();
}
@Override
public void encryptEventContent(final JsonElement eventContent,
final String eventType,
final List<String> userIds,
final ApiCallback<JsonElement> callback) {
// pick the list of recipients based on the membership list.
//
// TODO: there is a race condition here! What if a new user turns up
ensureSession(userIds, new SimpleApiCallback<Void>(callback) {
@Override
public void onSuccess(Void info) {
List<MXDeviceInfo> deviceInfos = new ArrayList<>();
for (String userId : userIds) {
List<MXDeviceInfo> devices = getUserDevices(userId);
if (null != devices) {
for (MXDeviceInfo device : devices) {
String key = device.identityKey();
if (TextUtils.equals(key, mCrypto.getOlmDevice().getDeviceCurve25519Key())) {
// Don't bother setting up session to ourself
continue;
}
if (device.isBlocked()) {
// Don't bother setting up sessions with blocked users
continue;
}
deviceInfos.add(device);
}
}
}
Map<String, Object> messageMap = new HashMap<>();
messageMap.put("room_id", mRoomId);
messageMap.put("type", eventType);
messageMap.put("content", eventContent);
mCrypto.encryptMessage(messageMap, deviceInfos);
callback.onSuccess(JsonUtils.getGson(false).toJsonTree(messageMap));
}
}
);
}
/**
* Ensure that the session
*
* @param users the user ids list
* @param callback the asynchronous callback
*/
private void ensureSession(final List<String> users, final ApiCallback<Void> callback) {
mCrypto.getDeviceList().downloadKeys(users, false, new SimpleApiCallback<MXUsersDevicesMap<MXDeviceInfo>>(callback) {
@Override
public void onSuccess(MXUsersDevicesMap<MXDeviceInfo> info) {
mCrypto.ensureOlmSessionsForUsers(users, new SimpleApiCallback<MXUsersDevicesMap<MXOlmSessionResult>>(callback) {
@Override
public void onSuccess(MXUsersDevicesMap<MXOlmSessionResult> result) {
if (null != callback) {
callback.onSuccess(null);
}
}
});
}
});
}
}

View file

@ -0,0 +1,225 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.crypto.data;
import android.text.TextUtils;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MXDeviceInfo implements Serializable {
private static final long serialVersionUID = 20129670646382964L;
//
//private static final String LOG_TAG = "MXDeviceInfo";
// This device is a new device and the user was not warned it has been added.
public static final int DEVICE_VERIFICATION_UNKNOWN = -1;
// The user has not yet verified this device.
public static final int DEVICE_VERIFICATION_UNVERIFIED = 0;
// The user has verified this device.
public static final int DEVICE_VERIFICATION_VERIFIED = 1;
// The user has blocked this device.
public static final int DEVICE_VERIFICATION_BLOCKED = 2;
/**
* The id of this device.
*/
public String deviceId;
/**
* the user id
*/
public String userId;
/**
* The list of algorithms supported by this device.
*/
public List<String> algorithms;
/**
* A map from <key type>:<id> to <base64-encoded key>>.
*/
public Map<String, String> keys;
/**
* The signature of this MXDeviceInfo.
* A map from <key type>:<device_id> to <base64-encoded key>>.
*/
public Map<String, Map<String, String>> signatures;
/*
* Additional data from the home server.
*/
public Map<String, Object> unsigned;
/**
* Verification state of this device.
*/
public int mVerified;
/**
* Constructor
*/
public MXDeviceInfo() {
mVerified = DEVICE_VERIFICATION_UNKNOWN;
}
/**
* Constructor
*
* @param aDeviceId the device id
*/
public MXDeviceInfo(String aDeviceId) {
deviceId = aDeviceId;
mVerified = DEVICE_VERIFICATION_UNKNOWN;
}
/**
* Tells if the device is unknown
*
* @return true if the device is unknown
*/
public boolean isUnknown() {
return mVerified == DEVICE_VERIFICATION_UNKNOWN;
}
/**
* Tells if the device is verified.
*
* @return true if the device is verified
*/
public boolean isVerified() {
return mVerified == DEVICE_VERIFICATION_VERIFIED;
}
/**
* Tells if the device is unverified.
*
* @return true if the device is unverified
*/
public boolean isUnverified() {
return mVerified == DEVICE_VERIFICATION_UNVERIFIED;
}
/**
* Tells if the device is blocked.
*
* @return true if the device is blocked
*/
public boolean isBlocked() {
return mVerified == DEVICE_VERIFICATION_BLOCKED;
}
/**
* @return the fingerprint
*/
public String fingerprint() {
if ((null != keys) && !TextUtils.isEmpty(deviceId)) {
return keys.get("ed25519:" + deviceId);
}
return null;
}
/**
* @return the identity key
*/
public String identityKey() {
if ((null != keys) && !TextUtils.isEmpty(deviceId)) {
return keys.get("curve25519:" + deviceId);
}
return null;
}
/**
* @return the display name
*/
public String displayName() {
if (null != unsigned) {
return (String) unsigned.get("device_display_name");
}
return null;
}
/**
* @return the signed data map
*/
public Map<String, Object> signalableJSONDictionary() {
Map<String, Object> map = new HashMap<>();
map.put("device_id", deviceId);
if (null != userId) {
map.put("user_id", userId);
}
if (null != algorithms) {
map.put("algorithms", algorithms);
}
if (null != keys) {
map.put("keys", keys);
}
return map;
}
/**
* @return a dictionary of the parameters
*/
public Map<String, Object> JSONDictionary() {
Map<String, Object> JSONDictionary = new HashMap<>();
JSONDictionary.put("device_id", deviceId);
if (null != userId) {
JSONDictionary.put("user_id", userId);
}
if (null != algorithms) {
JSONDictionary.put("algorithms", algorithms);
}
if (null != keys) {
JSONDictionary.put("keys", keys);
}
if (null != signatures) {
JSONDictionary.put("signatures", signatures);
}
if (null != unsigned) {
JSONDictionary.put("unsigned", unsigned);
}
return JSONDictionary;
}
@Override
public java.lang.String toString() {
return "MXDeviceInfo " + userId + ":" + deviceId;
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,140 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.crypto.data;
import android.text.TextUtils;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MXKey implements Serializable {
private static final String LOG_TAG = "MXKey";
/**
* Key types.
*/
public static final String KEY_CURVE_25519_TYPE = "curve25519";
public static final String KEY_SIGNED_CURVE_25519_TYPE = "signed_curve25519";
//public static final String KEY_ED_25519_TYPE = "ed25519";
/**
* The type of the key.
*/
public String type;
/**
* The id of the key.
*/
public String keyId;
/**
* The key.
*/
public String value;
/**
* signature user Id to [deviceid][signature]
*/
public Map<String, Map<String, String>> signatures;
/**
* Default constructor
*/
public MXKey() {
}
/**
* Convert a map to a MXKey
*
* @param map the map to convert
*/
public MXKey(Map<String, Map<String, Object>> map) {
if ((null != map) && (map.size() > 0)) {
List<String> mapKeys = new ArrayList<>(map.keySet());
String firstEntry = mapKeys.get(0);
setKeyFullId(firstEntry);
Map<String, Object> params = map.get(firstEntry);
value = (String) params.get("key");
signatures = (Map<String, Map<String, String>>) params.get("signatures");
}
}
/**
* @return the key full id
*/
public String getKeyFullId() {
return type + ":" + keyId;
}
/**
* Update the key fields with a key full id
*
* @param keyFullId the key full id
*/
private void setKeyFullId(String keyFullId) {
if (!TextUtils.isEmpty(keyFullId)) {
try {
String[] components = keyFullId.split(":");
if (components.length == 2) {
type = components[0];
keyId = components[1];
}
} catch (Exception e) {
Log.e(LOG_TAG, "## setKeyFullId() failed : " + e.getMessage(), e);
}
}
}
/**
* @return the signed data map
*/
public Map<String, Object> signalableJSONDictionary() {
Map<String, Object> map = new HashMap<>();
if (null != value) {
map.put("key", value);
}
return map;
}
/**
* Returns a signature for an user Id and a signkey
*
* @param userId the user id
* @param signkey the sign key
* @return the signature
*/
public String signatureForUserId(String userId, String signkey) {
// sanity checks
if (!TextUtils.isEmpty(userId) && !TextUtils.isEmpty(signkey)) {
if ((null != signatures) && signatures.containsKey(userId)) {
return signatures.get(userId).get(signkey);
}
}
return null;
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright 2016 OpenMarket Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.crypto.data;
import im.vector.matrix.android.internal.legacy.util.Log;
import org.matrix.olm.OlmInboundGroupSession;
import java.io.Serializable;
import java.util.Map;
/**
* This class adds more context to a OLMInboundGroupSession object.
* This allows additional checks. The class implements NSCoding so that the context can be stored.
*/
public class MXOlmInboundGroupSession implements Serializable {
//
private static final String LOG_TAG = "OlmInboundGroupSession";
// The associated olm inbound group session.
public OlmInboundGroupSession mSession;
// The room in which this session is used.
public String mRoomId;
// The base64-encoded curve25519 key of the sender.
public String mSenderKey;
// Other keys the sender claims.
public Map<String, String> mKeysClaimed;
/**
* Constructor
*
* @param sessionKey the session key
*/
public MXOlmInboundGroupSession(String sessionKey) {
try {
mSession = new OlmInboundGroupSession(sessionKey);
} catch (Exception e) {
Log.e(LOG_TAG, "Cannot create : " + e.getMessage(), e);
}
}
}

View file

@ -0,0 +1,172 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.crypto.data;
import android.text.TextUtils;
import im.vector.matrix.android.internal.legacy.crypto.MXCryptoAlgorithms;
import im.vector.matrix.android.internal.legacy.util.Log;
import org.matrix.olm.OlmInboundGroupSession;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* This class adds more context to a OLMInboundGroupSession object.
* This allows additional checks. The class implements NSCoding so that the context can be stored.
*/
public class MXOlmInboundGroupSession2 implements Serializable {
//
private static final String LOG_TAG = "OlmInboundGroupSession";
// define a serialVersionUID to avoid having to redefine the class after updates
private static final long serialVersionUID = 201702011617L;
// The associated olm inbound group session.
public OlmInboundGroupSession mSession;
// The room in which this session is used.
public String mRoomId;
// The base64-encoded curve25519 key of the sender.
public String mSenderKey;
// Other keys the sender claims.
public Map<String, String> mKeysClaimed;
// Devices which forwarded this session to us (normally empty).
public List<String> mForwardingCurve25519KeyChain = new ArrayList<>();
/**
* Constructor
*
* @param prevFormatSession the previous session format
*/
public MXOlmInboundGroupSession2(MXOlmInboundGroupSession prevFormatSession) {
mSession = prevFormatSession.mSession;
mRoomId = prevFormatSession.mRoomId;
mSenderKey = prevFormatSession.mSenderKey;
mKeysClaimed = prevFormatSession.mKeysClaimed;
}
/**
* Constructor
*
* @param sessionKey the session key
* @param isImported true if it is an imported session key
*/
public MXOlmInboundGroupSession2(String sessionKey, boolean isImported) {
try {
if (!isImported) {
mSession = new OlmInboundGroupSession(sessionKey);
} else {
mSession = OlmInboundGroupSession.importSession(sessionKey);
}
} catch (Exception e) {
Log.e(LOG_TAG, "Cannot create : " + e.getMessage(), e);
}
}
/**
* Create a new instance from the provided keys map.
*
* @param map the map
* @throws Exception if the data are invalid
*/
public MXOlmInboundGroupSession2(Map<String, Object> map) throws Exception {
try {
mSession = OlmInboundGroupSession.importSession((String) map.get("session_key"));
if (!TextUtils.equals(mSession.sessionIdentifier(), (String) map.get("session_id"))) {
throw new Exception("Mismatched group session Id");
}
mSenderKey = (String) map.get("sender_key");
mKeysClaimed = (Map<String, String>) map.get("sender_claimed_keys");
mRoomId = (String) map.get("room_id");
} catch (Exception e) {
throw new Exception(e.getMessage());
}
}
/**
* Export the inbound group session keys
*
* @return the inbound group session as map if the operation succeeds
*/
public Map<String, Object> exportKeys() {
Map<String, Object> map = new HashMap<>();
try {
if (null == mForwardingCurve25519KeyChain) {
mForwardingCurve25519KeyChain = new ArrayList<>();
}
map.put("sender_claimed_ed25519_key", mKeysClaimed.get("ed25519"));
map.put("forwardingCurve25519KeyChain", mForwardingCurve25519KeyChain);
map.put("sender_key", mSenderKey);
map.put("sender_claimed_keys", mKeysClaimed);
map.put("room_id", mRoomId);
map.put("session_id", mSession.sessionIdentifier());
map.put("session_key", mSession.export(mSession.getFirstKnownIndex()));
map.put("algorithm", MXCryptoAlgorithms.MXCRYPTO_ALGORITHM_MEGOLM);
} catch (Exception e) {
map = null;
Log.e(LOG_TAG, "## export() : senderKey " + mSenderKey + " failed " + e.getMessage(), e);
}
return map;
}
/**
* @return the first known message index
*/
public Long getFirstKnownIndex() {
if (null != mSession) {
try {
return mSession.getFirstKnownIndex();
} catch (Exception e) {
Log.e(LOG_TAG, "## getFirstKnownIndex() : getFirstKnownIndex failed " + e.getMessage(), e);
}
}
return null;
}
/**
* Export the session for a message index.
*
* @param messageIndex the message index
* @return the exported data
*/
public String exportSession(long messageIndex) {
if (null != mSession) {
try {
return mSession.export(messageIndex);
} catch (Exception e) {
Log.e(LOG_TAG, "## exportSession() : export failed " + e.getMessage(), e);
}
}
return null;
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright 2016 OpenMarket Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.crypto.data;
import com.google.gson.JsonElement;
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
public class MXQueuedEncryption {
/**
* The data to encrypt.
*/
public JsonElement mEventContent;
public String mEventType;
/**
* the asynchronous callback
*/
public ApiCallback<JsonElement> mApiCallback;
}

View file

@ -0,0 +1,186 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.crypto.data;
import android.text.TextUtils;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class MXUsersDevicesMap<E> implements Serializable {
// The device keys as returned by the homeserver: a map of a map (userId -> deviceId -> Object).
private final Map<String, Map<String, E>> mMap = new HashMap<>();
/**
* @return the inner map
*/
public Map<String, Map<String, E>> getMap() {
return mMap;
}
/**
* Default constructor constructor
*/
public MXUsersDevicesMap() {
}
/**
* The constructor
*
* @param map the map
*/
public MXUsersDevicesMap(Map<String, Map<String, E>> map) {
if (null != map) {
Set<String> keys = map.keySet();
for (String key : keys) {
mMap.put(key, new HashMap<>(map.get(key)));
}
}
}
/**
* @return a deep copy
*/
public MXUsersDevicesMap<E> deepCopy() {
MXUsersDevicesMap<E> copy = new MXUsersDevicesMap<>();
Set<String> keys = mMap.keySet();
for (String key : keys) {
copy.mMap.put(key, new HashMap<>(mMap.get(key)));
}
return copy;
}
/**
* @return the user Ids
*/
public List<String> getUserIds() {
return new ArrayList<>(mMap.keySet());
}
/**
* Provides the device ids list for an user id
*
* @param userId the user id
* @return the device ids list
*/
public List<String> getUserDeviceIds(String userId) {
if (!TextUtils.isEmpty(userId) && mMap.containsKey(userId)) {
return new ArrayList<>(mMap.get(userId).keySet());
}
return null;
}
/**
* Provides the object for a device id and an user Id
*
* @param deviceId the device id
* @param userId the object id
* @return the object
*/
public E getObject(String deviceId, String userId) {
if (!TextUtils.isEmpty(userId) && mMap.containsKey(userId) && !TextUtils.isEmpty(deviceId)) {
return mMap.get(userId).get(deviceId);
}
return null;
}
/**
* Set an object for a dedicated user Id and device Id
*
* @param object the object to set
* @param userId the user Id
* @param deviceId the device id
*/
public void setObject(E object, String userId, String deviceId) {
if ((null != object) && !TextUtils.isEmpty(userId) && !TextUtils.isEmpty(deviceId)) {
Map<String, E> subMap = mMap.get(userId);
if (null == subMap) {
subMap = new HashMap<>();
mMap.put(userId, subMap);
}
subMap.put(deviceId, object);
}
}
/**
* Defines the objects map for an user Id
*
* @param objectsPerDevices the objects maps
* @param userId the user id
*/
public void setObjects(Map<String, E> objectsPerDevices, String userId) {
if (!TextUtils.isEmpty(userId)) {
if (null == objectsPerDevices) {
mMap.remove(userId);
} else {
mMap.put(userId, new HashMap<>(objectsPerDevices));
}
}
}
/**
* Removes objects for a dedicated user
*
* @param userId the user id.
*/
public void removeUserObjects(String userId) {
if (!TextUtils.isEmpty(userId)) {
mMap.remove(userId);
}
}
/**
* Clear the internal dictionary
*/
public void removeAllObjects() {
mMap.clear();
}
/**
* Add entries from another MXUsersDevicesMap
*
* @param other the other one
*/
public void addEntriesFromMap(MXUsersDevicesMap<E> other) {
if (null != other) {
mMap.putAll(other.getMap());
}
}
@Override
public java.lang.String toString() {
if (null != mMap) {
return "MXUsersDevicesMap " + mMap.toString();
} else {
return "MXDeviceInfo : null map";
}
}
}

View file

@ -0,0 +1,423 @@
/*
* Copyright 2014 OpenMarket Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.data;
import android.os.Looper;
import android.text.TextUtils;
import im.vector.matrix.android.internal.legacy.data.store.IMXStore;
import im.vector.matrix.android.internal.legacy.data.timeline.EventTimeline;
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
import im.vector.matrix.android.internal.legacy.rest.callback.SimpleApiCallback;
import im.vector.matrix.android.internal.legacy.rest.client.RoomsRestClient;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
import im.vector.matrix.android.internal.legacy.rest.model.TokensChunkEvents;
import im.vector.matrix.android.internal.legacy.util.FilterUtil;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* Layer for retrieving data either from the storage implementation, or from the server if the information is not available.
*/
public class DataRetriever {
private static final String LOG_TAG = DataRetriever.class.getSimpleName();
private RoomsRestClient mRestClient;
private final Map<String, String> mPendingForwardRequestTokenByRoomId = new HashMap<>();
private final Map<String, String> mPendingBackwardRequestTokenByRoomId = new HashMap<>();
private final Map<String, String> mPendingRemoteRequestTokenByRoomId = new HashMap<>();
public RoomsRestClient getRoomsRestClient() {
return mRestClient;
}
public void setRoomsRestClient(final RoomsRestClient client) {
mRestClient = client;
}
/**
* Provides the cached messages for a dedicated roomId
*
* @param store the store.
* @param roomId the roomId
* @return the events list, null if the room does not exist
*/
public Collection<Event> getCachedRoomMessages(final IMXStore store, final String roomId) {
return store.getRoomMessages(roomId);
}
/**
* Cancel any history requests for a dedicated room
*
* @param roomId the room id.
*/
public void cancelHistoryRequests(final String roomId) {
Log.d(LOG_TAG, "## cancelHistoryRequests() : roomId " + roomId);
clearPendingToken(mPendingForwardRequestTokenByRoomId, roomId);
clearPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
}
/**
* Cancel any request history requests for a dedicated room
*
* @param roomId the room id.
*/
public void cancelRemoteHistoryRequest(final String roomId) {
Log.d(LOG_TAG, "## cancelRemoteHistoryRequest() : roomId " + roomId);
clearPendingToken(mPendingRemoteRequestTokenByRoomId, roomId);
}
/**
* Get the event associated with the eventId and roomId
* Look in the store before hitting the rest client.
*
* @param store the store to look in
* @param roomId the room Id
* @param eventId the eventId
* @param callback the callback
*/
public void getEvent(final IMXStore store, final String roomId, final String eventId, final ApiCallback<Event> callback) {
final Event event = store.getEvent(eventId, roomId);
if (event == null) {
mRestClient.getEvent(roomId, eventId, callback);
} else {
callback.onSuccess(event);
}
}
/**
* Trigger a back pagination for a dedicated room from Token.
*
* @param store the store to use
* @param roomId the room Id
* @param token the start token.
* @param limit the maximum number of messages to retrieve
* @param withLazyLoading true when lazy loading is enabled
* @param callback the callback
*/
public void backPaginate(final IMXStore store,
final String roomId,
final String token,
final int limit,
final boolean withLazyLoading,
final ApiCallback<TokensChunkEvents> callback) {
// reach the marker end
if (TextUtils.equals(token, Event.PAGINATE_BACK_TOKEN_END)) {
// nothing more to provide
final android.os.Handler handler = new android.os.Handler(Looper.getMainLooper());
// call the callback with a delay
// to reproduce the same behaviour as a network request.
// except for the initial request.
Runnable r = new Runnable() {
@Override
public void run() {
handler.postDelayed(new Runnable() {
public void run() {
callback.onSuccess(new TokensChunkEvents());
}
}, 0);
}
};
handler.post(r);
return;
}
Log.d(LOG_TAG, "## backPaginate() : starts for roomId " + roomId);
TokensChunkEvents storageResponse = store.getEarlierMessages(roomId, token, limit);
putPendingToken(mPendingBackwardRequestTokenByRoomId, roomId, token);
if (storageResponse != null) {
final android.os.Handler handler = new android.os.Handler(Looper.getMainLooper());
final TokensChunkEvents fStorageResponse = storageResponse;
Log.d(LOG_TAG, "## backPaginate() : some data has been retrieved into the local storage (" + fStorageResponse.chunk.size() + " events)");
// call the callback with a delay
// to reproduce the same behaviour as a network request.
// except for the initial request.
Runnable r = new Runnable() {
@Override
public void run() {
handler.postDelayed(new Runnable() {
public void run() {
String expectedToken = getPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
Log.d(LOG_TAG, "## backPaginate() : local store roomId " + roomId + " token " + token + " vs " + expectedToken);
if (TextUtils.equals(expectedToken, token)) {
clearPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
callback.onSuccess(fStorageResponse);
}
}
}, 0);
}
};
Thread t = new Thread(r);
t.start();
} else {
Log.d(LOG_TAG, "## backPaginate() : trigger a remote request");
mRestClient.getRoomMessagesFrom(roomId, token, EventTimeline.Direction.BACKWARDS, limit, FilterUtil.createRoomEventFilter(withLazyLoading),
new SimpleApiCallback<TokensChunkEvents>(callback) {
@Override
public void onSuccess(TokensChunkEvents tokensChunkEvents) {
String expectedToken = getPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
Log.d(LOG_TAG, "## backPaginate() succeeds : roomId " + roomId + " token " + token + " vs " + expectedToken);
if (TextUtils.equals(expectedToken, token)) {
clearPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
// Watch for the one event overlap
Event oldestEvent = store.getOldestEvent(roomId);
if (tokensChunkEvents.chunk.size() != 0) {
tokensChunkEvents.chunk.get(0).mToken = tokensChunkEvents.start;
// there is no more data on server side
if (null == tokensChunkEvents.end) {
tokensChunkEvents.end = Event.PAGINATE_BACK_TOKEN_END;
}
tokensChunkEvents.chunk.get(tokensChunkEvents.chunk.size() - 1).mToken = tokensChunkEvents.end;
Event firstReturnedEvent = tokensChunkEvents.chunk.get(0);
if ((oldestEvent != null) && (firstReturnedEvent != null)
&& TextUtils.equals(oldestEvent.eventId, firstReturnedEvent.eventId)) {
tokensChunkEvents.chunk.remove(0);
}
store.storeRoomEvents(roomId, tokensChunkEvents, EventTimeline.Direction.BACKWARDS);
}
Log.d(LOG_TAG, "## backPaginate() succeed : roomId " + roomId
+ " token " + token
+ " got " + tokensChunkEvents.chunk.size());
callback.onSuccess(tokensChunkEvents);
}
}
private void logErrorMessage(String expectedToken, String errorMessage) {
Log.e(LOG_TAG, "## backPaginate() failed : roomId " + roomId
+ " token " + token
+ " expected " + expectedToken
+ " with " + errorMessage);
}
@Override
public void onNetworkError(Exception e) {
String expectedToken = getPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
logErrorMessage(expectedToken, e.getMessage());
// dispatch only if it is expected
if (TextUtils.equals(token, expectedToken)) {
clearPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
callback.onNetworkError(e);
}
}
@Override
public void onMatrixError(MatrixError e) {
String expectedToken = getPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
logErrorMessage(expectedToken, e.getMessage());
// dispatch only if it is expected
if (TextUtils.equals(token, expectedToken)) {
clearPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
callback.onMatrixError(e);
}
}
@Override
public void onUnexpectedError(Exception e) {
String expectedToken = getPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
logErrorMessage(expectedToken, e.getMessage());
// dispatch only if it is expected
if (TextUtils.equals(token, expectedToken)) {
clearPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
callback.onUnexpectedError(e);
}
}
});
}
}
/**
* Trigger a forward pagination for a dedicated room from Token.
*
* @param store the store to use
* @param roomId the room Id
* @param token the start token.
* @param withLazyLoading true when lazy loading is enabled
* @param callback the callback
*/
private void forwardPaginate(final IMXStore store,
final String roomId,
final String token,
final boolean withLazyLoading,
final ApiCallback<TokensChunkEvents> callback) {
putPendingToken(mPendingForwardRequestTokenByRoomId, roomId, token);
mRestClient.getRoomMessagesFrom(roomId, token, EventTimeline.Direction.FORWARDS, RoomsRestClient.DEFAULT_MESSAGES_PAGINATION_LIMIT,
FilterUtil.createRoomEventFilter(withLazyLoading),
new SimpleApiCallback<TokensChunkEvents>(callback) {
@Override
public void onSuccess(TokensChunkEvents tokensChunkEvents) {
if (TextUtils.equals(getPendingToken(mPendingForwardRequestTokenByRoomId, roomId), token)) {
clearPendingToken(mPendingForwardRequestTokenByRoomId, roomId);
store.storeRoomEvents(roomId, tokensChunkEvents, EventTimeline.Direction.FORWARDS);
callback.onSuccess(tokensChunkEvents);
}
}
});
}
/**
* Request messages than the given token. These will come from storage if available, from the server otherwise.
*
* @param store the store to use
* @param roomId the room id
* @param token the token to go back from. Null to start from live.
* @param direction the pagination direction
* @param withLazyLoading true when lazy loading is enabled
* @param callback the onComplete callback
*/
public void paginate(final IMXStore store,
final String roomId,
final String token,
final EventTimeline.Direction direction,
final boolean withLazyLoading,
final ApiCallback<TokensChunkEvents> callback) {
if (direction == EventTimeline.Direction.BACKWARDS) {
backPaginate(store, roomId, token, RoomsRestClient.DEFAULT_MESSAGES_PAGINATION_LIMIT, withLazyLoading, callback);
} else {
forwardPaginate(store, roomId, token, withLazyLoading, callback);
}
}
/**
* Request events to the server. The local cache is not used.
* The events will not be saved in the local storage.
*
* @param roomId the room id
* @param token the token to go back from.
* @param paginationCount the number of events to retrieve.
* @param withLazyLoading true when lazy loading is enabled
* @param callback the onComplete callback
*/
public void requestServerRoomHistory(final String roomId,
final String token,
final int paginationCount,
final boolean withLazyLoading,
final ApiCallback<TokensChunkEvents> callback) {
putPendingToken(mPendingRemoteRequestTokenByRoomId, roomId, token);
mRestClient.getRoomMessagesFrom(roomId, token, EventTimeline.Direction.BACKWARDS, paginationCount, FilterUtil.createRoomEventFilter(withLazyLoading),
new SimpleApiCallback<TokensChunkEvents>(callback) {
@Override
public void onSuccess(TokensChunkEvents info) {
if (TextUtils.equals(getPendingToken(mPendingRemoteRequestTokenByRoomId, roomId), token)) {
if (info.chunk.size() != 0) {
info.chunk.get(0).mToken = info.start;
info.chunk.get(info.chunk.size() - 1).mToken = info.end;
}
clearPendingToken(mPendingRemoteRequestTokenByRoomId, roomId);
callback.onSuccess(info);
}
}
});
}
//==============================================================================================================
// Pending token management
//==============================================================================================================
/**
* Clear token for a dedicated room
*
* @param dict the token cache
* @param roomId the room id
*/
private void clearPendingToken(final Map<String, String> dict, final String roomId) {
Log.d(LOG_TAG, "## clearPendingToken() : roomId " + roomId);
if (null != roomId) {
synchronized (dict) {
dict.remove(roomId);
}
}
}
/**
* Get the pending token for a dedicated room
*
* @param dict the token cache
* @param roomId the room Id
* @return the token
*/
private String getPendingToken(final Map<String, String> dict, final String roomId) {
String expectedToken = "Not a valid token";
synchronized (dict) {
// token == null is a valid value
if (dict.containsKey(roomId)) {
expectedToken = dict.get(roomId);
if (TextUtils.isEmpty(expectedToken)) {
expectedToken = null;
}
}
}
Log.d(LOG_TAG, "## getPendingToken() : roomId " + roomId + " token " + expectedToken);
return expectedToken;
}
/**
* Store a token for a dedicated room
*
* @param dict the token cache
* @param roomId the room id
* @param token the token
*/
private void putPendingToken(final Map<String, String> dict, final String roomId, final String token) {
Log.d(LOG_TAG, "## putPendingToken() : roomId " + roomId + " token " + token);
synchronized (dict) {
// null is allowed for a request
if (null == token) {
dict.put(roomId, "");
} else {
dict.put(roomId, token);
}
}
}
}

View file

@ -0,0 +1,446 @@
/*
* Copyright 2014 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.data;
import android.os.Handler;
import android.os.Looper;
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
import im.vector.matrix.android.internal.legacy.rest.callback.SimpleApiCallback;
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
import im.vector.matrix.android.internal.legacy.rest.model.User;
import im.vector.matrix.android.internal.legacy.rest.model.pid.ThirdPartyIdentifier;
import im.vector.matrix.android.internal.legacy.rest.model.pid.ThreePid;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.util.ArrayList;
import java.util.List;
/**
* Class representing the logged-in user.
*/
public class MyUser extends User {
private static final String LOG_TAG = MyUser.class.getSimpleName();
// refresh status
private boolean mIsAvatarRefreshed = false;
private boolean mIsDisplayNameRefreshed = false;
private boolean mAre3PIdsLoaded = false;
// the account info is refreshed in one row
// so, if there is a pending refresh the listeners are added to this list.
private transient List<ApiCallback<Void>> mRefreshListeners;
private transient final Handler mUiHandler;
// linked emails to the account
private transient List<ThirdPartyIdentifier> mEmailIdentifiers = new ArrayList<>();
// linked phone number to the account
private transient List<ThirdPartyIdentifier> mPhoneNumberIdentifiers = new ArrayList<>();
public MyUser(User user) {
clone(user);
mUiHandler = new Handler(Looper.getMainLooper());
}
/**
* Update the user's display name.
*
* @param displayName the new name
* @param callback the async callback
*/
public void updateDisplayName(final String displayName, final ApiCallback<Void> callback) {
mDataHandler.getProfileRestClient().updateDisplayname(displayName, new SimpleApiCallback<Void>(callback) {
@Override
public void onSuccess(Void info) {
// Update the object member before calling the given callback
MyUser.this.displayname = displayName;
mDataHandler.getStore().setDisplayName(displayName, System.currentTimeMillis());
callback.onSuccess(info);
}
});
}
/**
* Update the user's avatar URL.
*
* @param avatarUrl the new avatar URL
* @param callback the async callback
*/
public void updateAvatarUrl(final String avatarUrl, final ApiCallback<Void> callback) {
mDataHandler.getProfileRestClient().updateAvatarUrl(avatarUrl, new SimpleApiCallback<Void>(callback) {
@Override
public void onSuccess(Void info) {
// Update the object member before calling the given callback
setAvatarUrl(avatarUrl);
mDataHandler.getStore().setAvatarURL(avatarUrl, System.currentTimeMillis());
callback.onSuccess(info);
}
});
}
/**
* Request a validation token for an email address 3Pid
*
* @param pid the pid to retrieve a token
* @param callback the callback when the operation is done
*/
public void requestEmailValidationToken(ThreePid pid, ApiCallback<Void> callback) {
if (null != pid) {
pid.requestEmailValidationToken(mDataHandler.getProfileRestClient(), null, false, callback);
}
}
/**
* Request a validation token for a phone number 3Pid
*
* @param pid the pid to retrieve a token
* @param callback the callback when the operation is done
*/
public void requestPhoneNumberValidationToken(ThreePid pid, ApiCallback<Void> callback) {
if (null != pid) {
pid.requestPhoneNumberValidationToken(mDataHandler.getProfileRestClient(), false, callback);
}
}
/**
* Add a new pid to the account.
*
* @param pid the pid to add.
* @param bind true to add it.
* @param callback the async callback
*/
public void add3Pid(final ThreePid pid, final boolean bind, final ApiCallback<Void> callback) {
if (null != pid) {
mDataHandler.getProfileRestClient().add3PID(pid, bind, new SimpleApiCallback<Void>(callback) {
@Override
public void onSuccess(Void info) {
// refresh the third party identifiers lists
refreshThirdPartyIdentifiers(callback);
}
});
}
}
/**
* Delete a 3pid from an account
*
* @param pid the pid to delete
* @param callback the async callback
*/
public void delete3Pid(final ThirdPartyIdentifier pid, final ApiCallback<Void> callback) {
if (null != pid) {
mDataHandler.getProfileRestClient().delete3PID(pid, new SimpleApiCallback<Void>(callback) {
@Override
public void onSuccess(Void info) {
// refresh the third party identifiers lists
refreshThirdPartyIdentifiers(callback);
}
});
}
}
/**
* Build the lists of identifiers
*/
private void buildIdentifiersLists() {
List<ThirdPartyIdentifier> identifiers = mDataHandler.getStore().thirdPartyIdentifiers();
mEmailIdentifiers = new ArrayList<>();
mPhoneNumberIdentifiers = new ArrayList<>();
for (ThirdPartyIdentifier identifier : identifiers) {
switch (identifier.medium) {
case ThreePid.MEDIUM_EMAIL:
mEmailIdentifiers.add(identifier);
break;
case ThreePid.MEDIUM_MSISDN:
mPhoneNumberIdentifiers.add(identifier);
break;
}
}
}
/**
* @return the list of linked emails
*/
public List<ThirdPartyIdentifier> getlinkedEmails() {
if (mEmailIdentifiers == null) {
buildIdentifiersLists();
}
return mEmailIdentifiers;
}
/**
* @return the list of linked emails
*/
public List<ThirdPartyIdentifier> getlinkedPhoneNumbers() {
if (mPhoneNumberIdentifiers == null) {
buildIdentifiersLists();
}
return mPhoneNumberIdentifiers;
}
//================================================================================
// Refresh
//================================================================================
/**
* Refresh the user data if it is required
*
* @param callback callback when the job is done.
*/
public void refreshUserInfos(final ApiCallback<Void> callback) {
refreshUserInfos(false, callback);
}
/**
* Refresh the user data if it is required
*
* @param callback callback when the job is done.
*/
public void refreshThirdPartyIdentifiers(final ApiCallback<Void> callback) {
mAre3PIdsLoaded = false;
refreshUserInfos(false, callback);
}
/**
* Refresh the user data if it is required
*
* @param skipPendingTest true to do not check if the refreshes started (private use)
* @param callback callback when the job is done.
*/
public void refreshUserInfos(boolean skipPendingTest, final ApiCallback<Void> callback) {
if (!skipPendingTest) {
boolean isPending;
synchronized (this) {
// mRefreshListeners == null => no refresh in progress
// mRefreshListeners != null -> a refresh is in progress
isPending = (null != mRefreshListeners);
if (null == mRefreshListeners) {
mRefreshListeners = new ArrayList<>();
}
if (null != callback) {
mRefreshListeners.add(callback);
}
}
if (isPending) {
// please wait
return;
}
}
if (!mIsDisplayNameRefreshed) {
refreshUserDisplayname();
return;
}
if (!mIsAvatarRefreshed) {
refreshUserAvatarUrl();
return;
}
if (!mAre3PIdsLoaded) {
refreshThirdPartyIdentifiers();
return;
}
synchronized (this) {
if (null != mRefreshListeners) {
for (ApiCallback<Void> listener : mRefreshListeners) {
try {
listener.onSuccess(null);
} catch (Exception e) {
Log.e(LOG_TAG, "## refreshUserInfos() : listener.onSuccess failed " + e.getMessage(), e);
}
}
}
// no more pending refreshes
mRefreshListeners = null;
}
}
/**
* Refresh the avatar url
*/
private void refreshUserAvatarUrl() {
mDataHandler.getProfileRestClient().avatarUrl(user_id, new SimpleApiCallback<String>() {
@Override
public void onSuccess(String anAvatarUrl) {
if (mDataHandler.isAlive()) {
// local value
setAvatarUrl(anAvatarUrl);
// metadata file
mDataHandler.getStore().setAvatarURL(anAvatarUrl, System.currentTimeMillis());
// user
mDataHandler.getStore().storeUser(MyUser.this);
mIsAvatarRefreshed = true;
// jump to the next items
refreshUserInfos(true, null);
}
}
private void onError() {
if (mDataHandler.isAlive()) {
mUiHandler.postDelayed(new Runnable() {
@Override
public void run() {
refreshUserAvatarUrl();
}
}, 1 * 1000);
}
}
@Override
public void onNetworkError(Exception e) {
onError();
}
@Override
public void onMatrixError(final MatrixError e) {
// cannot retrieve this value, jump to the next items
mIsAvatarRefreshed = true;
refreshUserInfos(true, null);
}
@Override
public void onUnexpectedError(final Exception e) {
// cannot retrieve this value, jump to the next items
mIsAvatarRefreshed = true;
refreshUserInfos(true, null);
}
});
}
/**
* Refresh the displayname.
*/
private void refreshUserDisplayname() {
mDataHandler.getProfileRestClient().displayname(user_id, new SimpleApiCallback<String>() {
@Override
public void onSuccess(String aDisplayname) {
if (mDataHandler.isAlive()) {
// local value
displayname = aDisplayname;
// store metadata
mDataHandler.getStore().setDisplayName(aDisplayname, System.currentTimeMillis());
mIsDisplayNameRefreshed = true;
// jump to the next items
refreshUserInfos(true, null);
}
}
private void onError() {
if (mDataHandler.isAlive()) {
mUiHandler.postDelayed(new Runnable() {
@Override
public void run() {
refreshUserDisplayname();
}
}, 1 * 1000);
}
}
@Override
public void onNetworkError(Exception e) {
onError();
}
@Override
public void onMatrixError(final MatrixError e) {
// cannot retrieve this value, jump to the next items
mIsDisplayNameRefreshed = true;
refreshUserInfos(true, null);
}
@Override
public void onUnexpectedError(final Exception e) {
// cannot retrieve this value, jump to the next items
mIsDisplayNameRefreshed = true;
refreshUserInfos(true, null);
}
});
}
/**
* Refresh the Third party identifiers i.e. the linked email to this account
*/
public void refreshThirdPartyIdentifiers() {
mDataHandler.getProfileRestClient().threePIDs(new SimpleApiCallback<List<ThirdPartyIdentifier>>() {
@Override
public void onSuccess(List<ThirdPartyIdentifier> identifiers) {
if (mDataHandler.isAlive()) {
// store
mDataHandler.getStore().setThirdPartyIdentifiers(identifiers);
buildIdentifiersLists();
mAre3PIdsLoaded = true;
// jump to the next items
refreshUserInfos(true, null);
}
}
private void onError() {
if (mDataHandler.isAlive()) {
mUiHandler.postDelayed(new Runnable() {
@Override
public void run() {
refreshThirdPartyIdentifiers();
}
}, 1 * 1000);
}
}
@Override
public void onNetworkError(Exception e) {
onError();
}
@Override
public void onMatrixError(final MatrixError e) {
// cannot retrieve this value, jump to the next items
mAre3PIdsLoaded = true;
refreshUserInfos(true, null);
}
@Override
public void onUnexpectedError(final Exception e) {
// cannot retrieve this value, jump to the next items
mAre3PIdsLoaded = true;
refreshUserInfos(true, null);
}
});
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright 2015 OpenMarket Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.data;
import java.util.Map;
public class Pusher {
public String pushkey;
public Object kind;
public String profileTag;
public String appId;
public String appDisplayName;
public String deviceDisplayName;
public String lang;
public Map<String, String> data;
public Boolean append;
@Override
public java.lang.String toString() {
return "Pusher : \n\tappDisplayName " + appDisplayName + "\n\tdeviceDisplayName " + deviceDisplayName + "\n\tpushkey " + pushkey;
}
}

View file

@ -0,0 +1,83 @@
/*
* Copyright 2014 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.data;
import android.support.annotation.Nullable;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import java.util.Map;
import java.util.Set;
/**
* Class representing private data that the user has defined for a room.
*/
public class RoomAccountData implements java.io.Serializable {
private static final long serialVersionUID = -8406116277864521120L;
// The tags the user defined for this room.
// The key is the tag name. The value, the associated MXRoomTag object.
private Map<String, RoomTag> tags = null;
/**
* Process an event that modifies room account data (like m.tag event).
*
* @param event an event
*/
public void handleTagEvent(Event event) {
if (event.getType().equals(Event.EVENT_TYPE_TAGS)) {
tags = RoomTag.roomTagsWithTagEvent(event);
}
}
/**
* Provide a RoomTag for a key.
*
* @param key the key.
* @return the roomTag if it is found else null
*/
@Nullable
public RoomTag roomTag(String key) {
if ((null != tags) && tags.containsKey(key)) {
return tags.get(key);
}
return null;
}
/**
* @return true if some tags are defined
*/
public boolean hasTags() {
return (null != tags) && (tags.size() > 0);
}
/**
* @return the list of keys, or null if no tag
*/
@Nullable
public Set<String> getKeys() {
if (hasTags()) {
return tags.keySet();
} else {
return null;
}
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright 2014 OpenMarket Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.data;
import java.util.Map;
/**
* Class representing the email invitation parameters
*/
public class RoomEmailInvitation {
// the email invitation parameters
// earch parameter can be null
public String email;
public String signUrl;
public String roomName;
public String roomAvatarUrl;
public String inviterName;
public String guestAccessToken;
public String guestUserId;
// the constructor
public RoomEmailInvitation(Map<String, String> parameters) {
if (null != parameters) {
email = parameters.get("email");
signUrl = parameters.get("signurl");
roomName = parameters.get("room_name");
roomAvatarUrl = parameters.get("room_avatar_url");
inviterName = parameters.get("inviter_name");
guestAccessToken = parameters.get("guestAccessToken");
guestUserId = parameters.get("guest_user_id");
}
}
}

View file

@ -0,0 +1,901 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.data;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Pair;
import android.webkit.MimeTypeMap;
import im.vector.matrix.android.internal.legacy.listeners.IMXMediaUploadListener;
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import im.vector.matrix.android.internal.legacy.rest.model.message.Message;
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
import im.vector.matrix.android.internal.legacy.util.Log;
import im.vector.matrix.android.internal.legacy.util.ResourceUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* RoomMediaMessage encapsulates the media information to be sent.
*/
public class RoomMediaMessage implements Parcelable {
private static final String LOG_TAG = RoomMediaMessage.class.getSimpleName();
private static final Uri mDummyUri = Uri.parse("http://www.matrixdummy.org");
/**
* Interface to monitor event creation.
*/
public interface EventCreationListener {
/**
* The dedicated event has been created and added to the events list.
*
* @param roomMediaMessage the room media message.
*/
void onEventCreated(RoomMediaMessage roomMediaMessage);
/**
* The event creation failed.
*
* @param roomMediaMessage the room media message.
* @param errorMessage the failure reason
*/
void onEventCreationFailed(RoomMediaMessage roomMediaMessage, String errorMessage);
/**
* The media encryption failed.
*
* @param roomMediaMessage the room media message.
*/
void onEncryptionFailed(RoomMediaMessage roomMediaMessage);
}
// the item is defined either from an uri
private Uri mUri;
private String mMimeType;
// the message to send
private Event mEvent;
// or a clipData Item
private ClipData.Item mClipDataItem;
// the filename
private String mFileName;
// Message.MSGTYPE_XX value
private String mMessageType;
// The replyTo event
@Nullable
private Event mReplyToEvent;
// thumbnail size
private Pair<Integer, Integer> mThumbnailSize = new Pair<>(100, 100);
// upload media upload listener
private transient IMXMediaUploadListener mMediaUploadListener;
// event sending callback
private transient ApiCallback<Void> mEventSendingCallback;
// event creation listener
private transient EventCreationListener mEventCreationListener;
/**
* Constructor from a ClipData.Item.
* It might be used by a third party medias selection.
*
* @param clipDataItem the data item
* @param mimeType the mime type
*/
public RoomMediaMessage(ClipData.Item clipDataItem, String mimeType) {
mClipDataItem = clipDataItem;
mMimeType = mimeType;
}
/**
* Constructor for a text message.
*
* @param text the text
* @param htmlText the HTML text
* @param format the formatted text format
*/
public RoomMediaMessage(CharSequence text, String htmlText, String format) {
mClipDataItem = new ClipData.Item(text, htmlText);
mMimeType = (null == htmlText) ? ClipDescription.MIMETYPE_TEXT_PLAIN : format;
}
/**
* Constructor from a media Uri/
*
* @param uri the media uri
*/
public RoomMediaMessage(Uri uri) {
this(uri, null);
}
/**
* Constructor from a media Uri/
*
* @param uri the media uri
* @param filename the media file name
*/
public RoomMediaMessage(Uri uri, String filename) {
mUri = uri;
mFileName = filename;
}
/**
* Constructor from an event.
*
* @param event the event
*/
public RoomMediaMessage(Event event) {
setEvent(event);
Message message = JsonUtils.toMessage(event.getContent());
if (null != message) {
setMessageType(message.msgtype);
}
}
/**
* Constructor from a parcel
*
* @param source the parcel
*/
private RoomMediaMessage(Parcel source) {
mUri = unformatNullUri((Uri) source.readParcelable(Uri.class.getClassLoader()));
mMimeType = unformatNullString(source.readString());
CharSequence clipDataItemText = unformatNullString(source.readString());
String clipDataItemHtml = unformatNullString(source.readString());
Uri clipDataItemUri = unformatNullUri((Uri) source.readParcelable(Uri.class.getClassLoader()));
if (!TextUtils.isEmpty(clipDataItemText) || !TextUtils.isEmpty(clipDataItemHtml) || (null != clipDataItemUri)) {
mClipDataItem = new ClipData.Item(clipDataItemText, clipDataItemHtml, null, clipDataItemUri);
}
mFileName = unformatNullString(source.readString());
}
@Override
public java.lang.String toString() {
String description = "";
description += "mUri " + mUri;
description += " -- mMimeType " + mMimeType;
description += " -- mEvent " + mEvent;
description += " -- mClipDataItem " + mClipDataItem;
description += " -- mFileName " + mFileName;
description += " -- mMessageType " + mMessageType;
description += " -- mThumbnailSize " + mThumbnailSize;
return description;
}
//==============================================================================================================
// Parcelable
//==============================================================================================================
/**
* Unformat parcelled String
*
* @param string the string to unformat
* @return the unformatted string
*/
private static String unformatNullString(final String string) {
if (TextUtils.isEmpty(string)) {
return null;
}
return string;
}
/**
* Convert null uri to a dummy one
*
* @param uri the uri to unformat
* @return the unformatted
*/
private static Uri unformatNullUri(final Uri uri) {
if ((null == uri) || mDummyUri.equals(uri)) {
return null;
}
return uri;
}
@Override
public int describeContents() {
return 0;
}
/**
* Convert null string to ""
*
* @param string the string to format
* @return the formatted string
*/
private static String formatNullString(final String string) {
if (TextUtils.isEmpty(string)) {
return "";
}
return string;
}
private static String formatNullString(final CharSequence charSequence) {
if (TextUtils.isEmpty(charSequence)) {
return "";
}
return charSequence.toString();
}
/**
* Convert null uri to a dummy one
*
* @param uri the uri to format
* @return the formatted
*/
private static Uri formatNullUri(final Uri uri) {
if (null == uri) {
return mDummyUri;
}
return uri;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeParcelable(formatNullUri(mUri), 0);
dest.writeString(formatNullString(mMimeType));
if (null == mClipDataItem) {
dest.writeString("");
dest.writeString("");
dest.writeParcelable(formatNullUri(null), 0);
} else {
dest.writeString(formatNullString(mClipDataItem.getText()));
dest.writeString(formatNullString(mClipDataItem.getHtmlText()));
dest.writeParcelable(formatNullUri(mClipDataItem.getUri()), 0);
}
dest.writeString(formatNullString(mFileName));
}
// Creator
public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
public RoomMediaMessage createFromParcel(Parcel in) {
return new RoomMediaMessage(in);
}
public RoomMediaMessage[] newArray(int size) {
return new RoomMediaMessage[size];
}
};
//==============================================================================================================
// Setters / getters
//==============================================================================================================
/**
* Set the message type.
*
* @param messageType the message type.
*/
public void setMessageType(String messageType) {
mMessageType = messageType;
}
/**
* @return the message type.
*/
public String getMessageType() {
return mMessageType;
}
/**
* Set the replyTo event.
*
* @param replyToEvent the event to reply to
*/
public void setReplyToEvent(@Nullable Event replyToEvent) {
mReplyToEvent = replyToEvent;
}
/**
* @return the replyTo event.
*/
@Nullable
public Event getReplyToEvent() {
return mReplyToEvent;
}
/**
* Update the inner event.
*
* @param event the new event.
*/
public void setEvent(Event event) {
mEvent = event;
}
/**
* @return the inner event objects
*/
public Event getEvent() {
return mEvent;
}
/**
* Update the thumbnail size.
*
* @param size the new thumbnail size.
*/
public void setThumbnailSize(Pair<Integer, Integer> size) {
mThumbnailSize = size;
}
/**
* @return the thumbnail size.
*/
public Pair<Integer, Integer> getThumbnailSize() {
return mThumbnailSize;
}
/**
* Update the media upload listener.
*
* @param mediaUploadListener the media upload listener.
*/
public void setMediaUploadListener(IMXMediaUploadListener mediaUploadListener) {
mMediaUploadListener = mediaUploadListener;
}
/**
* @return the media upload listener.
*/
public IMXMediaUploadListener getMediaUploadListener() {
return mMediaUploadListener;
}
/**
* Update the event sending callback.
*
* @param callback the callback
*/
public void setEventSendingCallback(ApiCallback<Void> callback) {
mEventSendingCallback = callback;
}
/**
* @return the event sending callback.
*/
public ApiCallback<Void> getSendingCallback() {
return mEventSendingCallback;
}
/**
* Update the listener
*
* @param eventCreationListener the new listener
*/
public void setEventCreationListener(EventCreationListener eventCreationListener) {
mEventCreationListener = eventCreationListener;
}
/**
* @return the listener.
*/
public EventCreationListener getEventCreationListener() {
return mEventCreationListener;
}
/**
* Retrieve the raw text contained in this Item.
*
* @return the raw text
*/
public CharSequence getText() {
if (null != mClipDataItem) {
return mClipDataItem.getText();
}
return null;
}
/**
* Retrieve the raw HTML text contained in this Item.
*
* @return the raw HTML text
*/
public String getHtmlText() {
if (null != mClipDataItem) {
return mClipDataItem.getHtmlText();
}
return null;
}
/**
* Retrieve the Intent contained in this Item.
*
* @return the intent
*/
public Intent getIntent() {
if (null != mClipDataItem) {
return mClipDataItem.getIntent();
}
return null;
}
/**
* Retrieve the URI contained in this Item.
*
* @return the Uri
*/
public Uri getUri() {
if (null != mUri) {
return mUri;
} else if (null != mClipDataItem) {
return mClipDataItem.getUri();
}
return null;
}
/**
* Returns the mimetype.
*
* @param context the context
* @return the mimetype
*/
public String getMimeType(Context context) {
if ((null == mMimeType) && (null != getUri())) {
try {
Uri uri = getUri();
mMimeType = context.getContentResolver().getType(uri);
// try to find the mimetype from the filename
if (null == mMimeType) {
String extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString().toLowerCase());
if (extension != null) {
mMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
}
if (null != mMimeType) {
// the mimetype is sometimes in uppercase.
mMimeType = mMimeType.toLowerCase();
}
} catch (Exception e) {
Log.e(LOG_TAG, "Failed to open resource input stream", e);
}
}
return mMimeType;
}
/**
* Gets the MINI_KIND image thumbnail.
*
* @param context the context
* @return the MINI_KIND thumbnail it it exists
*/
public Bitmap getMiniKindImageThumbnail(Context context) {
return getImageThumbnail(context, MediaStore.Images.Thumbnails.MINI_KIND);
}
/**
* Gets the FULL_SCREEN image thumbnail.
*
* @param context the context
* @return the FULL_SCREEN thumbnail it it exists
*/
public Bitmap getFullScreenImageKindThumbnail(Context context) {
return getImageThumbnail(context, MediaStore.Images.Thumbnails.FULL_SCREEN_KIND);
}
/**
* Gets the image thumbnail.
*
* @param context the context.
* @param kind the thumbnail kind.
* @return the thumbnail.
*/
private Bitmap getImageThumbnail(Context context, int kind) {
// sanity check
if ((null == getMimeType(context)) || !getMimeType(context).startsWith("image/")) {
return null;
}
Bitmap thumbnailBitmap = null;
try {
ContentResolver resolver = context.getContentResolver();
List uriPath = getUri().getPathSegments();
Long imageId;
String lastSegment = (String) uriPath.get(uriPath.size() - 1);
// > Kitkat
if (lastSegment.startsWith("image:")) {
lastSegment = lastSegment.substring("image:".length());
}
try {
imageId = Long.parseLong(lastSegment);
} catch (Exception e) {
imageId = null;
}
if (null != imageId) {
thumbnailBitmap = MediaStore.Images.Thumbnails.getThumbnail(resolver, imageId, kind, null);
}
} catch (Exception e) {
Log.e(LOG_TAG, "MediaStore.Images.Thumbnails.getThumbnail " + e.getMessage(), e);
}
return thumbnailBitmap;
}
/**
* @param context the context
* @return the filename
*/
public String getFileName(Context context) {
if ((null == mFileName) && (null != getUri())) {
Uri mediaUri = getUri();
if (null != mediaUri) {
try {
if (mediaUri.toString().startsWith("content://")) {
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(mediaUri, null, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
mFileName = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
}
} catch (Exception e) {
Log.e(LOG_TAG, "cursor.getString " + e.getMessage(), e);
} finally {
if (null != cursor) {
cursor.close();
}
}
if (TextUtils.isEmpty(mFileName)) {
List uriPath = mediaUri.getPathSegments();
mFileName = (String) uriPath.get(uriPath.size() - 1);
}
} else if (mediaUri.toString().startsWith("file://")) {
mFileName = mediaUri.getLastPathSegment();
}
} catch (Exception e) {
mFileName = null;
}
}
}
return mFileName;
}
/**
* Save a media into a dedicated folder
*
* @param context the context
* @param folder the folder.
*/
public void saveMedia(Context context, File folder) {
mFileName = null;
Uri mediaUri = getUri();
if (null != mediaUri) {
try {
ResourceUtils.Resource resource = ResourceUtils.openResource(context, mediaUri, getMimeType(context));
if (null == resource) {
Log.e(LOG_TAG, "## saveMedia : Fail to retrieve the resource " + mediaUri);
} else {
mUri = saveFile(folder, resource.mContentStream, getFileName(context), resource.mMimeType);
resource.mContentStream.close();
}
} catch (Exception e) {
Log.e(LOG_TAG, "## saveMedia : failed " + e.getMessage(), e);
}
}
}
/**
* Save a file in a dedicated directory.
* The filename is optional.
*
* @param folder the destination folder
* @param stream the file stream
* @param defaultFileName the filename, null to generate a new one
* @param mimeType the file mimetype.
* @return the file uri
*/
private static Uri saveFile(File folder, InputStream stream, String defaultFileName, String mimeType) {
String filename = defaultFileName;
if (null == filename) {
filename = "file" + System.currentTimeMillis();
if (null != mimeType) {
String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
if (null != extension) {
filename += "." + extension;
}
}
}
Uri fileUri = null;
try {
File file = new File(folder, filename);
// if the file exits, delete it
if (file.exists()) {
file.delete();
}
FileOutputStream fos = new FileOutputStream(file.getPath());
try {
byte[] buf = new byte[1024 * 32];
int len;
while ((len = stream.read(buf)) != -1) {
fos.write(buf, 0, len);
}
} catch (Exception e) {
Log.e(LOG_TAG, "## saveFile failed " + e.getMessage(), e);
}
fos.flush();
fos.close();
stream.close();
fileUri = Uri.fromFile(file);
} catch (Exception e) {
Log.e(LOG_TAG, "## saveFile failed " + e.getMessage(), e);
}
return fileUri;
}
//==============================================================================================================
// Dispatchers
//==============================================================================================================
/**
* Dispatch onEventCreated.
*/
void onEventCreated() {
if (null != getEventCreationListener()) {
try {
getEventCreationListener().onEventCreated(this);
} catch (Exception e) {
Log.e(LOG_TAG, "## onEventCreated() failed : " + e.getMessage(), e);
}
}
// clear the listener
mEventCreationListener = null;
}
/**
* Dispatch onEventCreationFailed.
*/
void onEventCreationFailed(String errorMessage) {
if (null != getEventCreationListener()) {
try {
getEventCreationListener().onEventCreationFailed(this, errorMessage);
} catch (Exception e) {
Log.e(LOG_TAG, "## onEventCreationFailed() failed : " + e.getMessage(), e);
}
}
// clear the listeners
mMediaUploadListener = null;
mEventSendingCallback = null;
mEventCreationListener = null;
}
/**
* Dispatch onEncryptionFailed.
*/
void onEncryptionFailed() {
if (null != getEventCreationListener()) {
try {
getEventCreationListener().onEncryptionFailed(this);
} catch (Exception e) {
Log.e(LOG_TAG, "## onEncryptionFailed() failed : " + e.getMessage(), e);
}
}
// clear the listeners
mMediaUploadListener = null;
mEventSendingCallback = null;
mEventCreationListener = null;
}
//==============================================================================================================
// Retrieve RoomMediaMessages from intents.
//==============================================================================================================
/**
* List the item provided in an intent.
*
* @param intent the intent.
* @return the RoomMediaMessages list
*/
public static List<RoomMediaMessage> listRoomMediaMessages(Intent intent) {
return listRoomMediaMessages(intent, null);
}
/**
* List the item provided in an intent.
*
* @param intent the intent.
* @param loader the class loader.
* @return the room list
*/
public static List<RoomMediaMessage> listRoomMediaMessages(Intent intent, ClassLoader loader) {
List<RoomMediaMessage> roomMediaMessages = new ArrayList<>();
if (null != intent) {
// chrome adds many items when sharing an web page link
// so, test first the type
if (TextUtils.equals(intent.getType(), ClipDescription.MIMETYPE_TEXT_PLAIN)) {
String message = intent.getStringExtra(Intent.EXTRA_TEXT);
if (null == message) {
CharSequence sequence = intent.getCharSequenceExtra(Intent.EXTRA_TEXT);
if (null != sequence) {
message = sequence.toString();
}
}
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
if (!TextUtils.isEmpty(subject)) {
if (TextUtils.isEmpty(message)) {
message = subject;
} else if (android.util.Patterns.WEB_URL.matcher(message).matches()) {
message = subject + "\n" + message;
}
}
if (!TextUtils.isEmpty(message)) {
roomMediaMessages.add(new RoomMediaMessage(message, null, intent.getType()));
return roomMediaMessages;
}
}
ClipData clipData = null;
List<String> mimetypes = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
clipData = intent.getClipData();
}
// multiple data
if (null != clipData) {
if (null != clipData.getDescription()) {
if (0 != clipData.getDescription().getMimeTypeCount()) {
mimetypes = new ArrayList<>();
for (int i = 0; i < clipData.getDescription().getMimeTypeCount(); i++) {
mimetypes.add(clipData.getDescription().getMimeType(i));
}
// if the filter is "accept anything" the mimetype does not make sense
if (1 == mimetypes.size()) {
if (mimetypes.get(0).endsWith("/*")) {
mimetypes = null;
}
}
}
}
int count = clipData.getItemCount();
for (int i = 0; i < count; i++) {
ClipData.Item item = clipData.getItemAt(i);
String mimetype = null;
if (null != mimetypes) {
if (i < mimetypes.size()) {
mimetype = mimetypes.get(i);
} else {
mimetype = mimetypes.get(0);
}
// uris list is not a valid mimetype
if (TextUtils.equals(mimetype, ClipDescription.MIMETYPE_TEXT_URILIST)) {
mimetype = null;
}
}
roomMediaMessages.add(new RoomMediaMessage(item, mimetype));
}
} else if (null != intent.getData()) {
roomMediaMessages.add(new RoomMediaMessage(intent.getData()));
} else {
Bundle bundle = intent.getExtras();
if (null != bundle) {
// provide a custom loader
bundle.setClassLoader(RoomMediaMessage.class.getClassLoader());
// list the Uris list
if (bundle.containsKey(Intent.EXTRA_STREAM)) {
try {
Object streamUri = bundle.get(Intent.EXTRA_STREAM);
if (streamUri instanceof Uri) {
roomMediaMessages.add(new RoomMediaMessage((Uri) streamUri));
} else if (streamUri instanceof List) {
List<Object> streams = (List<Object>) streamUri;
for (Object object : streams) {
if (object instanceof Uri) {
roomMediaMessages.add(new RoomMediaMessage((Uri) object));
} else if (object instanceof RoomMediaMessage) {
roomMediaMessages.add((RoomMediaMessage) object);
}
}
}
} catch (Exception e) {
Log.e(LOG_TAG, "fail to extract the extra stream", e);
}
}
}
}
}
return roomMediaMessages;
}
}

View file

@ -0,0 +1,983 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.data;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.ThumbnailUtils;
import android.net.Uri;
import android.os.HandlerThread;
import android.os.Looper;
import android.provider.MediaStore;
import android.text.Html;
import android.text.TextUtils;
import android.util.Pair;
import im.vector.matrix.android.R;
import im.vector.matrix.android.internal.legacy.MXDataHandler;
import im.vector.matrix.android.internal.legacy.crypto.MXEncryptedAttachments;
import im.vector.matrix.android.internal.legacy.db.MXMediasCache;
import im.vector.matrix.android.internal.legacy.listeners.MXMediaUploadListener;
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
import im.vector.matrix.android.internal.legacy.rest.model.message.AudioMessage;
import im.vector.matrix.android.internal.legacy.rest.model.message.FileMessage;
import im.vector.matrix.android.internal.legacy.rest.model.message.ImageMessage;
import im.vector.matrix.android.internal.legacy.rest.model.message.MediaMessage;
import im.vector.matrix.android.internal.legacy.rest.model.message.Message;
import im.vector.matrix.android.internal.legacy.rest.model.message.RelatesTo;
import im.vector.matrix.android.internal.legacy.rest.model.message.VideoMessage;
import im.vector.matrix.android.internal.legacy.util.ImageUtils;
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
import im.vector.matrix.android.internal.legacy.util.Log;
import im.vector.matrix.android.internal.legacy.util.PermalinkUtils;
import im.vector.matrix.android.internal.legacy.util.ResourceUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* Room helper to send media messages in the right order.
*/
class RoomMediaMessagesSender {
private static final String LOG_TAG = RoomMediaMessagesSender.class.getSimpleName();
// pending events list
private final List<RoomMediaMessage> mPendingRoomMediaMessages = new ArrayList<>();
// linked room
private final Room mRoom;
// data handler
private final MXDataHandler mDataHandler;
// linked context
private final Context mContext;
// the sending item
private RoomMediaMessage mSendingRoomMediaMessage;
// UI thread
private static android.os.Handler mUiHandler = null;
// events creation threads
private static android.os.Handler mEventHandler = null;
// encoding creation threads
private static android.os.Handler mEncodingHandler = null;
/**
* Constructor
*
* @param context the context
* @param dataHandler the dataHanlder
* @param room the room
*/
RoomMediaMessagesSender(Context context, MXDataHandler dataHandler, Room room) {
mRoom = room;
mContext = context.getApplicationContext();
mDataHandler = dataHandler;
if (null == mUiHandler) {
mUiHandler = new android.os.Handler(Looper.getMainLooper());
HandlerThread eventHandlerThread = new HandlerThread("RoomDataItemsSender_event", Thread.MIN_PRIORITY);
eventHandlerThread.start();
mEventHandler = new android.os.Handler(eventHandlerThread.getLooper());
HandlerThread encodingHandlerThread = new HandlerThread("RoomDataItemsSender_encoding", Thread.MIN_PRIORITY);
encodingHandlerThread.start();
mEncodingHandler = new android.os.Handler(encodingHandlerThread.getLooper());
}
}
/**
* Send a new media message to the room
*
* @param roomMediaMessage the message to send
*/
void send(final RoomMediaMessage roomMediaMessage) {
mEventHandler.post(new Runnable() {
@Override
public void run() {
if (null == roomMediaMessage.getEvent()) {
Message message;
String mimeType = roomMediaMessage.getMimeType(mContext);
// avoid null case
if (null == mimeType) {
mimeType = "";
}
if (null == roomMediaMessage.getUri()) {
message = buildTextMessage(roomMediaMessage);
} else if (mimeType.startsWith("image/")) {
message = buildImageMessage(roomMediaMessage);
} else if (mimeType.startsWith("video/")) {
message = buildVideoMessage(roomMediaMessage);
} else {
message = buildFileMessage(roomMediaMessage);
}
if (null == message) {
Log.e(LOG_TAG, "## send " + roomMediaMessage + " not supported");
mUiHandler.post(new Runnable() {
@Override
public void run() {
roomMediaMessage.onEventCreationFailed("not supported " + roomMediaMessage);
}
});
return;
}
roomMediaMessage.setMessageType(message.msgtype);
if (roomMediaMessage.getReplyToEvent() != null) {
// Note: it is placed here, but may be moved to the outer event during the encryption of the content
message.relatesTo = new RelatesTo();
message.relatesTo.dict = new HashMap<>();
message.relatesTo.dict.put("event_id", roomMediaMessage.getReplyToEvent().eventId);
}
Event event = new Event(message, mDataHandler.getUserId(), mRoom.getRoomId());
roomMediaMessage.setEvent(event);
}
mDataHandler.updateEventState(roomMediaMessage.getEvent(), Event.SentState.UNSENT);
mRoom.storeOutgoingEvent(roomMediaMessage.getEvent());
mDataHandler.getStore().commit();
mUiHandler.post(new Runnable() {
@Override
public void run() {
roomMediaMessage.onEventCreated();
}
});
synchronized (LOG_TAG) {
if (!mPendingRoomMediaMessages.contains(roomMediaMessage)) {
mPendingRoomMediaMessages.add(roomMediaMessage);
}
}
mUiHandler.post(new Runnable() {
@Override
public void run() {
// send the item
sendNext();
}
});
}
});
}
/**
* Skip the sending media item.
*/
private void skip() {
synchronized (LOG_TAG) {
mSendingRoomMediaMessage = null;
}
sendNext();
}
/**
* Send the next pending item
*/
private void sendNext() {
RoomMediaMessage roomMediaMessage;
synchronized (LOG_TAG) {
// please wait
if (null != mSendingRoomMediaMessage) {
return;
}
if (!mPendingRoomMediaMessages.isEmpty()) {
mSendingRoomMediaMessage = mPendingRoomMediaMessages.get(0);
mPendingRoomMediaMessages.remove(0);
} else {
// nothing to do
return;
}
roomMediaMessage = mSendingRoomMediaMessage;
}
// upload the medias first
if (uploadMedias(roomMediaMessage)) {
return;
}
// send the event
sendEvent(roomMediaMessage.getEvent());
}
/**
* Send the event after uploading the medias
*
* @param event the event to send
*/
private void sendEvent(final Event event) {
mUiHandler.post(new Runnable() {
@Override
public void run() {
// nothing more to upload
mRoom.sendEvent(event, new ApiCallback<Void>() {
private ApiCallback<Void> getCallback() {
ApiCallback<Void> callback;
synchronized (LOG_TAG) {
callback = mSendingRoomMediaMessage.getSendingCallback();
mSendingRoomMediaMessage.setEventSendingCallback(null);
mSendingRoomMediaMessage = null;
}
return callback;
}
@Override
public void onSuccess(Void info) {
ApiCallback<Void> callback = getCallback();
if (null != callback) {
try {
callback.onSuccess(null);
} catch (Exception e) {
Log.e(LOG_TAG, "## sendNext() failed " + e.getMessage(), e);
}
}
sendNext();
}
@Override
public void onNetworkError(Exception e) {
ApiCallback<Void> callback = getCallback();
if (null != callback) {
try {
callback.onNetworkError(e);
} catch (Exception e2) {
Log.e(LOG_TAG, "## sendNext() failed " + e2.getMessage(), e2);
}
}
sendNext();
}
@Override
public void onMatrixError(MatrixError e) {
ApiCallback<Void> callback = getCallback();
if (null != callback) {
try {
callback.onMatrixError(e);
} catch (Exception e2) {
Log.e(LOG_TAG, "## sendNext() failed " + e2.getMessage(), e2);
}
}
sendNext();
}
@Override
public void onUnexpectedError(Exception e) {
ApiCallback<Void> callback = getCallback();
if (null != callback) {
try {
callback.onUnexpectedError(e);
} catch (Exception e2) {
Log.e(LOG_TAG, "## sendNext() failed " + e2.getMessage(), e2);
}
}
sendNext();
}
});
}
});
}
//==============================================================================================================
// Messages builder methods.
//==============================================================================================================
/**
* Build a text message from a RoomMediaMessage.
*
* @param roomMediaMessage the RoomMediaMessage.
* @return the message
*/
private Message buildTextMessage(RoomMediaMessage roomMediaMessage) {
CharSequence sequence = roomMediaMessage.getText();
String htmlText = roomMediaMessage.getHtmlText();
String text = null;
if (null == sequence) {
if (null != htmlText) {
text = Html.fromHtml(htmlText).toString();
}
} else {
text = sequence.toString();
}
// a text message cannot be null
if (TextUtils.isEmpty(text) && !TextUtils.equals(roomMediaMessage.getMessageType(), Message.MSGTYPE_EMOTE)) {
return null;
}
Message message = new Message();
message.msgtype = (null == roomMediaMessage.getMessageType()) ? Message.MSGTYPE_TEXT : roomMediaMessage.getMessageType();
message.body = text;
// an emote can have an empty body
if (null == message.body) {
message.body = "";
}
if (!TextUtils.isEmpty(htmlText)) {
message.formatted_body = htmlText;
message.format = Message.FORMAT_MATRIX_HTML;
}
// Deals with in reply to event
Event replyToEvent = roomMediaMessage.getReplyToEvent();
if (replyToEvent != null) {
// Cf. https://docs.google.com/document/d/1BPd4lBrooZrWe_3s_lHw_e-Dydvc7bXbm02_sV2k6Sc
String msgType = JsonUtils.getMessageMsgType(replyToEvent.getContentAsJsonObject());
// Build body and formatted body, depending of the `msgtype` of the event the user is replying to
if (msgType != null) {
// Compute the content of the event user is replying to
String replyToBody;
String replyToFormattedBody;
boolean replyToEventIsAlreadyAReply = false;
switch (msgType) {
case Message.MSGTYPE_TEXT:
case Message.MSGTYPE_NOTICE:
case Message.MSGTYPE_EMOTE:
Message messageToReplyTo = JsonUtils.toMessage(replyToEvent.getContentAsJsonObject());
replyToBody = messageToReplyTo.body;
if (TextUtils.isEmpty(messageToReplyTo.formatted_body)) {
replyToFormattedBody = messageToReplyTo.body;
} else {
replyToFormattedBody = messageToReplyTo.formatted_body;
}
replyToEventIsAlreadyAReply = messageToReplyTo.relatesTo != null
&& messageToReplyTo.relatesTo.dict != null
&& !TextUtils.isEmpty(messageToReplyTo.relatesTo.dict.get("event_id"));
break;
case Message.MSGTYPE_IMAGE:
replyToBody = mContext.getString(R.string.reply_to_an_image);
replyToFormattedBody = replyToBody;
break;
case Message.MSGTYPE_VIDEO:
replyToBody = mContext.getString(R.string.reply_to_a_video);
replyToFormattedBody = replyToBody;
break;
case Message.MSGTYPE_AUDIO:
replyToBody = mContext.getString(R.string.reply_to_an_audio_file);
replyToFormattedBody = replyToBody;
break;
case Message.MSGTYPE_FILE:
replyToBody = mContext.getString(R.string.reply_to_a_file);
replyToFormattedBody = replyToBody;
break;
default:
// Other msg types are not supported yet
Log.w(LOG_TAG, "Reply to: unsupported msgtype: " + msgType);
replyToBody = null;
replyToFormattedBody = null;
break;
}
if (replyToBody != null) {
String replyContent;
if (TextUtils.isEmpty(message.formatted_body)) {
replyContent = message.body;
} else {
replyContent = message.formatted_body;
}
message.body = includeReplyToToBody(replyToEvent,
replyToBody,
replyToEventIsAlreadyAReply,
message.body,
msgType.equals(Message.MSGTYPE_EMOTE));
message.formatted_body = includeReplyToToFormattedBody(replyToEvent,
replyToFormattedBody,
replyToEventIsAlreadyAReply,
replyContent,
msgType.equals(Message.MSGTYPE_EMOTE));
// Note: we need to force the format to Message.FORMAT_MATRIX_HTML
message.format = Message.FORMAT_MATRIX_HTML;
} else {
Log.e(LOG_TAG, "Unsupported 'msgtype': " + msgType + ". Consider calling Room.canReplyTo(Event)");
// Ensure there will not be "m.relates_to" data in the sent event
roomMediaMessage.setReplyToEvent(null);
}
} else {
Log.e(LOG_TAG, "Null 'msgtype'. Consider calling Room.canReplyTo(Event)");
// Ensure there will not be "m.relates_to" data in the sent event
roomMediaMessage.setReplyToEvent(null);
}
}
return message;
}
private String includeReplyToToBody(Event replyToEvent,
String replyToBody,
boolean stripPreviousReplyTo,
String messageBody,
boolean isEmote) {
int firstLineIndex = 0;
String[] lines = replyToBody.split("\n");
if (stripPreviousReplyTo) {
// Strip replyToBody from previous reply to
// Strip line starting with "> "
while (firstLineIndex < lines.length && lines[firstLineIndex].startsWith("> ")) {
firstLineIndex++;
}
// Strip empty line after
if (firstLineIndex < lines.length && lines[firstLineIndex].isEmpty()) {
firstLineIndex++;
}
}
StringBuilder ret = new StringBuilder();
if (firstLineIndex < lines.length) {
// Add <${mxid}> to the first line
if (isEmote) {
lines[firstLineIndex] = "* <" + replyToEvent.sender + "> " + lines[firstLineIndex];
} else {
lines[firstLineIndex] = "<" + replyToEvent.sender + "> " + lines[firstLineIndex];
}
for (int i = firstLineIndex; i < lines.length; i++) {
ret.append("> ")
.append(lines[i])
.append("\n");
}
}
ret.append("\n")
.append(messageBody);
return ret.toString();
}
private String includeReplyToToFormattedBody(Event replyToEvent,
String replyToFormattedBody,
boolean stripPreviousReplyTo,
String messageFormattedBody,
boolean isEmote) {
if (stripPreviousReplyTo) {
// Strip replyToFormattedBody from previous reply to
replyToFormattedBody = replyToFormattedBody.replaceAll("^<mx-reply>.*</mx-reply>", "");
}
StringBuilder ret = new StringBuilder("<mx-reply><blockquote><a href=\"")
// ${evLink}
.append(PermalinkUtils.createPermalink(replyToEvent))
.append("\">")
// "In reply to"
.append(mContext.getString(R.string.message_reply_to_prefix))
.append("</a> ");
if (isEmote) {
ret.append("* ");
}
ret.append("<a href=\"")
// ${userLink}
.append(PermalinkUtils.createPermalink(replyToEvent.sender))
.append("\">")
// ${mxid}
.append(replyToEvent.sender)
.append("</a><br>")
.append(replyToFormattedBody)
.append("</blockquote></mx-reply>")
.append(messageFormattedBody);
return ret.toString();
}
/**
* Returns the thumbnail path of shot image.
*
* @param picturePath the image path
* @return the thumbnail image path.
*/
private static String getThumbnailPath(String picturePath) {
if (!TextUtils.isEmpty(picturePath) && picturePath.endsWith(".jpg")) {
return picturePath.replace(".jpg", "_thumb.jpg");
}
return null;
}
/**
* Retrieves the image thumbnail saved by the medias picker.
*
* @param sharedDataItem the sharedItem
* @return the thumbnail if it exits.
*/
private Bitmap getMediasPickerThumbnail(RoomMediaMessage sharedDataItem) {
Bitmap thumbnailBitmap = null;
try {
String thumbPath = getThumbnailPath(sharedDataItem.getUri().getPath());
if (null != thumbPath) {
File thumbFile = new File(thumbPath);
if (thumbFile.exists()) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
thumbnailBitmap = BitmapFactory.decodeFile(thumbPath, options);
}
}
} catch (Exception e) {
Log.e(LOG_TAG, "cannot restore the medias picker thumbnail " + e.getMessage(), e);
} catch (OutOfMemoryError oom) {
Log.e(LOG_TAG, "cannot restore the medias picker thumbnail oom", oom);
}
return thumbnailBitmap;
}
/**
* Retrieve the media Url.
*
* @param roomMediaMessage the room media message
* @return the media URL
*/
private String getMediaUrl(RoomMediaMessage roomMediaMessage) {
String mediaUrl = roomMediaMessage.getUri().toString();
if (!mediaUrl.startsWith("file:")) {
// save the content:// file in to the medias cache
String mimeType = roomMediaMessage.getMimeType(mContext);
ResourceUtils.Resource resource = ResourceUtils.openResource(mContext, roomMediaMessage.getUri(), mimeType);
// save the file in the filesystem
mediaUrl = mDataHandler.getMediasCache().saveMedia(resource.mContentStream, null, mimeType);
resource.close();
}
return mediaUrl;
}
/**
* Build an image message from a RoomMediaMessage.
*
* @param roomMediaMessage the roomMediaMessage
* @return the image message
*/
private Message buildImageMessage(RoomMediaMessage roomMediaMessage) {
try {
String mimeType = roomMediaMessage.getMimeType(mContext);
final MXMediasCache mediasCache = mDataHandler.getMediasCache();
String mediaUrl = getMediaUrl(roomMediaMessage);
// compute the thumbnail
Bitmap thumbnailBitmap = roomMediaMessage.getFullScreenImageKindThumbnail(mContext);
if (null == thumbnailBitmap) {
thumbnailBitmap = getMediasPickerThumbnail(roomMediaMessage);
}
if (null == thumbnailBitmap) {
Pair<Integer, Integer> thumbnailSize = roomMediaMessage.getThumbnailSize();
thumbnailBitmap = ResourceUtils.createThumbnailBitmap(mContext, roomMediaMessage.getUri(), thumbnailSize.first, thumbnailSize.second);
}
if (null == thumbnailBitmap) {
thumbnailBitmap = roomMediaMessage.getMiniKindImageThumbnail(mContext);
}
String thumbnailURL = null;
if (null != thumbnailBitmap) {
thumbnailURL = mediasCache.saveBitmap(thumbnailBitmap, null);
}
// get the exif rotation angle
final int rotationAngle = ImageUtils.getRotationAngleForBitmap(mContext, Uri.parse(mediaUrl));
if (0 != rotationAngle) {
// always apply the rotation to the image
ImageUtils.rotateImage(mContext, thumbnailURL, rotationAngle, mediasCache);
}
ImageMessage imageMessage = new ImageMessage();
imageMessage.url = mediaUrl;
imageMessage.body = roomMediaMessage.getFileName(mContext);
if (TextUtils.isEmpty(imageMessage.body)) {
imageMessage.body = "Image";
}
Uri imageUri = Uri.parse(mediaUrl);
if (null == imageMessage.info) {
Room.fillImageInfo(mContext, imageMessage, imageUri, mimeType);
}
if ((null != thumbnailURL) && (null != imageMessage.info) && (null == imageMessage.info.thumbnailInfo)) {
Uri thumbUri = Uri.parse(thumbnailURL);
Room.fillThumbnailInfo(mContext, imageMessage, thumbUri, "image/jpeg");
imageMessage.info.thumbnailUrl = thumbnailURL;
}
return imageMessage;
} catch (Exception e) {
Log.e(LOG_TAG, "## buildImageMessage() failed " + e.getMessage(), e);
}
return null;
}
/**
* Compute the video thumbnail
*
* @param videoUrl the video url
* @return the video thumbnail
*/
public String getVideoThumbnailUrl(final String videoUrl) {
String thumbUrl = null;
try {
Uri uri = Uri.parse(videoUrl);
Bitmap thumb = ThumbnailUtils.createVideoThumbnail(uri.getPath(), MediaStore.Images.Thumbnails.MINI_KIND);
thumbUrl = mDataHandler.getMediasCache().saveBitmap(thumb, null);
} catch (Exception e) {
Log.e(LOG_TAG, "## getVideoThumbnailUrl() failed with " + e.getMessage(), e);
}
return thumbUrl;
}
/**
* Build an video message from a RoomMediaMessage.
*
* @param roomMediaMessage the roomMediaMessage
* @return the video message
*/
private Message buildVideoMessage(RoomMediaMessage roomMediaMessage) {
try {
String mediaUrl = getMediaUrl(roomMediaMessage);
String thumbnailUrl = getVideoThumbnailUrl(mediaUrl);
if (null == thumbnailUrl) {
return buildFileMessage(roomMediaMessage);
}
VideoMessage videoMessage = new VideoMessage();
videoMessage.url = mediaUrl;
videoMessage.body = roomMediaMessage.getFileName(mContext);
Uri videoUri = Uri.parse(mediaUrl);
Uri thumbnailUri = (null != thumbnailUrl) ? Uri.parse(thumbnailUrl) : null;
Room.fillVideoInfo(mContext, videoMessage, videoUri, roomMediaMessage.getMimeType(mContext), thumbnailUri, "image/jpeg");
if (null == videoMessage.body) {
videoMessage.body = videoUri.getLastPathSegment();
}
return videoMessage;
} catch (Exception e) {
Log.e(LOG_TAG, "## buildVideoMessage() failed " + e.getMessage(), e);
}
return null;
}
/**
* Build an file message from a RoomMediaMessage.
*
* @param roomMediaMessage the roomMediaMessage
* @return the video message
*/
private Message buildFileMessage(RoomMediaMessage roomMediaMessage) {
try {
String mimeType = roomMediaMessage.getMimeType(mContext);
String mediaUrl = getMediaUrl(roomMediaMessage);
FileMessage fileMessage;
if (mimeType.startsWith("audio/")) {
fileMessage = new AudioMessage();
} else {
fileMessage = new FileMessage();
}
fileMessage.url = mediaUrl;
fileMessage.body = roomMediaMessage.getFileName(mContext);
Uri uri = Uri.parse(mediaUrl);
Room.fillFileInfo(mContext, fileMessage, uri, mimeType);
if (null == fileMessage.body) {
fileMessage.body = uri.getLastPathSegment();
}
return fileMessage;
} catch (Exception e) {
Log.e(LOG_TAG, "## buildFileMessage() failed " + e.getMessage(), e);
}
return null;
}
//==============================================================================================================
// Upload medias management
//==============================================================================================================
/**
* Upload the medias.
*
* @param roomMediaMessage the roomMediaMessage
* @return true if a media is uploaded
*/
private boolean uploadMedias(final RoomMediaMessage roomMediaMessage) {
final Event event = roomMediaMessage.getEvent();
final Message message = JsonUtils.toMessage(event.getContent());
if (!(message instanceof MediaMessage)) {
return false;
}
final MediaMessage mediaMessage = (MediaMessage) message;
final String url;
final String fMimeType;
if (mediaMessage.isThumbnailLocalContent()) {
url = mediaMessage.getThumbnailUrl();
fMimeType = "image/jpeg";
} else if (mediaMessage.isLocalContent()) {
url = mediaMessage.getUrl();
fMimeType = mediaMessage.getMimeType();
} else {
return false;
}
mEncodingHandler.post(new Runnable() {
@Override
public void run() {
final MXMediasCache mediasCache = mDataHandler.getMediasCache();
Uri uri = Uri.parse(url);
String mimeType = fMimeType;
final MXEncryptedAttachments.EncryptionResult encryptionResult;
final Uri encryptedUri;
InputStream stream;
String filename = null;
try {
stream = new FileInputStream(new File(uri.getPath()));
if (mRoom.isEncrypted() && mDataHandler.isCryptoEnabled() && (null != stream)) {
encryptionResult = MXEncryptedAttachments.encryptAttachment(stream, mimeType);
stream.close();
if (null != encryptionResult) {
mimeType = "application/octet-stream";
encryptedUri = Uri.parse(mediasCache.saveMedia(encryptionResult.mEncryptedStream, null, fMimeType));
File file = new File(encryptedUri.getPath());
stream = new FileInputStream(file);
} else {
skip();
mUiHandler.post(new Runnable() {
@Override
public void run() {
mDataHandler.updateEventState(roomMediaMessage.getEvent(), Event.SentState.UNDELIVERED);
mRoom.storeOutgoingEvent(roomMediaMessage.getEvent());
mDataHandler.getStore().commit();
roomMediaMessage.onEncryptionFailed();
}
});
return;
}
} else {
// Only pass filename string to server in non-encrypted rooms to prevent leaking filename
filename = mediaMessage.isThumbnailLocalContent() ? ("thumb" + message.body) : message.body;
encryptionResult = null;
encryptedUri = null;
}
} catch (Exception e) {
skip();
return;
}
mDataHandler.updateEventState(roomMediaMessage.getEvent(), Event.SentState.SENDING);
mediasCache.uploadContent(stream, filename, mimeType, url,
new MXMediaUploadListener() {
@Override
public void onUploadStart(final String uploadId) {
mUiHandler.post(new Runnable() {
@Override
public void run() {
if (null != roomMediaMessage.getMediaUploadListener()) {
roomMediaMessage.getMediaUploadListener().onUploadStart(uploadId);
}
}
});
}
@Override
public void onUploadCancel(final String uploadId) {
mUiHandler.post(new Runnable() {
@Override
public void run() {
mDataHandler.updateEventState(roomMediaMessage.getEvent(), Event.SentState.UNDELIVERED);
if (null != roomMediaMessage.getMediaUploadListener()) {
roomMediaMessage.getMediaUploadListener().onUploadCancel(uploadId);
roomMediaMessage.setMediaUploadListener(null);
roomMediaMessage.setEventSendingCallback(null);
}
skip();
}
});
}
@Override
public void onUploadError(final String uploadId, final int serverResponseCode, final String serverErrorMessage) {
mUiHandler.post(new Runnable() {
@Override
public void run() {
mDataHandler.updateEventState(roomMediaMessage.getEvent(), Event.SentState.UNDELIVERED);
if (null != roomMediaMessage.getMediaUploadListener()) {
roomMediaMessage.getMediaUploadListener().onUploadError(uploadId, serverResponseCode, serverErrorMessage);
roomMediaMessage.setMediaUploadListener(null);
roomMediaMessage.setEventSendingCallback(null);
}
skip();
}
});
}
@Override
public void onUploadComplete(final String uploadId, final String contentUri) {
mUiHandler.post(new Runnable() {
@Override
public void run() {
boolean isThumbnailUpload = mediaMessage.isThumbnailLocalContent();
if (isThumbnailUpload) {
mediaMessage.setThumbnailUrl(encryptionResult, contentUri);
if (null != encryptionResult) {
mediasCache.saveFileMediaForUrl(contentUri, encryptedUri.toString(), -1, -1, "image/jpeg");
try {
new File(Uri.parse(url).getPath()).delete();
} catch (Exception e) {
Log.e(LOG_TAG, "## cannot delete the uncompress media", e);
}
} else {
Pair<Integer, Integer> thumbnailSize = roomMediaMessage.getThumbnailSize();
mediasCache.saveFileMediaForUrl(contentUri, url, thumbnailSize.first, thumbnailSize.second, "image/jpeg");
}
// update the event content with the new message info
event.updateContent(JsonUtils.toJson(message));
// force to save the room events list
// https://github.com/vector-im/riot-android/issues/1390
mDataHandler.getStore().flushRoomEvents(mRoom.getRoomId());
// upload the media
uploadMedias(roomMediaMessage);
} else {
if (null != encryptedUri) {
// replace the thumbnail and the media contents by the computed one
mediasCache.saveFileMediaForUrl(contentUri, encryptedUri.toString(), mediaMessage.getMimeType());
try {
new File(Uri.parse(url).getPath()).delete();
} catch (Exception e) {
Log.e(LOG_TAG, "## cannot delete the uncompress media", e);
}
} else {
// replace the thumbnail and the media contents by the computed one
mediasCache.saveFileMediaForUrl(contentUri, url, mediaMessage.getMimeType());
}
mediaMessage.setUrl(encryptionResult, contentUri);
// update the event content with the new message info
event.updateContent(JsonUtils.toJson(message));
// force to save the room events list
// https://github.com/vector-im/riot-android/issues/1390
mDataHandler.getStore().flushRoomEvents(mRoom.getRoomId());
Log.d(LOG_TAG, "Uploaded to " + contentUri);
// send
sendEvent(event);
}
if (null != roomMediaMessage.getMediaUploadListener()) {
roomMediaMessage.getMediaUploadListener().onUploadComplete(uploadId, contentUri);
if (!isThumbnailUpload) {
roomMediaMessage.setMediaUploadListener(null);
}
}
}
});
}
});
}
});
return true;
}
}

View file

@ -0,0 +1,275 @@
/*
* Copyright 2016 OpenMarket Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.data;
import android.os.AsyncTask;
import android.os.Looper;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import im.vector.matrix.android.internal.legacy.MXSession;
import im.vector.matrix.android.internal.legacy.data.timeline.EventTimeline;
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
import im.vector.matrix.android.internal.legacy.rest.model.publicroom.PublicRoom;
import im.vector.matrix.android.internal.legacy.rest.model.sync.RoomResponse;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.util.Map;
/**
* The `RoomEmailInvitation` gathers information for displaying the preview of a room that is unknown for the user.
* Such room can come from an email invitation link or a link to a room.
*/
public class RoomPreviewData {
private static final String LOG_TAG = RoomPreviewData.class.getSimpleName();
// The id of the room to preview.
private String mRoomId;
// the room Alias
private String mRoomAlias;
// the id of the event to preview
private String mEventId;
// In case of email invitation, the information extracted from the email invitation link.
private RoomEmailInvitation mRoomEmailInvitation;
// preview information
// comes from the email invitation or retrieve from an initialSync
private String mRoomName;
private String mRoomAvatarUrl;
// the room state
private RoomState mRoomState;
// If the RoomState cannot be retrieved, this may contains some data
private PublicRoom mPublicRoom;
// the initial sync data
private RoomResponse mRoomResponse;
// the session
private MXSession mSession;
/**
* Create an RoomPreviewData instance
*
* @param session the session.
* @param roomId the room Id to preview
* @param eventId the event Id to preview (optional)
* @param roomAlias the room alias (optional)
* @param emailInvitationParams the email invitation parameters (optional)
*/
public RoomPreviewData(MXSession session, String roomId, String eventId, String roomAlias, Map<String, String> emailInvitationParams) {
mSession = session;
mRoomId = roomId;
mRoomAlias = roomAlias;
mEventId = eventId;
if (null != emailInvitationParams) {
mRoomEmailInvitation = new RoomEmailInvitation(emailInvitationParams);
mRoomName = mRoomEmailInvitation.roomName;
mRoomAvatarUrl = mRoomEmailInvitation.roomAvatarUrl;
}
}
/**
* @return the room state
*/
@Nullable
public RoomState getRoomState() {
return mRoomState;
}
/**
* @return the public room data
*/
@Nullable
public PublicRoom getPublicRoom() {
return mPublicRoom;
}
/**
* Update the room state.
*
* @param roomState the new roomstate
*/
public void setRoomState(RoomState roomState) {
mRoomState = roomState;
}
/**
* @return the room name
*/
public String getRoomName() {
String roomName = mRoomName;
if (TextUtils.isEmpty(roomName)) {
roomName = getRoomIdOrAlias();
}
return roomName;
}
/**
* Set the room name.
*
* @param aRoomName the new room name
*/
public void setRoomName(String aRoomName) {
mRoomName = aRoomName;
}
/**
* @return the room avatar URL
*/
public String getRoomAvatarUrl() {
return mRoomAvatarUrl;
}
/**
* @return the room id
*/
public String getRoomId() {
return mRoomId;
}
/**
* @return the room id or the alias (alias is preferred)
*/
public String getRoomIdOrAlias() {
if (!TextUtils.isEmpty(mRoomAlias)) {
return mRoomAlias;
} else {
return mRoomId;
}
}
/**
* @return the event id.
*/
public String getEventId() {
return mEventId;
}
/**
* @return the session
*/
public MXSession getSession() {
return mSession;
}
/**
* @return the initial sync response
*/
public RoomResponse getRoomResponse() {
return mRoomResponse;
}
/**
* @return the room invitation
*/
public RoomEmailInvitation getRoomEmailInvitation() {
return mRoomEmailInvitation;
}
/**
* Attempt to get more information from the homeserver about the room.
*
* @param apiCallback the callback when the operation is done.
*/
public void fetchPreviewData(final ApiCallback<Void> apiCallback) {
mSession.getRoomsApiClient().initialSync(mRoomId, new ApiCallback<RoomResponse>() {
@Override
public void onSuccess(final RoomResponse roomResponse) {
AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
// save the initial sync response
mRoomResponse = roomResponse;
mRoomState = new RoomState();
mRoomState.roomId = mRoomId;
for (Event event : roomResponse.state) {
mRoomState.applyState(null, event, EventTimeline.Direction.FORWARDS);
}
// TODO LazyLoading handle case where room has no name
mRoomName = mRoomState.name;
mRoomAvatarUrl = mRoomState.getAvatarUrl();
return null;
}
@Override
protected void onPostExecute(Void args) {
apiCallback.onSuccess(null);
}
};
try {
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} catch (final Exception e) {
Log.e(LOG_TAG, "## fetchPreviewData() failed " + e.getMessage(), e);
task.cancel(true);
(new android.os.Handler(Looper.getMainLooper())).post(new Runnable() {
@Override
public void run() {
if (null != apiCallback) {
apiCallback.onUnexpectedError(e);
}
}
});
}
}
@Override
public void onNetworkError(Exception e) {
mRoomState = new RoomState();
mRoomState.roomId = mRoomId;
apiCallback.onNetworkError(e);
}
@Override
public void onMatrixError(MatrixError e) {
mRoomState = new RoomState();
mRoomState.roomId = mRoomId;
apiCallback.onMatrixError(e);
}
@Override
public void onUnexpectedError(Exception e) {
mRoomState = new RoomState();
mRoomState.roomId = mRoomId;
apiCallback.onUnexpectedError(e);
}
});
}
/**
* Set Public RoomData, In case RoomState cannot be retrieved
*
* @param publicRoom
*/
public void setPublicRoom(PublicRoom publicRoom) {
mPublicRoom = publicRoom;
}
}

View file

@ -0,0 +1,593 @@
/*
* Copyright 2015 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.data;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import im.vector.matrix.android.internal.legacy.call.MXCallsManager;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import im.vector.matrix.android.internal.legacy.rest.model.EventContent;
import im.vector.matrix.android.internal.legacy.rest.model.RoomMember;
import im.vector.matrix.android.internal.legacy.rest.model.message.Message;
import im.vector.matrix.android.internal.legacy.rest.model.sync.RoomSyncSummary;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Stores summarised information about the room.
*/
public class RoomSummary implements java.io.Serializable {
private static final String LOG_TAG = RoomSummary.class.getSimpleName();
private static final long serialVersionUID = -3683013938626566489L;
// list of supported types
private static final List<String> sSupportedType = Arrays.asList(
Event.EVENT_TYPE_STATE_ROOM_TOPIC,
Event.EVENT_TYPE_MESSAGE_ENCRYPTED,
Event.EVENT_TYPE_MESSAGE_ENCRYPTION,
Event.EVENT_TYPE_STATE_ROOM_NAME,
Event.EVENT_TYPE_STATE_ROOM_MEMBER,
Event.EVENT_TYPE_STATE_ROOM_CREATE,
Event.EVENT_TYPE_STATE_HISTORY_VISIBILITY,
Event.EVENT_TYPE_STATE_ROOM_THIRD_PARTY_INVITE,
Event.EVENT_TYPE_STICKER);
// List of known unsupported types
private static final List<String> sKnownUnsupportedType = Arrays.asList(
Event.EVENT_TYPE_TYPING,
Event.EVENT_TYPE_STATE_ROOM_POWER_LEVELS,
Event.EVENT_TYPE_STATE_ROOM_JOIN_RULES,
Event.EVENT_TYPE_STATE_CANONICAL_ALIAS,
Event.EVENT_TYPE_STATE_ROOM_ALIASES,
Event.EVENT_TYPE_URL_PREVIEW,
Event.EVENT_TYPE_STATE_RELATED_GROUPS,
Event.EVENT_TYPE_STATE_ROOM_GUEST_ACCESS,
Event.EVENT_TYPE_REDACTION);
private String mRoomId = null;
private String mTopic = null;
private Event mLatestReceivedEvent = null;
// the room state is only used to check
// 1- the invitation status
// 2- the members display name
private transient RoomState mLatestRoomState = null;
// defines the latest read message
private String mReadReceiptEventId;
// the read marker event id
private String mReadMarkerEventId;
private Set<String> mRoomTags;
// counters
public int mUnreadEventsCount;
public int mNotificationCount;
public int mHighlightsCount;
// invitation status
// retrieved at initial sync
// the roomstate is not always known
private String mInviterUserId = null;
// retrieved from the roomState
private String mInviterName = null;
private String mUserId = null;
// Info from sync, depending on the room position in the sync
private String mUserMembership;
/**
* Tell if the room is a user conference user one
*/
private Boolean mIsConferenceUserRoom = null;
/**
* Data from RoomSyncSummary
*/
private List<String> mHeroes = new ArrayList<>();
private int mJoinedMembersCountFromSyncRoomSummary;
private int mInvitedMembersCountFromSyncRoomSummary;
public RoomSummary() {
}
/**
* Create a room summary
*
* @param fromSummary the summary source
* @param event the latest event of the room
* @param roomState the room state - used to display the event
* @param userId our own user id - used to display the room name
*/
public RoomSummary(@Nullable RoomSummary fromSummary,
Event event,
RoomState roomState,
String userId) {
mUserId = userId;
if (null != roomState) {
setRoomId(roomState.roomId);
}
if ((null == getRoomId()) && (null != event)) {
setRoomId(event.roomId);
}
setLatestReceivedEvent(event, roomState);
// if no summary is provided
if (null == fromSummary) {
if (null != event) {
setReadMarkerEventId(event.eventId);
setReadReceiptEventId(event.eventId);
}
if (null != roomState) {
setHighlightCount(roomState.getHighlightCount());
setNotificationCount(roomState.getHighlightCount());
}
setUnreadEventsCount(Math.max(getHighlightCount(), getNotificationCount()));
} else {
// else use the provided summary data
setReadMarkerEventId(fromSummary.getReadMarkerEventId());
setReadReceiptEventId(fromSummary.getReadReceiptEventId());
setUnreadEventsCount(fromSummary.getUnreadEventsCount());
setHighlightCount(fromSummary.getHighlightCount());
setNotificationCount(fromSummary.getNotificationCount());
mHeroes.addAll(fromSummary.mHeroes);
mJoinedMembersCountFromSyncRoomSummary = fromSummary.mJoinedMembersCountFromSyncRoomSummary;
mInvitedMembersCountFromSyncRoomSummary = fromSummary.mInvitedMembersCountFromSyncRoomSummary;
mUserMembership = fromSummary.mUserMembership;
}
}
/**
* Test if the event can be summarized.
* Some event types are not yet supported.
*
* @param event the event to test.
* @return true if the event can be summarized
*/
public static boolean isSupportedEvent(Event event) {
String type = event.getType();
boolean isSupported = false;
// check if the msgtype is supported
if (TextUtils.equals(Event.EVENT_TYPE_MESSAGE, type)) {
try {
JsonObject eventContent = event.getContentAsJsonObject();
String msgType = "";
JsonElement element = eventContent.get("msgtype");
if (null != element) {
msgType = element.getAsString();
}
isSupported = TextUtils.equals(msgType, Message.MSGTYPE_TEXT)
|| TextUtils.equals(msgType, Message.MSGTYPE_EMOTE)
|| TextUtils.equals(msgType, Message.MSGTYPE_NOTICE)
|| TextUtils.equals(msgType, Message.MSGTYPE_IMAGE)
|| TextUtils.equals(msgType, Message.MSGTYPE_AUDIO)
|| TextUtils.equals(msgType, Message.MSGTYPE_VIDEO)
|| TextUtils.equals(msgType, Message.MSGTYPE_FILE);
if (!isSupported && !TextUtils.isEmpty(msgType)) {
Log.e(LOG_TAG, "isSupportedEvent : Unsupported msg type " + msgType);
}
} catch (Exception e) {
Log.e(LOG_TAG, "isSupportedEvent failed " + e.getMessage(), e);
}
} else if (TextUtils.equals(Event.EVENT_TYPE_MESSAGE_ENCRYPTED, type)) {
isSupported = event.hasContentFields();
} else if (TextUtils.equals(Event.EVENT_TYPE_STATE_ROOM_MEMBER, type)) {
JsonObject eventContentAsJsonObject = event.getContentAsJsonObject();
if (null != eventContentAsJsonObject) {
if (eventContentAsJsonObject.entrySet().isEmpty()) {
Log.d(LOG_TAG, "isSupportedEvent : room member with no content is not supported");
} else {
// do not display the avatar / display name update
EventContent prevEventContent = event.getPrevContent();
EventContent eventContent = event.getEventContent();
String membership = null;
String preMembership = null;
if (eventContent != null) {
membership = eventContent.membership;
}
if (prevEventContent != null) {
preMembership = prevEventContent.membership;
}
isSupported = !TextUtils.equals(membership, preMembership);
if (!isSupported) {
Log.d(LOG_TAG, "isSupportedEvent : do not support avatar display name update");
}
}
}
} else {
isSupported = sSupportedType.contains(type)
|| (event.isCallEvent() && !TextUtils.isEmpty(type) && !Event.EVENT_TYPE_CALL_CANDIDATES.equals(type));
}
if (!isSupported) {
// some events are known to be never traced
// avoid warning when it is not required.
if (!sKnownUnsupportedType.contains(type)) {
Log.e(LOG_TAG, "isSupportedEvent : Unsupported event type " + type);
}
}
return isSupported;
}
/**
* @return the user id
*/
public String getUserId() {
return mUserId;
}
/**
* @return the room id
*/
public String getRoomId() {
return mRoomId;
}
/**
* @return the topic.
*/
public String getRoomTopic() {
return mTopic;
}
/**
* @return the room summary event.
*/
public Event getLatestReceivedEvent() {
return mLatestReceivedEvent;
}
/**
* @return the dedicated room state.
*/
public RoomState getLatestRoomState() {
return mLatestRoomState;
}
/**
* @return true if the current user is invited
*/
public boolean isInvited() {
return RoomMember.MEMBERSHIP_INVITE.equals(mUserMembership);
}
/**
* To call when the room is in the invited section of the sync response
*/
public void setIsInvited() {
mUserMembership = RoomMember.MEMBERSHIP_INVITE;
}
/**
* To call when the room is in the joined section of the sync response
*/
public void setIsJoined() {
mUserMembership = RoomMember.MEMBERSHIP_JOIN;
}
/**
* @return true if the current user is invited
*/
public boolean isJoined() {
return RoomMember.MEMBERSHIP_JOIN.equals(mUserMembership);
}
/**
* @return the inviter user id.
*/
public String getInviterUserId() {
return mInviterUserId;
}
/**
* Set the room's {@link org.matrix.androidsdk.rest.model.Event#EVENT_TYPE_STATE_ROOM_TOPIC}.
*
* @param topic The topic
* @return This summary for chaining calls.
*/
public RoomSummary setTopic(String topic) {
mTopic = topic;
return this;
}
/**
* Set the room's ID..
*
* @param roomId The room ID
* @return This summary for chaining calls.
*/
public RoomSummary setRoomId(String roomId) {
mRoomId = roomId;
return this;
}
/**
* Set the latest tracked event (e.g. the latest m.room.message)
*
* @param event The most-recent event.
* @param roomState The room state
* @return This summary for chaining calls.
*/
public RoomSummary setLatestReceivedEvent(Event event, RoomState roomState) {
setLatestReceivedEvent(event);
setLatestRoomState(roomState);
if (null != roomState) {
setTopic(roomState.topic);
}
return this;
}
/**
* Set the latest tracked event (e.g. the latest m.room.message)
*
* @param event The most-recent event.
* @return This summary for chaining calls.
*/
public RoomSummary setLatestReceivedEvent(Event event) {
mLatestReceivedEvent = event;
return this;
}
/**
* Set the latest RoomState
*
* @param roomState The room state of the latest event.
* @return This summary for chaining calls.
*/
public RoomSummary setLatestRoomState(RoomState roomState) {
mLatestRoomState = roomState;
// Keep this code for compatibility?
boolean isInvited = false;
// check for the invitation status
if (null != mLatestRoomState) {
RoomMember member = mLatestRoomState.getMember(mUserId);
isInvited = (null != member) && RoomMember.MEMBERSHIP_INVITE.equals(member.membership);
}
// when invited, the only received message should be the invitation one
if (isInvited) {
mInviterName = null;
if (null != mLatestReceivedEvent) {
mInviterName = mInviterUserId = mLatestReceivedEvent.getSender();
// try to retrieve a display name
if (null != mLatestRoomState) {
mInviterName = mLatestRoomState.getMemberName(mLatestReceivedEvent.getSender());
}
}
} else {
mInviterUserId = mInviterName = null;
}
return this;
}
/**
* Set the read receipt event Id
*
* @param eventId the read receipt event id.
*/
public void setReadReceiptEventId(String eventId) {
Log.d(LOG_TAG, "## setReadReceiptEventId() : " + eventId + " roomId " + getRoomId());
mReadReceiptEventId = eventId;
}
/**
* @return the read receipt event id
*/
public String getReadReceiptEventId() {
return mReadReceiptEventId;
}
/**
* Set the read marker event Id
*
* @param eventId the read marker event id.
*/
public void setReadMarkerEventId(String eventId) {
Log.d(LOG_TAG, "## setReadMarkerEventId() : " + eventId + " roomId " + getRoomId());
if (TextUtils.isEmpty(eventId)) {
Log.e(LOG_TAG, "## setReadMarkerEventId') : null mReadMarkerEventId, in " + getRoomId());
}
mReadMarkerEventId = eventId;
}
/**
* @return the read receipt event id
*/
public String getReadMarkerEventId() {
if (TextUtils.isEmpty(mReadMarkerEventId)) {
Log.e(LOG_TAG, "## getReadMarkerEventId') : null mReadMarkerEventId, in " + getRoomId());
mReadMarkerEventId = getReadReceiptEventId();
}
return mReadMarkerEventId;
}
/**
* Update the unread message counter
*
* @param count the unread events count.
*/
public void setUnreadEventsCount(int count) {
Log.d(LOG_TAG, "## setUnreadEventsCount() : " + count + " roomId " + getRoomId());
mUnreadEventsCount = count;
}
/**
* @return the unread events count
*/
public int getUnreadEventsCount() {
return mUnreadEventsCount;
}
/**
* Update the notification counter
*
* @param count the notification counter
*/
public void setNotificationCount(int count) {
Log.d(LOG_TAG, "## setNotificationCount() : " + count + " roomId " + getRoomId());
mNotificationCount = count;
}
/**
* @return the notification count
*/
public int getNotificationCount() {
return mNotificationCount;
}
/**
* Update the highlight counter
*
* @param count the highlight counter
*/
public void setHighlightCount(int count) {
Log.d(LOG_TAG, "## setHighlightCount() : " + count + " roomId " + getRoomId());
mHighlightsCount = count;
}
/**
* @return the highlight count
*/
public int getHighlightCount() {
return mHighlightsCount;
}
/**
* @return the room tags
*/
public Set<String> getRoomTags() {
return mRoomTags;
}
/**
* Update the room tags
*
* @param roomTags the room tags
*/
public void setRoomTags(final Set<String> roomTags) {
if (roomTags != null) {
// wraps the set into a serializable one
mRoomTags = new HashSet<>(roomTags);
} else {
mRoomTags = new HashSet<>();
}
}
public boolean isConferenceUserRoom() {
// test if it is not yet initialized
if (null == mIsConferenceUserRoom) {
mIsConferenceUserRoom = false;
// FIXME LazyLoading Heroes does not contains me
// FIXME I'ms not sure this code will work anymore
Collection<String> membersId = getHeroes();
// works only with 1:1 room
if (2 == membersId.size()) {
for (String userId : membersId) {
if (MXCallsManager.isConferenceUserId(userId)) {
mIsConferenceUserRoom = true;
break;
}
}
}
}
return mIsConferenceUserRoom;
}
public void setIsConferenceUserRoom(boolean isConferenceUserRoom) {
mIsConferenceUserRoom = isConferenceUserRoom;
}
public void setRoomSyncSummary(@NonNull RoomSyncSummary roomSyncSummary) {
if (roomSyncSummary.heroes != null) {
mHeroes.clear();
mHeroes.addAll(roomSyncSummary.heroes);
}
if (roomSyncSummary.joinedMembersCount != null) {
// Update the value
mJoinedMembersCountFromSyncRoomSummary = roomSyncSummary.joinedMembersCount;
}
if (roomSyncSummary.invitedMembersCount != null) {
// Update the value
mInvitedMembersCountFromSyncRoomSummary = roomSyncSummary.invitedMembersCount;
}
}
@NonNull
public List<String> getHeroes() {
return mHeroes;
}
public int getNumberOfJoinedMembers() {
return mJoinedMembersCountFromSyncRoomSummary;
}
public int getNumberOfInvitedMembers() {
return mInvitedMembersCountFromSyncRoomSummary;
}
}

View file

@ -0,0 +1,92 @@
/*
* Copyright 2015 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.data;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import im.vector.matrix.android.internal.legacy.rest.model.RoomTags;
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.util.HashMap;
import java.util.Map;
/**
* Class representing a room tag.
*/
public class RoomTag implements java.io.Serializable {
private static final long serialVersionUID = 5172602958896551204L;
private static final String LOG_TAG = RoomTag.class.getSimpleName();
//
public static final String ROOM_TAG_FAVOURITE = "m.favourite";
public static final String ROOM_TAG_LOW_PRIORITY = "m.lowpriority";
public static final String ROOM_TAG_NO_TAG = "m.recent";
public static final String ROOM_TAG_SERVER_NOTICE = "m.server_notice";
/**
* The name of a tag.
*/
public String mName;
/**
* Try to parse order as Double.
* Provides nil if the items cannot be parsed.
*/
public Double mOrder;
/**
* RoomTag creator.
*
* @param aName the tag name.
* @param anOrder the tag order
*/
public RoomTag(String aName, Double anOrder) {
mName = aName;
mOrder = anOrder;
}
/**
* Extract a list of tags from a room tag event.
*
* @param event a room tag event (which can contains several tags)
* @return a dictionary containing the tags the user defined for one room.
*/
public static Map<String, RoomTag> roomTagsWithTagEvent(Event event) {
Map<String, RoomTag> tags = new HashMap<>();
try {
RoomTags roomtags = JsonUtils.toRoomTags(event.getContent());
if ((null != roomtags.tags) && (0 != roomtags.tags.size())) {
for (String tagName : roomtags.tags.keySet()) {
Map<String, Double> params = roomtags.tags.get(tagName);
if (params != null) {
tags.put(tagName, new RoomTag(tagName, params.get("order")));
} else {
tags.put(tagName, new RoomTag(tagName, null));
}
}
}
} catch (Exception e) {
Log.e(LOG_TAG, "roomTagsWithTagEvent fails " + e.getMessage(), e);
}
return tags;
}
}

View file

@ -0,0 +1,40 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.data.comparator;
import im.vector.matrix.android.internal.legacy.interfaces.DatedObject;
import java.util.Comparator;
public class Comparators {
// comparator to sort from the oldest to the latest.
public static final Comparator<DatedObject> ascComparator = new Comparator<DatedObject>() {
@Override
public int compare(DatedObject datedObject1, DatedObject datedObject2) {
return (int) (datedObject1.getDate() - datedObject2.getDate());
}
};
// comparator to sort from the latest to the oldest.
public static final Comparator<DatedObject> descComparator = new Comparator<DatedObject>() {
@Override
public int compare(DatedObject datedObject1, DatedObject datedObject2) {
return (int) (datedObject2.getDate() - datedObject1.getDate());
}
};
}

View file

@ -0,0 +1,50 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.data.comparator;
import im.vector.matrix.android.internal.legacy.data.Room;
import im.vector.matrix.android.internal.legacy.data.RoomTag;
import java.util.Comparator;
/**
* This class is responsible for comparing rooms by the tag's order
*/
public class RoomComparatorWithTag implements Comparator<Room> {
private final String mTag;
public RoomComparatorWithTag(final String tag) {
mTag = tag;
}
@Override
public int compare(final Room r1, final Room r2) {
final int res;
final RoomTag tag1 = r1.getAccountData().roomTag(mTag);
final RoomTag tag2 = r2.getAccountData().roomTag(mTag);
if (tag1 != null && tag1.mOrder != null && tag2 != null && tag2.mOrder != null) {
res = Double.compare(tag1.mOrder, tag2.mOrder);
} else if (tag1 != null && tag1.mOrder != null) {
res = 1;
} else {
res = -1;
}
return res;
}
}

View file

@ -0,0 +1,310 @@
/*
* Copyright 2016 OpenMarket Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.data.cryptostore;
import android.content.Context;
import im.vector.matrix.android.internal.legacy.crypto.IncomingRoomKeyRequest;
import im.vector.matrix.android.internal.legacy.crypto.OutgoingRoomKeyRequest;
import im.vector.matrix.android.internal.legacy.crypto.data.MXDeviceInfo;
import im.vector.matrix.android.internal.legacy.crypto.data.MXOlmInboundGroupSession2;
import im.vector.matrix.android.internal.legacy.rest.model.login.Credentials;
import org.matrix.olm.OlmAccount;
import org.matrix.olm.OlmSession;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* the crypto data store
*/
public interface IMXCryptoStore {
/**
* Init a crypto store for the passed credentials.
*
* @param context the application context
* @param credentials the credentials of the account.
*/
void initWithCredentials(Context context, Credentials credentials);
/**
* @return if the corrupted is corrupted.
*/
boolean isCorrupted();
/**
* Indicate if the store contains data for the passed account.
*
* @return true means that the user enabled the crypto in a previous session
*/
boolean hasData();
/**
* Delete the crypto store for the passed credentials.
*/
void deleteStore();
/**
* open any existing crypto store
*/
void open();
/**
* Close the store
*/
void close();
/**
* Store the device id.
*
* @param deviceId the device id
*/
void storeDeviceId(String deviceId);
/**
* @return the device id
*/
String getDeviceId();
/**
* Store the end to end account for the logged-in user.
*
* @param account the account to save
*/
void storeAccount(OlmAccount account);
/**
* @return the olm account
*/
OlmAccount getAccount();
/**
* Store a device for a user.
*
* @param userId The user's id.
* @param device the device to store.
*/
void storeUserDevice(String userId, MXDeviceInfo device);
/**
* Retrieve a device for a user.
*
* @param deviceId The device id.
* @param userId The user's id.
* @return A map from device id to 'MXDevice' object for the device.
*/
MXDeviceInfo getUserDevice(String deviceId, String userId);
/**
* Store the known devices for a user.
*
* @param userId The user's id.
* @param devices A map from device id to 'MXDevice' object for the device.
*/
void storeUserDevices(String userId, Map<String, MXDeviceInfo> devices);
/**
* Retrieve the known devices for a user.
*
* @param userId The user's id.
* @return The devices map if some devices are known, else null
*/
Map<String, MXDeviceInfo> getUserDevices(String userId);
/**
* Store the crypto algorithm for a room.
*
* @param roomId the id of the room.
* @param algorithm the algorithm.
*/
void storeRoomAlgorithm(String roomId, String algorithm);
/**
* Provides the algorithm used in a dedicated room.
*
* @param roomId the room id
* @return the algorithm, null is the room is not encrypted
*/
String getRoomAlgorithm(String roomId);
/**
* Store a session between the logged-in user and another device.
*
* @param session the end-to-end session.
* @param deviceKey the public key of the other device.
*/
void storeSession(OlmSession session, String deviceKey);
/**
* Retrieve the end-to-end sessions between the logged-in user and another
* device.
*
* @param deviceKey the public key of the other device.
* @return A map from sessionId to Base64 end-to-end session.
*/
Map<String, OlmSession> getDeviceSessions(String deviceKey);
/**
* Store an inbound group session.
*
* @param session the inbound group session and its context.
*/
void storeInboundGroupSession(MXOlmInboundGroupSession2 session);
/**
* Retrieve an inbound group session.
*
* @param sessionId the session identifier.
* @param senderKey the base64-encoded curve25519 key of the sender.
* @return an inbound group session.
*/
MXOlmInboundGroupSession2 getInboundGroupSession(String sessionId, String senderKey);
/**
* Retrieve the known inbound group sessions.
*
* @return an inbound group session.
*/
List<MXOlmInboundGroupSession2> getInboundGroupSessions();
/**
* Remove an inbound group session
*
* @param sessionId the session identifier.
* @param senderKey the base64-encoded curve25519 key of the sender.
*/
void removeInboundGroupSession(String sessionId, String senderKey);
/**
* Set the global override for whether the client should ever send encrypted
* messages to unverified devices.
* If false, it can still be overridden per-room.
* If true, it overrides the per-room settings.
*
* @param block true to unilaterally blacklist all
*/
void setGlobalBlacklistUnverifiedDevices(boolean block);
/**
* @return true to unilaterally blacklist all unverified devices.
*/
boolean getGlobalBlacklistUnverifiedDevices();
/**
* Updates the rooms ids list in which the messages are not encrypted for the unverified devices.
*
* @param roomIds the room ids list
*/
void setRoomsListBlacklistUnverifiedDevices(List<String> roomIds);
/**
* Provides the rooms ids list in which the messages are not encrypted for the unverified devices.
*
* @return the room Ids list
*/
List<String> getRoomsListBlacklistUnverifiedDevices();
/**
* @return the devices statuses map
*/
Map<String, Integer> getDeviceTrackingStatuses();
/**
* Save the device statuses
*
* @param deviceTrackingStatuses the device tracking statuses
*/
void saveDeviceTrackingStatuses(Map<String, Integer> deviceTrackingStatuses);
/**
* Get the tracking status of a specified userId devices.
*
* @param userId the user id
* @param defaultValue the default avlue
* @return the tracking status
*/
int getDeviceTrackingStatus(String userId, int defaultValue);
/**
* Look for an existing outgoing room key request, and if none is found,
*
* @param requestBody the request body
* @return an OutgoingRoomKeyRequest instance or null
*/
OutgoingRoomKeyRequest getOutgoingRoomKeyRequest(Map<String, String> requestBody);
/**
* Look for an existing outgoing room key request, and if none is found,
* + add a new one.
*
* @param request the request
* @return either the same instance as passed in, or the existing one.
*/
OutgoingRoomKeyRequest getOrAddOutgoingRoomKeyRequest(OutgoingRoomKeyRequest request);
/**
* Look for room key requests by state.
*
* @param states the states
* @return an OutgoingRoomKeyRequest or null
*/
OutgoingRoomKeyRequest getOutgoingRoomKeyRequestByState(Set<OutgoingRoomKeyRequest.RequestState> states);
/**
* Update an existing outgoing request.
*
* @param request the request
*/
void updateOutgoingRoomKeyRequest(OutgoingRoomKeyRequest request);
/**
* Delete an outgoing room key request.
*
* @param transactionId the transaction id.
*/
void deleteOutgoingRoomKeyRequest(String transactionId);
/**
* Store an incomingRoomKeyRequest instance
*
* @param incomingRoomKeyRequest the incoming key request
*/
void storeIncomingRoomKeyRequest(IncomingRoomKeyRequest incomingRoomKeyRequest);
/**
* Delete an incomingRoomKeyRequest instance
*
* @param incomingRoomKeyRequest the incoming key request
*/
void deleteIncomingRoomKeyRequest(IncomingRoomKeyRequest incomingRoomKeyRequest);
/**
* Search an IncomingRoomKeyRequest
*
* @param userId the user id
* @param deviceId the device id
* @param requestId the request id
* @return an IncomingRoomKeyRequest if it exists, else null
*/
IncomingRoomKeyRequest getIncomingRoomKeyRequest(String userId, String deviceId, String requestId);
/**
* @return the pending IncomingRoomKeyRequest requests
*/
List<IncomingRoomKeyRequest> getPendingIncomingRoomKeyRequests();
}

View file

@ -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;
}

View file

@ -0,0 +1,75 @@
/*
* Copyright 2017 Vector Creations Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.data.cryptostore;
import java.util.ArrayList;
import java.util.List;
public class MXFileCryptoStoreMetaData2 implements java.io.Serializable {
// avoid creating another MXFileCryptoStoreMetaData3
// set a serialVersionUID allows to update the class.
private static final long serialVersionUID = 9166554107081078408L;
// The obtained user id.
public String mUserId;
// the device id
public String mDeviceId;
// The current version of the store.
public int mVersion;
// flag to tell if the device is announced
// not anymore used
public boolean mDeviceAnnounced;
// flag to tell if the unverified devices are blacklisted for any room.
public boolean mGlobalBlacklistUnverifiedDevices;
// Room ids list in which the unverified devices are blacklisted
public List<String> mBlacklistUnverifiedDevicesRoomIdsList;
/**
* Default constructor
*
* @param userId the user id
* @param deviceId the device id
* @param version the version
*/
public MXFileCryptoStoreMetaData2(String userId, String deviceId, int version) {
mUserId = new String(userId);
mDeviceId = (null != deviceId) ? new String(deviceId) : null;
mVersion = version;
mDeviceAnnounced = false;
mGlobalBlacklistUnverifiedDevices = false;
mBlacklistUnverifiedDevicesRoomIdsList = new ArrayList<>();
}
/**
* Constructor with the genuine metadata format data.
*
* @param metadata the genuine metadata format data.
*/
public MXFileCryptoStoreMetaData2(MXFileCryptoStoreMetaData metadata) {
mUserId = metadata.mUserId;
mDeviceId = metadata.mDeviceId;
mVersion = metadata.mVersion;
mDeviceAnnounced = metadata.mDeviceAnnounced;
mGlobalBlacklistUnverifiedDevices = false;
mBlacklistUnverifiedDevicesRoomIdsList = new ArrayList<>();
}
}

View file

@ -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);
}

View file

@ -0,0 +1,645 @@
/*
* Copyright 2015 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.data.store;
import android.content.Context;
import android.support.annotation.Nullable;
import im.vector.matrix.android.internal.legacy.data.timeline.EventTimeline;
import im.vector.matrix.android.internal.legacy.data.Room;
import im.vector.matrix.android.internal.legacy.data.RoomAccountData;
import im.vector.matrix.android.internal.legacy.data.RoomSummary;
import im.vector.matrix.android.internal.legacy.data.metrics.MetricsListener;
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import im.vector.matrix.android.internal.legacy.rest.model.ReceiptData;
import im.vector.matrix.android.internal.legacy.rest.model.RoomMember;
import im.vector.matrix.android.internal.legacy.rest.model.TokensChunkEvents;
import im.vector.matrix.android.internal.legacy.rest.model.User;
import im.vector.matrix.android.internal.legacy.rest.model.group.Group;
import im.vector.matrix.android.internal.legacy.rest.model.pid.ThirdPartyIdentifier;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* An interface for storing and retrieving Matrix objects.
*/
public interface IMXStore {
/**
* Save changes in the store.
* If the store uses permanent storage like database or file, it is the optimised time
* to commit the last changes.
*/
void commit();
/**
* Open the store.
*/
void open();
/**
* Close the store.
* Any pending operation must be complete in this call.
*/
void close();
/**
* Clear the store.
* Any pending operation must be complete in this call.
*/
void clear();
/**
* @return the used context
*/
Context getContext();
/**
* Indicate if the MXStore implementation stores data permanently.
* Permanent storage allows the SDK to make less requests at the startup.
*
* @return true if permanent.
*/
boolean isPermanent();
/**
* Check if the initial load is performed.
*
* @return true if it is ready.
*/
boolean isReady();
/**
* Check if the read receipts are ready to be used.
*
* @return true if they are ready.
*/
boolean areReceiptsReady();
/**
* @return true if the store is corrupted.
*/
boolean isCorrupted();
/**
* Warn that the store data are corrupted.
* It might append if an update request failed.
*
* @param reason the corruption reason
*/
void setCorrupted(String reason);
/**
* Returns to disk usage size in bytes.
*
* @return disk usage size
*/
long diskUsage();
/**
* Returns the latest known event stream token
*
* @return the event stream token
*/
String getEventStreamToken();
/**
* Set the event stream token.
*
* @param token the event stream token
*/
void setEventStreamToken(String token);
/**
* Add a MXStore listener.
*
* @param listener the listener
*/
void addMXStoreListener(IMXStoreListener listener);
/**
* remove a MXStore listener.
*
* @param listener the listener
*/
void removeMXStoreListener(IMXStoreListener listener);
/**
* @return the display name
*/
String displayName();
/**
* Update the user display name
*
* @param displayName the displayname
* @param ts the timestamp update
* @return true if there is an update
*/
boolean setDisplayName(String displayName, long ts);
/**
* @return the avatar URL
*/
String avatarURL();
/**
* Update the avatar URL
*
* @param avatarURL the new URL
* @param ts the timestamp update
* @return true if there is an update
*/
boolean setAvatarURL(String avatarURL, long ts);
/**
* @return the third party identifiers list
*/
List<ThirdPartyIdentifier> thirdPartyIdentifiers();
/**
* Update the third party identifiers list.
*
* @param identifiers the identifiers list
*/
void setThirdPartyIdentifiers(List<ThirdPartyIdentifier> identifiers);
/**
* Update the ignored user ids list.
*
* @param users the user ids list
*/
void setIgnoredUserIdsList(List<String> users);
/**
* Update the direct chat rooms list
*
* @param directChatRoomsDict the direct chats map
*/
void setDirectChatRoomsDict(Map<String, List<String>> directChatRoomsDict);
/**
* @return the known rooms list
*/
Collection<Room> getRooms();
/**
* Retrieve a room from its room id
*
* @param roomId the room id
* @return the room if it exists
*/
Room getRoom(String roomId);
/**
* @return the known users lists
*/
Collection<User> getUsers();
/**
* Retrieves an user by its user id.
*
* @param userId the user id
* @return the user
*/
User getUser(String userId);
/**
* @return the ignored user ids list
*/
List<String> getIgnoredUserIdsList();
/**
* @return the direct chats rooms list
*/
Map<String, List<String>> getDirectChatRoomsDict();
/**
* Flush an updated user.
*
* @param user the user
*/
void storeUser(User user);
/**
* Flush an user from a room member.
*
* @param roomMember the room member
*/
void updateUserWithRoomMemberEvent(RoomMember roomMember);
/**
* Flush a room.
*
* @param room the room
*/
void storeRoom(Room room);
/**
* Store a block of room events either live or from pagination.
*
* @param roomId the room id
* @param tokensChunkEvents the events to be stored.
* @param direction the direction; forwards for live, backwards for pagination
*/
void storeRoomEvents(String roomId, TokensChunkEvents tokensChunkEvents, EventTimeline.Direction direction);
/**
* Store the back token of a room.
*
* @param roomId the room id.
* @param backToken the back token
*/
void storeBackToken(String roomId, String backToken);
/**
* Store a live room event.
*
* @param event The event to be stored.
*/
void storeLiveRoomEvent(Event event);
/**
* @param eventId the id of the event to retrieve.
* @param roomId the id of the room.
* @return true if the event exists in the store.
*/
boolean doesEventExist(String eventId, String roomId);
/**
* Retrieve an event from its room Id and its Event id
*
* @param eventId the event id
* @param roomId the room Id
* @return the event (null if it is not found)
*/
Event getEvent(String eventId, String roomId);
/**
* Delete an event
*
* @param event The event to be deleted.
*/
void deleteEvent(Event event);
/**
* Remove all sent messages in a room.
*
* @param roomId the id of the room.
* @param keepUnsent set to true to do not delete the unsent message
*/
void deleteAllRoomMessages(String roomId, boolean keepUnsent);
/**
* Flush the room events.
*
* @param roomId the id of the room.
*/
void flushRoomEvents(String roomId);
/**
* Delete the room from the storage.
* The room data and its reference will be deleted.
*
* @param roomId the roomId.
*/
void deleteRoom(String roomId);
/**
* Delete the room data from the storage;
* The room data are cleared but the getRoom returned object will be the same.
*
* @param roomId the roomId.
*/
void deleteRoomData(String roomId);
/**
* Retrieve all non-state room events for this room.
*
* @param roomId The room ID
* @return A collection of events. null if there is no cached event.
*/
Collection<Event> getRoomMessages(final String roomId);
/**
* Retrieve all non-state room events for this room.
*
* @param roomId The room ID
* @param fromToken the token
* @param limit the maximum number of messages to retrieve.
* @return A collection of events. null if there is no cached event.
*/
TokensChunkEvents getEarlierMessages(final String roomId, final String fromToken, final int limit);
/**
* Get the oldest event from the given room (to prevent pagination overlap).
*
* @param roomId the room id
* @return the event
*/
Event getOldestEvent(String roomId);
/**
* Get the latest event from the given room (to update summary for example)
*
* @param roomId the room id
* @return the event
*/
Event getLatestEvent(String roomId);
/**
* Count the number of events after the provided events id
*
* @param roomId the room id.
* @param eventId the event id to find.
* @return the events count after this event if
*/
int eventsCountAfter(String roomId, String eventId);
// Design note: This is part of the store interface so the concrete implementation can leverage
// how they are storing the data to do this in an efficient manner (e.g. SQL JOINs)
// compared to calling getRooms() then getRoomEvents(roomId, limit=1) for each room
// (which forces single SELECTs)
/**
* <p>Retrieve a list of all the room summaries stored.</p>
* Typically this method will be called when generating a 'Recent Activity' list.
*
* @return A collection of room summaries.
*/
Collection<RoomSummary> getSummaries();
/**
* Get the stored summary for the given room.
*
* @param roomId the room id
* @return the summary for the room, or null in case of error
*/
@Nullable
RoomSummary getSummary(String roomId);
/**
* Flush a room summary
*
* @param summary the summary.
*/
void flushSummary(RoomSummary summary);
/**
* Flush the room summaries
*/
void flushSummaries();
/**
* Store a new summary.
*
* @param summary the summary
*/
void storeSummary(RoomSummary summary);
/**
* Store the room liveState.
*
* @param roomId roomId the id of the room.
*/
void storeLiveStateForRoom(String roomId);
/**
* Store a room state event.
* The room states are built with several events.
*
* @param roomId the room id
* @param event the event
*/
void storeRoomStateEvent(String roomId, Event event);
/**
* Retrieve the room state creation events
*
* @param roomId the room id
* @param callback the asynchronous callback
*/
void getRoomStateEvents(String roomId, ApiCallback<List<Event>> callback);
/**
* Return the list of latest unsent events.
* The provided events are the unsent ones since the last sent one.
* They are ordered.
*
* @param roomId the room id
* @return list of unsent events
*/
List<Event> getLatestUnsentEvents(String roomId);
/**
* Return the list of undelivered events
*
* @param roomId the room id
* @return list of undelivered events
*/
List<Event> getUndeliveredEvents(String roomId);
/**
* Return the list of unknown device events.
*
* @param roomId the room id
* @return list of unknown device events
*/
List<Event> getUnknownDeviceEvents(String roomId);
/**
* Returns the receipts list for an event in a dedicated room.
* if sort is set to YES, they are sorted from the latest to the oldest ones.
*
* @param roomId The room Id.
* @param eventId The event Id. (null to retrieve all existing receipts)
* @param excludeSelf exclude the oneself read receipts.
* @param sort to sort them from the latest to the oldest
* @return the receipts for an event in a dedicated room.
*/
List<ReceiptData> getEventReceipts(String roomId, String eventId, boolean excludeSelf, boolean sort);
/**
* Store the receipt for an user in a room.
* The receipt validity is checked i.e the receipt is not for an already read message.
*
* @param receipt The event
* @param roomId The roomId
* @return true if the receipt has been stored
*/
boolean storeReceipt(ReceiptData receipt, String roomId);
/**
* Get the receipt for an user in a dedicated room.
*
* @param roomId the room id.
* @param userId the user id.
* @return the dedicated receipt
*/
ReceiptData getReceipt(String roomId, String userId);
/**
* Provides the unread events list.
*
* @param roomId the room id.
* @param types an array of event types strings (Event.EVENT_TYPE_XXX).
* @return the unread events list.
*/
List<Event> unreadEvents(String roomId, List<String> types);
/**
* Check if an event has been read by an user.
*
* @param roomId the room Id
* @param userId the user id
* @param eventId the event id
* @return true if the user has read the message.
*/
boolean isEventRead(String roomId, String userId, String eventId);
/**
* Store the user data for a room.
*
* @param roomId The room Id.
* @param accountData the account data.
*/
void storeAccountData(String roomId, RoomAccountData accountData);
/**
* Provides the store preload time in milliseconds.
*
* @return the store preload time in milliseconds.
*/
long getPreloadTime();
/**
* Provides some store stats
*
* @return the store stats
*/
Map<String, Long> getStats();
/**
* Start a runnable from the store thread
*
* @param runnable the runnable to call
*/
void post(Runnable runnable);
/**
* Store a group
*
* @param group the group to store
*/
void storeGroup(Group group);
/**
* Flush a group in store.
*
* @param group the group
*/
void flushGroup(Group group);
/**
* Delete a group
*
* @param groupId the group id to delete
*/
void deleteGroup(String groupId);
/**
* Retrieve a group from its id.
*
* @param groupId the group id
* @return the group if it exists
*/
Group getGroup(String groupId);
/**
* @return the stored groups
*/
Collection<Group> getGroups();
/**
* Set the URL preview status
*
* @param value the URL preview status
*/
void setURLPreviewEnabled(boolean value);
/**
* Tells if the global URL preview is enabled.
*
* @return true if it is enabled
*/
boolean isURLPreviewEnabled();
/**
* Update the rooms list which don't have URL previews
*
* @param roomIds the room ids list
*/
void setRoomsWithoutURLPreview(Set<String> roomIds);
/**
* Set the user widgets
*/
void setUserWidgets(Map<String, Object> contentDict);
/**
* Get the user widgets
*/
Map<String, Object> getUserWidgets();
/**
* @return the room ids list which don't have URL preview enabled
*/
Set<String> getRoomsWithoutURLPreviews();
/**
* Add a couple Json filter / filterId
*/
void addFilter(String jsonFilter, String filterId);
/**
* Get the Map of all filters configured server side (note: only by this current instance of Riot)
*/
Map<String, String> getFilters();
/**
* Set the public key of the antivirus server
*/
void setAntivirusServerPublicKey(@Nullable String key);
/**
* @return the public key of the antivirus server
*/
@Nullable
String getAntivirusServerPublicKey();
/**
* Update the metrics listener
*
* @param metricsListener the metrics listener
*/
void setMetricsListener(MetricsListener metricsListener);
}

View file

@ -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);
}

View file

@ -0,0 +1,91 @@
/*
* Copyright 2015 OpenMarket Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.data.store;
import im.vector.matrix.android.internal.legacy.rest.model.pid.ThirdPartyIdentifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class MXFileStoreMetaData implements java.io.Serializable {
// The obtained user id.
public String mUserId = null;
// The access token to create a MXRestClient.
public String mAccessToken = null;
// The token indicating from where to start listening event stream to get live events.
public String mEventStreamToken = null;
//The current version of the store.
public int mVersion = -1;
/**
* User information
*/
public String mUserDisplayName = null;
public String mUserAvatarUrl = null;
public List<ThirdPartyIdentifier> mThirdPartyIdentifiers = null;
public List<String> mIgnoredUsers = new ArrayList<>();
public Map<String, List<String>> mDirectChatRoomsMap = null;
public boolean mIsUrlPreviewEnabled = false;
public Map<String, Object> mUserWidgets = new HashMap<>();
public Set<String> mRoomsListWithoutURLPrevew = new HashSet<>();
// To store known filters by the server. Keys are the filter as a Json String, Values are the filterId returned by the server
// Mainly used to store a filterId related to a corresponding Json string.
public Map<String, String> mKnownFilters = new HashMap<>();
// crypto
public boolean mEndToEndDeviceAnnounced = false;
public String mAntivirusServerPublicKey;
public MXFileStoreMetaData deepCopy() {
MXFileStoreMetaData copy = new MXFileStoreMetaData();
copy.mUserId = mUserId;
copy.mAccessToken = mAccessToken;
copy.mEventStreamToken = mEventStreamToken;
copy.mVersion = mVersion;
copy.mUserDisplayName = mUserDisplayName;
if (null != copy.mUserDisplayName) {
copy.mUserDisplayName.trim();
}
copy.mUserAvatarUrl = mUserAvatarUrl;
copy.mThirdPartyIdentifiers = mThirdPartyIdentifiers;
copy.mIgnoredUsers = mIgnoredUsers;
copy.mDirectChatRoomsMap = mDirectChatRoomsMap;
copy.mEndToEndDeviceAnnounced = mEndToEndDeviceAnnounced;
copy.mAntivirusServerPublicKey = mAntivirusServerPublicKey;
copy.mIsUrlPreviewEnabled = mIsUrlPreviewEnabled;
copy.mUserWidgets = mUserWidgets;
copy.mRoomsListWithoutURLPrevew = mRoomsListWithoutURLPrevew;
copy.mKnownFilters = new HashMap<>(mKnownFilters);
return copy;
}
}

View file

@ -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) {
}
}

View file

@ -0,0 +1,235 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.data.timeline;
import android.support.annotation.NonNull;
import im.vector.matrix.android.internal.legacy.data.Room;
import im.vector.matrix.android.internal.legacy.data.RoomState;
import im.vector.matrix.android.internal.legacy.data.store.IMXStore;
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import im.vector.matrix.android.internal.legacy.rest.model.sync.InvitedRoomSync;
import im.vector.matrix.android.internal.legacy.rest.model.sync.RoomSync;
/**
* A `EventTimeline` instance represents a contiguous sequence of events in a room.
* <p>
* There are two kinds of timeline:
* <p>
* - live timelines: they receive live events from the events stream. You can paginate
* backwards but not forwards.
* All (live or backwards) events they receive are stored in the store of the current
* MXSession.
* <p>
* - past timelines: they start in the past from an `initialEventId`. They are filled
* with events on calls of [MXEventTimeline paginate] in backwards or forwards direction.
* Events are stored in a in-memory store (MXMemoryStore).
*/
public interface EventTimeline {
/**
* Defines that the current timeline is an historical one
*
* @param isHistorical true when the current timeline is an historical one
*/
void setIsHistorical(boolean isHistorical);
/**
* Returns true if the current timeline is an historical one
*/
boolean isHistorical();
/**
* @return the unique identifier
*/
String getTimelineId();
/**
* @return the dedicated room
*/
Room getRoom();
/**
* @return the used store
*/
IMXStore getStore();
/**
* @return the initial event id.
*/
String getInitialEventId();
/**
* @return true if this timeline is the live one
*/
boolean isLiveTimeline();
/**
* Get whether we are at the end of the message stream
*
* @return true if end has been reached
*/
boolean hasReachedHomeServerForwardsPaginationEnd();
/**
* Reset the back state so that future history requests start over from live.
* Must be called when opening a room if interested in history.
*/
void initHistory();
/**
* @return The state of the room at the top most recent event of the timeline.
*/
RoomState getState();
/**
* Update the state.
*
* @param state the new state.
*/
void setState(RoomState state);
/**
* Handle the invitation room events
*
* @param invitedRoomSync the invitation room events.
*/
void handleInvitedRoomSync(InvitedRoomSync invitedRoomSync);
/**
* Manage the joined room events.
*
* @param roomSync the roomSync.
* @param isGlobalInitialSync true if the sync has been triggered by a global initial sync
*/
void handleJoinedRoomSync(@NonNull RoomSync roomSync, boolean isGlobalInitialSync);
/**
* Store an outgoing event.
*
* @param event the event to store
*/
void storeOutgoingEvent(Event event);
/**
* Tells if a back pagination can be triggered.
*
* @return true if a back pagination can be triggered.
*/
boolean canBackPaginate();
/**
* Request older messages.
*
* @param callback the asynchronous callback
* @return true if request starts
*/
boolean backPaginate(ApiCallback<Integer> callback);
/**
* Request older messages.
*
* @param eventCount number of events we want to retrieve
* @param callback callback to implement to be informed that the pagination request has been completed. Can be null.
* @return true if request starts
*/
boolean backPaginate(int eventCount, ApiCallback<Integer> callback);
/**
* Request older messages.
*
* @param eventCount number of events we want to retrieve
* @param useCachedOnly to use the cached events list only (i.e no request will be triggered)
* @param callback callback to implement to be informed that the pagination request has been completed. Can be null.
* @return true if request starts
*/
boolean backPaginate(int eventCount, boolean useCachedOnly, ApiCallback<Integer> callback);
/**
* Request newer messages.
*
* @param callback callback to implement to be informed that the pagination request has been completed. Can be null.
* @return true if request starts
*/
boolean forwardPaginate(ApiCallback<Integer> callback);
/**
* Trigger a pagination in the expected direction.
*
* @param direction the direction.
* @param callback the callback.
* @return true if the operation succeeds
*/
boolean paginate(Direction direction, ApiCallback<Integer> callback);
/**
* Cancel any pending pagination requests
*/
void cancelPaginationRequests();
/**
* Reset the pagination timeline and start loading the context around its `initialEventId`.
* The retrieved (backwards and forwards) events will be sent to registered listeners.
*
* @param limit the maximum number of messages to get around the initial event.
* @param callback the operation callback
*/
void resetPaginationAroundInitialEvent(int limit, ApiCallback<Void> callback);
/**
* Add an events listener.
*
* @param listener the listener to add.
*/
void addEventTimelineListener(Listener listener);
/**
* Remove an events listener.
*
* @param listener the listener to remove.
*/
void removeEventTimelineListener(Listener listener);
/**
* The direction from which an incoming event is considered.
*/
enum Direction {
/**
* Forwards when the event is added to the end of the timeline.
* These events come from the /sync stream or from forwards pagination.
*/
FORWARDS,
/**
* Backwards when the event is added to the start of the timeline.
* These events come from a back pagination.
*/
BACKWARDS
}
interface Listener {
/**
* Call when an event has been handled in the timeline.
*
* @param event the event.
* @param direction the direction.
* @param roomState the room state
*/
void onEvent(Event event, Direction direction, RoomState roomState);
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,990 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.data.timeline;
import android.os.AsyncTask;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import im.vector.matrix.android.internal.legacy.MXDataHandler;
import im.vector.matrix.android.internal.legacy.data.Room;
import im.vector.matrix.android.internal.legacy.data.RoomState;
import im.vector.matrix.android.internal.legacy.data.RoomSummary;
import im.vector.matrix.android.internal.legacy.data.store.IMXStore;
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
import im.vector.matrix.android.internal.legacy.rest.callback.SimpleApiCallback;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import im.vector.matrix.android.internal.legacy.rest.model.EventContext;
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
import im.vector.matrix.android.internal.legacy.rest.model.TokensChunkEvents;
import im.vector.matrix.android.internal.legacy.rest.model.sync.InvitedRoomSync;
import im.vector.matrix.android.internal.legacy.rest.model.sync.RoomSync;
import im.vector.matrix.android.internal.legacy.util.FilterUtil;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* A private implementation of EventTimeline interface. It's not exposed as you don't have to directly instantiate it.
* Should be instantiated through EventTimelineFactory.
*/
class MXEventTimeline implements EventTimeline {
private static final String LOG_TAG = MXEventTimeline.class.getSimpleName();
/**
* The initial event id used to initialise the timeline.
* null in case of live timeline.
*/
private String mInitialEventId;
/**
* Indicate if this timeline is a live one.
*/
private boolean mIsLiveTimeline;
/**
* The associated room.
*/
private final Room mRoom;
/**
* the room Id
*/
private String mRoomId;
/**
* The store.
*/
private IMXStore mStore;
/**
* MXStore does only back pagination. So, the forward pagination token for
* past timelines is managed locally.
*/
private String mForwardsPaginationToken;
private boolean mHasReachedHomeServerForwardsPaginationEnd;
/**
* The data handler : used to retrieve data from the store or to trigger REST requests.
*/
private MXDataHandler mDataHandler;
/**
* Pending request statuses
*/
private boolean mIsBackPaginating = false;
private boolean mIsForwardPaginating = false;
/**
* true if the back history has been retrieved.
*/
public boolean mCanBackPaginate = true;
/**
* true if the last back chunck has been received
*/
private boolean mIsLastBackChunk;
/**
* the server provides a token even for the first room message (which should never change it is the creator message).
* so requestHistory always triggers a remote request which returns an empty json.
* try to avoid such behaviour
*/
private String mBackwardTopToken = "not yet found";
// true when the current timeline is an historical one
private boolean mIsHistorical;
/**
* Unique identifier
*/
private final String mTimelineId = System.currentTimeMillis() + "";
/**
* * This class handles storing a live room event in a dedicated store.
*/
private final TimelineEventSaver mTimelineEventSaver;
/**
* This class is responsible for holding the state and backState of a room timeline
*/
private final TimelineStateHolder mStateHolder;
/**
* This class handle the timeline event listeners
*/
private final TimelineEventListeners mEventListeners;
/**
* This class is responsible for handling events coming down from the event stream.
*/
private final TimelineLiveEventHandler mLiveEventHandler;
/**
* Constructor with package visibility. Creation should be done through EventTimelineFactory
*
* @param store the store associated (in case of past timeline, the store is memory only)
* @param dataHandler the dataHandler
* @param room the room
* @param roomId the room id
* @param eventId the eventId
* @param isLive true if the timeline is a live one
*/
MXEventTimeline(@NonNull final IMXStore store,
@NonNull final MXDataHandler dataHandler,
@NonNull final Room room,
@NonNull final String roomId,
@Nullable final String eventId,
final boolean isLive) {
mIsLiveTimeline = isLive;
mInitialEventId = eventId;
mDataHandler = dataHandler;
mRoom = room;
mRoomId = roomId;
mStore = store;
mEventListeners = new TimelineEventListeners();
mStateHolder = new TimelineStateHolder(mDataHandler, mStore, roomId);
final StateEventRedactionChecker stateEventRedactionChecker = new StateEventRedactionChecker(this, mStateHolder);
mTimelineEventSaver = new TimelineEventSaver(mStore, mRoom, mStateHolder);
final TimelinePushWorker timelinePushWorker = new TimelinePushWorker(mDataHandler);
mLiveEventHandler = new TimelineLiveEventHandler(this,
mTimelineEventSaver,
stateEventRedactionChecker,
timelinePushWorker,
mStateHolder,
mEventListeners);
}
/**
* Defines that the current timeline is an historical one
*
* @param isHistorical true when the current timeline is an historical one
*/
@Override
public void setIsHistorical(boolean isHistorical) {
mIsHistorical = isHistorical;
}
/**
* Returns true if the current timeline is an historical one
*/
@Override
public boolean isHistorical() {
return mIsHistorical;
}
/*
* @return the unique identifier
*/
@Override
public String getTimelineId() {
return mTimelineId;
}
/**
* @return the dedicated room
*/
@Override
public Room getRoom() {
return mRoom;
}
/**
* @return the used store
*/
@Override
public IMXStore getStore() {
return mStore;
}
/**
* @return the initial event id.
*/
@Override
public String getInitialEventId() {
return mInitialEventId;
}
/**
* @return true if this timeline is the live one
*/
@Override
public boolean isLiveTimeline() {
return mIsLiveTimeline;
}
/**
* Get whether we are at the end of the message stream
*
* @return true if end has been reached
*/
@Override
public boolean hasReachedHomeServerForwardsPaginationEnd() {
return mHasReachedHomeServerForwardsPaginationEnd;
}
/**
* Reset the back state so that future history requests start over from live.
* Must be called when opening a room if interested in history.
*/
@Override
public void initHistory() {
final RoomState backState = getState().deepCopy();
setBackState(backState);
mCanBackPaginate = true;
mIsBackPaginating = false;
mIsForwardPaginating = false;
// sanity check
if (null != mDataHandler && null != mDataHandler.getDataRetriever()) {
mDataHandler.resetReplayAttackCheckInTimeline(getTimelineId());
mDataHandler.getDataRetriever().cancelHistoryRequests(mRoomId);
}
}
/**
* @return The state of the room at the top most recent event of the timeline.
*/
@Override
public RoomState getState() {
return mStateHolder.getState();
}
/**
* Update the state.
*
* @param state the new state.
*/
@Override
public void setState(RoomState state) {
mStateHolder.setState(state);
}
/**
* Update the backState.
*
* @param state the new backState.
*/
private void setBackState(RoomState state) {
mStateHolder.setBackState(state);
}
/**
* @return the backState.
*/
private RoomState getBackState() {
return mStateHolder.getBackState();
}
/**
* Lock over the backPaginate process
*
* @param canBackPaginate the state of the lock (true/false)
*/
protected void setCanBackPaginate(final boolean canBackPaginate) {
mCanBackPaginate = canBackPaginate;
}
/**
* Make a deep copy or the dedicated state.
*
* @param direction the room state direction to deep copy.
*/
private void deepCopyState(Direction direction) {
mStateHolder.deepCopyState(direction);
}
/**
* Process a state event to keep the internal live and back states up to date.
*
* @param event the state event
* @param direction the direction; ie. forwards for live state, backwards for back state
* @return true if the event has been processed.
*/
private boolean processStateEvent(Event event, Direction direction) {
return mStateHolder.processStateEvent(event, direction);
}
/**
* Handle the invitation room events
*
* @param invitedRoomSync the invitation room events.
*/
@Override
public void handleInvitedRoomSync(InvitedRoomSync invitedRoomSync) {
final TimelineInvitedRoomSyncHandler invitedRoomSyncHandler = new TimelineInvitedRoomSyncHandler(mRoom, mLiveEventHandler, invitedRoomSync);
invitedRoomSyncHandler.handle();
}
/**
* Manage the joined room events.
*
* @param roomSync the roomSync.
* @param isGlobalInitialSync true if the sync has been triggered by a global initial sync
*/
@Override
public void handleJoinedRoomSync(@NonNull final RoomSync roomSync, final boolean isGlobalInitialSync) {
final TimelineJoinRoomSyncHandler joinRoomSyncHandler = new TimelineJoinRoomSyncHandler(this,
roomSync,
mStateHolder,
mLiveEventHandler,
isGlobalInitialSync);
joinRoomSyncHandler.handle();
}
/**
* Store an outgoing event.
*
* @param event the event to store
*/
@Override
public void storeOutgoingEvent(Event event) {
if (mIsLiveTimeline) {
storeEvent(event);
}
}
/**
* Store the event and update the dedicated room summary
*
* @param event the event to store
*/
private void storeEvent(Event event) {
mTimelineEventSaver.storeEvent(event);
}
//================================================================================
// History request
//================================================================================
private static final int MAX_EVENT_COUNT_PER_PAGINATION = 30;
// the storage events are buffered to provide a small bunch of events
// the storage can provide a big bunch which slows down the UI.
public class SnapshotEvent {
public final Event mEvent;
public final RoomState mState;
public SnapshotEvent(Event event, RoomState state) {
mEvent = event;
mState = state;
}
}
// avoid adding to many events
// the room history request can provide more than expected event.
private final List<SnapshotEvent> mSnapshotEvents = new ArrayList<>();
/**
* Send MAX_EVENT_COUNT_PER_PAGINATION events to the caller.
*
* @param maxEventCount the max event count
* @param callback the callback.
*/
private void manageBackEvents(int maxEventCount, final ApiCallback<Integer> callback) {
// check if the SDK was not logged out
if (!mDataHandler.isAlive()) {
Log.d(LOG_TAG, "manageEvents : mDataHandler is not anymore active.");
return;
}
int count = Math.min(mSnapshotEvents.size(), maxEventCount);
Event latestSupportedEvent = null;
for (int i = 0; i < count; i++) {
SnapshotEvent snapshotedEvent = mSnapshotEvents.get(0);
// in some cases, there is no displayed summary
// https://github.com/vector-im/vector-android/pull/354
if (null == latestSupportedEvent && RoomSummary.isSupportedEvent(snapshotedEvent.mEvent)) {
latestSupportedEvent = snapshotedEvent.mEvent;
}
mSnapshotEvents.remove(0);
mEventListeners.onEvent(snapshotedEvent.mEvent, Direction.BACKWARDS, snapshotedEvent.mState);
}
// https://github.com/vector-im/vector-android/pull/354
// defines a new summary if the known is not supported
RoomSummary summary = mStore.getSummary(mRoomId);
if (null != latestSupportedEvent && (null == summary || !RoomSummary.isSupportedEvent(summary.getLatestReceivedEvent()))) {
mStore.storeSummary(new RoomSummary(null, latestSupportedEvent, getState(), mDataHandler.getUserId()));
}
Log.d(LOG_TAG, "manageEvents : commit");
mStore.commit();
if (mSnapshotEvents.size() < MAX_EVENT_COUNT_PER_PAGINATION && mIsLastBackChunk) {
mCanBackPaginate = false;
}
mIsBackPaginating = false;
if (callback != null) {
try {
callback.onSuccess(count);
} catch (Exception e) {
Log.e(LOG_TAG, "requestHistory exception " + e.getMessage(), e);
}
}
}
/**
* Add some events in a dedicated direction.
*
* @param events the events list
* @param stateEvents the received state events (in case of lazy loading of room members)
* @param direction the direction
*/
private void addPaginationEvents(List<Event> events,
@Nullable List<Event> stateEvents,
Direction direction) {
RoomSummary summary = mStore.getSummary(mRoomId);
boolean shouldCommitStore = false;
// Process additional state events (this happens in case of lazy loading)
if (stateEvents != null) {
for (Event stateEvent : stateEvents) {
if (direction == Direction.BACKWARDS) {
// Enrich the timeline root state with the additional state events observed during back pagination
processStateEvent(stateEvent, Direction.FORWARDS);
}
processStateEvent(stateEvent, direction);
}
}
// the backward events have a dedicated management to avoid providing too many events for each request
for (Event event : events) {
boolean processedEvent = true;
if (event.stateKey != null) {
deepCopyState(direction);
processedEvent = processStateEvent(event, direction);
}
// Decrypt event if necessary
mDataHandler.decryptEvent(event, getTimelineId());
if (processedEvent) {
// warn the listener only if the message is processed.
// it should avoid duplicated events.
if (direction == Direction.BACKWARDS) {
if (mIsLiveTimeline) {
// update the summary is the event has been received after the oldest known event
// it might happen after a timeline update (hole in the chat history)
if (null != summary
&& (null == summary.getLatestReceivedEvent()
|| event.isValidOriginServerTs()
&& summary.getLatestReceivedEvent().originServerTs < event.originServerTs
&& RoomSummary.isSupportedEvent(event))) {
summary.setLatestReceivedEvent(event, getState());
mStore.storeSummary(summary);
shouldCommitStore = true;
}
}
mSnapshotEvents.add(new SnapshotEvent(event, getBackState()));
// onEvent will be called in manageBackEvents
}
}
}
if (shouldCommitStore) {
mStore.commit();
}
}
/**
* Add some events in a dedicated direction.
*
* @param events the events list
* @param stateEvents the received state events (in case of lazy loading of room members)
* @param direction the direction
* @param callback the callback.
*/
private void addPaginationEvents(final List<Event> events,
@Nullable final List<Event> stateEvents,
final Direction direction,
final ApiCallback<Integer> callback) {
AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
addPaginationEvents(events, stateEvents, direction);
return null;
}
@Override
protected void onPostExecute(Void args) {
if (direction == Direction.BACKWARDS) {
manageBackEvents(MAX_EVENT_COUNT_PER_PAGINATION, callback);
} else {
for (Event event : events) {
mEventListeners.onEvent(event, Direction.FORWARDS, getState());
}
if (null != callback) {
callback.onSuccess(events.size());
}
}
}
};
try {
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} catch (final Exception e) {
Log.e(LOG_TAG, "## addPaginationEvents() failed " + e.getMessage(), e);
task.cancel(true);
new android.os.Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
if (null != callback) {
callback.onUnexpectedError(e);
}
}
});
}
}
/**
* Tells if a back pagination can be triggered.
*
* @return true if a back pagination can be triggered.
*/
@Override
public boolean canBackPaginate() {
// One at a time please
return !mIsBackPaginating
// history_visibility flag management
&& getState().canBackPaginate(mRoom.isJoined(), mRoom.isInvited())
// If we have already reached the end of history
&& mCanBackPaginate
// If the room is not finished being set up
&& mRoom.isReady();
}
/**
* Request older messages.
*
* @param callback the asynchronous callback
* @return true if request starts
*/
@Override
public boolean backPaginate(final ApiCallback<Integer> callback) {
return backPaginate(MAX_EVENT_COUNT_PER_PAGINATION, callback);
}
/**
* Request older messages.
*
* @param eventCount number of events we want to retrieve
* @param callback callback to implement to be informed that the pagination request has been completed. Can be null.
* @return true if request starts
*/
@Override
public boolean backPaginate(final int eventCount, final ApiCallback<Integer> callback) {
return backPaginate(eventCount, false, callback);
}
/**
* Request older messages.
*
* @param eventCount number of events we want to retrieve
* @param useCachedOnly to use the cached events list only (i.e no request will be triggered)
* @param callback callback to implement to be informed that the pagination request has been completed. Can be null.
* @return true if request starts
*/
@Override
public boolean backPaginate(final int eventCount, final boolean useCachedOnly, final ApiCallback<Integer> callback) {
if (!canBackPaginate()) {
Log.d(LOG_TAG, "cannot requestHistory " + mIsBackPaginating + " " + !getState().canBackPaginate(mRoom.isJoined(), mRoom.isInvited())
+ " " + !mCanBackPaginate + " " + !mRoom.isReady());
return false;
}
Log.d(LOG_TAG, "backPaginate starts");
// restart the pagination
if (null == getBackState().getToken()) {
mSnapshotEvents.clear();
}
final String fromBackToken = getBackState().getToken();
mIsBackPaginating = true;
// enough buffered data
if (useCachedOnly
|| mSnapshotEvents.size() >= eventCount
|| TextUtils.equals(fromBackToken, mBackwardTopToken)
|| TextUtils.equals(fromBackToken, Event.PAGINATE_BACK_TOKEN_END)) {
mIsLastBackChunk = TextUtils.equals(fromBackToken, mBackwardTopToken) || TextUtils.equals(fromBackToken, Event.PAGINATE_BACK_TOKEN_END);
final android.os.Handler handler = new android.os.Handler(Looper.getMainLooper());
final int maxEventsCount;
if (useCachedOnly) {
Log.d(LOG_TAG, "backPaginate : load " + mSnapshotEvents.size() + "cached events list");
maxEventsCount = Math.min(mSnapshotEvents.size(), eventCount);
} else if (mSnapshotEvents.size() >= eventCount) {
Log.d(LOG_TAG, "backPaginate : the events are already loaded.");
maxEventsCount = eventCount;
} else {
Log.d(LOG_TAG, "backPaginate : reach the history top");
maxEventsCount = eventCount;
}
// call the callback with a delay
// to reproduce the same behaviour as a network request.
Runnable r = new Runnable() {
@Override
public void run() {
handler.postDelayed(new Runnable() {
public void run() {
manageBackEvents(maxEventsCount, callback);
}
}, 0);
}
};
Thread t = new Thread(r);
t.start();
return true;
}
mDataHandler.getDataRetriever().backPaginate(mStore, mRoomId, getBackState().getToken(), eventCount, mDataHandler.isLazyLoadingEnabled(),
new SimpleApiCallback<TokensChunkEvents>(callback) {
@Override
public void onSuccess(TokensChunkEvents response) {
if (mDataHandler.isAlive()) {
if (null != response.chunk) {
Log.d(LOG_TAG, "backPaginate : " + response.chunk.size() + " events are retrieved.");
} else {
Log.d(LOG_TAG, "backPaginate : there is no event");
}
mIsLastBackChunk = null != response.chunk
&& 0 == response.chunk.size()
&& TextUtils.equals(response.end, response.start)
|| null == response.end;
if (mIsLastBackChunk && null != response.end) {
// save its token to avoid useless request
mBackwardTopToken = fromBackToken;
} else {
// the server returns a null pagination token when there is no more available data
if (null == response.end) {
getBackState().setToken(Event.PAGINATE_BACK_TOKEN_END);
} else {
getBackState().setToken(response.end);
}
}
addPaginationEvents(null == response.chunk ? new ArrayList<Event>() : response.chunk,
response.stateEvents,
Direction.BACKWARDS,
callback);
} else {
Log.d(LOG_TAG, "mDataHandler is not active.");
}
}
@Override
public void onMatrixError(MatrixError e) {
Log.d(LOG_TAG, "backPaginate onMatrixError");
// When we've retrieved all the messages from a room, the pagination token is some invalid value
if (MatrixError.UNKNOWN.equals(e.errcode)) {
mCanBackPaginate = false;
}
mIsBackPaginating = false;
super.onMatrixError(e);
}
@Override
public void onNetworkError(Exception e) {
Log.d(LOG_TAG, "backPaginate onNetworkError");
mIsBackPaginating = false;
super.onNetworkError(e);
}
@Override
public void onUnexpectedError(Exception e) {
Log.d(LOG_TAG, "backPaginate onUnexpectedError");
mIsBackPaginating = false;
super.onUnexpectedError(e);
}
});
return true;
}
/**
* Request newer messages.
*
* @param callback callback to implement to be informed that the pagination request has been completed. Can be null.
* @return true if request starts
*/
@Override
public boolean forwardPaginate(final ApiCallback<Integer> callback) {
if (mIsLiveTimeline) {
Log.d(LOG_TAG, "Cannot forward paginate on Live timeline");
return false;
}
if (mIsForwardPaginating || mHasReachedHomeServerForwardsPaginationEnd) {
Log.d(LOG_TAG, "forwardPaginate " + mIsForwardPaginating
+ " mHasReachedHomeServerForwardsPaginationEnd " + mHasReachedHomeServerForwardsPaginationEnd);
return false;
}
mIsForwardPaginating = true;
mDataHandler.getDataRetriever().paginate(mStore, mRoomId, mForwardsPaginationToken, Direction.FORWARDS, mDataHandler.isLazyLoadingEnabled(),
new SimpleApiCallback<TokensChunkEvents>(callback) {
@Override
public void onSuccess(TokensChunkEvents response) {
if (mDataHandler.isAlive()) {
Log.d(LOG_TAG, "forwardPaginate : " + response.chunk.size() + " are retrieved.");
mHasReachedHomeServerForwardsPaginationEnd = 0 == response.chunk.size() && TextUtils.equals(response.end, response.start);
mForwardsPaginationToken = response.end;
addPaginationEvents(response.chunk,
response.stateEvents,
Direction.FORWARDS,
callback);
mIsForwardPaginating = false;
} else {
Log.d(LOG_TAG, "mDataHandler is not active.");
}
}
@Override
public void onMatrixError(MatrixError e) {
mIsForwardPaginating = false;
super.onMatrixError(e);
}
@Override
public void onNetworkError(Exception e) {
mIsForwardPaginating = false;
super.onNetworkError(e);
}
@Override
public void onUnexpectedError(Exception e) {
mIsForwardPaginating = false;
super.onUnexpectedError(e);
}
});
return true;
}
/**
* Trigger a pagination in the expected direction.
*
* @param direction the direction.
* @param callback the callback.
* @return true if the operation succeeds
*/
@Override
public boolean paginate(Direction direction, final ApiCallback<Integer> callback) {
if (Direction.BACKWARDS == direction) {
return backPaginate(callback);
} else {
return forwardPaginate(callback);
}
}
/**
* Cancel any pending pagination requests
*/
@Override
public void cancelPaginationRequests() {
mDataHandler.getDataRetriever().cancelHistoryRequests(mRoomId);
mIsBackPaginating = false;
mIsForwardPaginating = false;
}
//==============================================================================================================
// pagination methods
//==============================================================================================================
/**
* Reset the pagination timeline and start loading the context around its `initialEventId`.
* The retrieved (backwards and forwards) events will be sent to registered listeners.
*
* @param limit the maximum number of messages to get around the initial event.
* @param callback the operation callback
*/
@Override
public void resetPaginationAroundInitialEvent(final int limit, final ApiCallback<Void> callback) {
// Reset the store
mStore.deleteRoomData(mRoomId);
mDataHandler.resetReplayAttackCheckInTimeline(getTimelineId());
mForwardsPaginationToken = null;
mHasReachedHomeServerForwardsPaginationEnd = false;
mDataHandler.getDataRetriever()
.getRoomsRestClient()
.getContextOfEvent(mRoomId, mInitialEventId, limit, FilterUtil.createRoomEventFilter(mDataHandler.isLazyLoadingEnabled()),
new SimpleApiCallback<EventContext>(callback) {
@Override
public void onSuccess(final EventContext eventContext) {
AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
// the state is the one after the latest event of the chunk i.e. the last message of eventContext.eventsAfter
for (Event event : eventContext.state) {
processStateEvent(event, Direction.FORWARDS);
}
// init the room states
initHistory();
// build the events list
List<Event> events = new ArrayList<>();
Collections.reverse(eventContext.eventsAfter);
events.addAll(eventContext.eventsAfter);
events.add(eventContext.event);
events.addAll(eventContext.eventsBefore);
// add events after
addPaginationEvents(events, null, Direction.BACKWARDS);
return null;
}
@Override
protected void onPostExecute(Void args) {
// create dummy forward events list
// to center the selected event id
// else if might be out of screen
List<SnapshotEvent> nextSnapshotEvents = new ArrayList<>(mSnapshotEvents.subList(0, (mSnapshotEvents.size() + 1) / 2));
// put in the right order
Collections.reverse(nextSnapshotEvents);
// send them one by one
for (SnapshotEvent snapshotEvent : nextSnapshotEvents) {
mSnapshotEvents.remove(snapshotEvent);
mEventListeners.onEvent(snapshotEvent.mEvent, Direction.FORWARDS, snapshotEvent.mState);
}
// init the tokens
getBackState().setToken(eventContext.start);
mForwardsPaginationToken = eventContext.end;
// send the back events to complete pagination
manageBackEvents(MAX_EVENT_COUNT_PER_PAGINATION, new ApiCallback<Integer>() {
@Override
public void onSuccess(Integer info) {
Log.d(LOG_TAG, "addPaginationEvents succeeds");
}
@Override
public void onNetworkError(Exception e) {
Log.e(LOG_TAG, "addPaginationEvents failed " + e.getMessage(), e);
}
@Override
public void onMatrixError(MatrixError e) {
Log.e(LOG_TAG, "addPaginationEvents failed " + e.getMessage());
}
@Override
public void onUnexpectedError(Exception e) {
Log.e(LOG_TAG, "addPaginationEvents failed " + e.getMessage(), e);
}
});
// everything is done
callback.onSuccess(null);
}
};
try {
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} catch (final Exception e) {
Log.e(LOG_TAG, "## resetPaginationAroundInitialEvent() failed " + e.getMessage(), e);
task.cancel(true);
new android.os.Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
if (callback != null) {
callback.onUnexpectedError(e);
}
}
});
}
}
});
}
//==============================================================================================================
// onEvent listener management.
//==============================================================================================================
/**
* Add an events listener.
*
* @param listener the listener to add.
*/
@Override
public void addEventTimelineListener(@Nullable final Listener listener) {
mEventListeners.add(listener);
}
/**
* Remove an events listener.
*
* @param listener the listener to remove.
*/
@Override
public void removeEventTimelineListener(@Nullable final Listener listener) {
mEventListeners.remove(listener);
}
}

View file

@ -0,0 +1,175 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.data.timeline;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import im.vector.matrix.android.internal.legacy.MXDataHandler;
import im.vector.matrix.android.internal.legacy.data.Room;
import im.vector.matrix.android.internal.legacy.data.RoomState;
import im.vector.matrix.android.internal.legacy.data.store.IMXStore;
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
import im.vector.matrix.android.internal.legacy.rest.callback.SimpleApiCallback;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
import im.vector.matrix.android.internal.legacy.rest.model.RoomMember;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.util.List;
import javax.annotation.Nonnull;
/**
* This class is responsible of checking state events redaction.
*/
class StateEventRedactionChecker {
private static final String LOG_TAG = StateEventRedactionChecker.class.getSimpleName();
private final EventTimeline mEventTimeline;
private final TimelineStateHolder mTimelineStateHolder;
StateEventRedactionChecker(@NonNull final EventTimeline eventTimeline,
@NonNull final TimelineStateHolder timelineStateHolder) {
mEventTimeline = eventTimeline;
mTimelineStateHolder = timelineStateHolder;
}
/**
* Redaction of a state event might require to reload the timeline
* because the room states has to be updated.
*
* @param redactionEvent the redaction event
*/
public void checkStateEventRedaction(@NonNull final Event redactionEvent) {
final IMXStore store = mEventTimeline.getStore();
final Room room = mEventTimeline.getRoom();
final MXDataHandler dataHandler = room.getDataHandler();
final String roomId = room.getRoomId();
final String eventId = redactionEvent.getRedactedEventId();
final RoomState state = mTimelineStateHolder.getState();
Log.d(LOG_TAG, "checkStateEventRedaction of event " + eventId);
// check if the state events is locally known
state.getStateEvents(store, null, new SimpleApiCallback<List<Event>>() {
@Override
public void onSuccess(List<Event> stateEvents) {
// Check whether the current room state depends on this redacted event.
boolean isFound = false;
for (int index = 0; index < stateEvents.size(); index++) {
Event stateEvent = stateEvents.get(index);
if (TextUtils.equals(stateEvent.eventId, eventId)) {
Log.d(LOG_TAG, "checkStateEventRedaction: the current room state has been modified by the event redaction");
// remove expected keys
stateEvent.prune(redactionEvent);
stateEvents.set(index, stateEvent);
// digest the updated state
mTimelineStateHolder.processStateEvent(stateEvent, EventTimeline.Direction.FORWARDS);
isFound = true;
break;
}
}
if (!isFound) {
// Else try to find the redacted event among members which
// are stored apart from other state events
// Reason: The membership events are not anymore stored in the application store
// until we have found a way to improve the way they are stored.
// It used to have many out of memory errors because they are too many stored small memory objects.
// see https://github.com/matrix-org/matrix-android-sdk/issues/196
// Note: if lazy loading is on, getMemberByEventId() can return null, but it is ok, because we just want to update our cache
RoomMember member = state.getMemberByEventId(eventId);
if (member != null) {
Log.d(LOG_TAG, "checkStateEventRedaction: the current room members list has been modified by the event redaction");
// the android SDK does not store stock member events but a representation of them, RoomMember.
// Prune this representation
member.prune();
isFound = true;
}
}
if (isFound) {
store.storeLiveStateForRoom(roomId);
// warn that there was a flush
mEventTimeline.initHistory();
dataHandler.onRoomFlush(roomId);
} else {
Log.d(LOG_TAG, "checkStateEventRedaction: the redacted event is unknown. Fetch it from the homeserver");
checkStateEventRedactionWithHomeserver(dataHandler, roomId, eventId);
}
}
});
}
/**
* Check with the HS whether the redacted event impacts the room data we have locally.
* If yes, local data must be pruned.
*
* @param eventId the redacted event id
*/
private void checkStateEventRedactionWithHomeserver(@Nonnull final MXDataHandler dataHandler,
@Nonnull final String roomId,
@Nonnull final String eventId) {
Log.d(LOG_TAG, "checkStateEventRedactionWithHomeserver on event Id " + eventId);
// We need to figure out if this redacted event is a room state in the past.
// If yes, we must prune the `prev_content` of the state event that replaced it.
// Indeed, redacted information shouldn't spontaneously appear when you backpaginate...
// TODO: This is no more implemented (see https://github.com/vector-im/riot-ios/issues/443).
// The previous implementation based on a room initial sync was too heavy server side
// and has been removed.
if (!TextUtils.isEmpty(eventId)) {
Log.d(LOG_TAG, "checkStateEventRedactionWithHomeserver : retrieving the event");
dataHandler.getDataRetriever().getRoomsRestClient().getEvent(roomId, eventId, new ApiCallback<Event>() {
@Override
public void onSuccess(Event event) {
if (null != event && null != event.stateKey) {
Log.d(LOG_TAG, "checkStateEventRedactionWithHomeserver : the redacted event is a state event in the past." +
" TODO: prune prev_content of the new state event");
} else {
Log.d(LOG_TAG, "checkStateEventRedactionWithHomeserver : the redacted event is a not state event -> job is done");
}
}
@Override
public void onNetworkError(Exception e) {
Log.e(LOG_TAG, "checkStateEventRedactionWithHomeserver : failed to retrieved the redacted event: onNetworkError " + e.getMessage(), e);
}
@Override
public void onMatrixError(MatrixError e) {
Log.e(LOG_TAG, "checkStateEventRedactionWithHomeserver : failed to retrieved the redacted event: onNetworkError " + e.getMessage());
}
@Override
public void onUnexpectedError(Exception e) {
Log.e(LOG_TAG, "checkStateEventRedactionWithHomeserver : failed to retrieved the redacted event: onNetworkError " + e.getMessage(), e);
}
});
}
}
}

View file

@ -0,0 +1,105 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.data.timeline;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import im.vector.matrix.android.internal.legacy.data.RoomState;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.util.ArrayList;
import java.util.List;
/**
* Handle the timeline event listeners
* Is responsible for dispatching events
*/
class TimelineEventListeners {
private static final String LOG_TAG = TimelineEventListeners.class.getSimpleName();
// The inner listeners
private final List<EventTimeline.Listener> mListeners = new ArrayList<>();
/**
* Add an events listener.
*
* @param listener the listener to add.
*/
public void add(@Nullable final EventTimeline.Listener listener) {
if (listener != null) {
synchronized (this) {
if (!mListeners.contains(listener)) {
mListeners.add(listener);
}
}
}
}
/**
* Remove an events listener.
*
* @param listener the listener to remove.
*/
public void remove(@Nullable final EventTimeline.Listener listener) {
if (null != listener) {
synchronized (this) {
mListeners.remove(listener);
}
}
}
/**
* Dispatch the onEvent callback.
*
* @param event the event.
* @param direction the direction.
* @param roomState the roomState.
*/
public void onEvent(@NonNull final Event event,
@NonNull final EventTimeline.Direction direction,
@NonNull final RoomState roomState) {
// ensure that the listeners are called in the UI thread
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
final List<EventTimeline.Listener> listeners;
synchronized (this) {
listeners = new ArrayList<>(mListeners);
}
for (EventTimeline.Listener listener : listeners) {
try {
listener.onEvent(event, direction, roomState);
} catch (Exception e) {
Log.e(LOG_TAG, "EventTimeline.onEvent " + listener + " crashes " + e.getMessage(), e);
}
}
} else {
final Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
onEvent(event, direction, roomState);
}
});
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -0,0 +1,298 @@
package im.vector.matrix.android.internal.legacy.data.timeline;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import im.vector.matrix.android.internal.legacy.MXDataHandler;
import im.vector.matrix.android.internal.legacy.data.Room;
import im.vector.matrix.android.internal.legacy.data.RoomState;
import im.vector.matrix.android.internal.legacy.data.RoomSummary;
import im.vector.matrix.android.internal.legacy.data.store.IMXStore;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import im.vector.matrix.android.internal.legacy.rest.model.RoomMember;
import im.vector.matrix.android.internal.legacy.rest.model.sync.RoomSync;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* This class is responsible for handling a Join RoomSync
*/
class TimelineJoinRoomSyncHandler {
private static final String LOG_TAG = TimelineJoinRoomSyncHandler.class.getSimpleName();
private final MXEventTimeline mEventTimeline;
private final RoomSync mRoomSync;
private final TimelineStateHolder mTimelineStateHolder;
private final TimelineLiveEventHandler mTimelineLiveEventHandler;
private final boolean mIsGlobalInitialSync;
TimelineJoinRoomSyncHandler(@NonNull final MXEventTimeline eventTimeline,
@NonNull final RoomSync roomSync,
@NonNull final TimelineStateHolder timelineStateHolder,
@NonNull final TimelineLiveEventHandler timelineLiveEventHandler,
final boolean isGlobalInitialSync) {
mEventTimeline = eventTimeline;
mRoomSync = roomSync;
mTimelineStateHolder = timelineStateHolder;
mTimelineLiveEventHandler = timelineLiveEventHandler;
mIsGlobalInitialSync = isGlobalInitialSync;
}
public void handle() {
final IMXStore store = mEventTimeline.getStore();
final Room room = mEventTimeline.getRoom();
final MXDataHandler dataHandler = room.getDataHandler();
final String roomId = room.getRoomId();
final String myUserId = dataHandler.getMyUser().user_id;
final RoomMember selfMember = mTimelineStateHolder.getState().getMember(myUserId);
final RoomSummary currentSummary = store.getSummary(roomId);
final String membership = selfMember != null ? selfMember.membership : null;
final boolean isRoomInitialSync = membership == null || TextUtils.equals(membership, RoomMember.MEMBERSHIP_INVITE);
// Check whether the room was pending on an invitation.
if (RoomMember.MEMBERSHIP_INVITE.equals(membership)) {
// Reset the storage of this room. An initial sync of the room will be done with the provided 'roomSync'.
cleanInvitedRoom(store, roomId);
}
if (mRoomSync.state != null && mRoomSync.state.events != null && mRoomSync.state.events.size() > 0) {
handleRoomSyncState(room, store, isRoomInitialSync);
}
// Handle now timeline.events, the room state is updated during this step too (Note: timeline events are in chronological order)
if (mRoomSync.timeline != null) {
handleRoomSyncTimeline(store, myUserId, roomId, currentSummary, isRoomInitialSync);
}
if (isRoomInitialSync) {
// any request history can be triggered by now.
room.setReadyState(true);
} else if (mRoomSync.timeline != null && mRoomSync.timeline.limited) {
// Finalize initial sync
// The room has been synced with a limited timeline
dataHandler.onRoomFlush(roomId);
}
// the EventTimeLine is used when displaying a room preview
// so, the following items should only be called when it is a live one.
if (mEventTimeline.isLiveTimeline()) {
handleLiveTimeline(dataHandler, store, roomId, myUserId, currentSummary);
}
}
private void handleRoomSyncState(@NonNull final Room room,
@NonNull final IMXStore store,
final boolean isRoomInitialSync) {
if (isRoomInitialSync) {
Log.d(LOG_TAG, "##" + mRoomSync.state.events.size() + " events "
+ "for room " + room.getRoomId()
+ "in store " + store
);
}
// Build/Update first the room state corresponding to the 'start' of the timeline.
// Note: We consider it is not required to clone the existing room state here, because no notification is posted for these events.
if (room.getDataHandler().isAlive()) {
for (Event event : mRoomSync.state.events) {
try {
mTimelineStateHolder.processStateEvent(event, EventTimeline.Direction.FORWARDS);
} catch (Exception e) {
Log.e(LOG_TAG, "processStateEvent failed " + e.getMessage(), e);
}
}
room.setReadyState(true);
} else {
Log.e(LOG_TAG, "## mDataHandler.isAlive() is false");
}
// if it is an initial sync, the live state is initialized here
// so the back state must also be initialized
if (isRoomInitialSync) {
final RoomState state = mTimelineStateHolder.getState();
Log.d(LOG_TAG, "## handleJoinedRoomSync() : retrieve X " + state.getLoadedMembers().size() + " members for room " + room.getRoomId());
mTimelineStateHolder.setBackState(state.deepCopy());
}
}
private void cleanInvitedRoom(@NonNull final IMXStore store,
@NonNull final String roomId) {
Log.d(LOG_TAG, "clean invited room from the store " + roomId);
store.deleteRoomData(roomId);
mTimelineStateHolder.clear();
}
private void handleRoomSyncTimeline(@NonNull final IMXStore store,
@NonNull final String myUserId,
@NonNull final String roomId,
@Nullable final RoomSummary currentSummary,
final boolean isRoomInitialSync) {
if (mRoomSync.timeline.limited) {
if (!isRoomInitialSync) {
final RoomState state = mTimelineStateHolder.getState();
// There is a gap between known events and received events in this incremental sync.
// define a summary if some messages are left
// the unsent messages are often displayed messages.
final Event oldestEvent = store.getOldestEvent(roomId);
// Flush the existing messages for this room by keeping state events.
store.deleteAllRoomMessages(roomId, true);
if (oldestEvent != null) {
if (RoomSummary.isSupportedEvent(oldestEvent)) {
if (currentSummary != null) {
currentSummary.setLatestReceivedEvent(oldestEvent, state);
store.storeSummary(currentSummary);
} else {
store.storeSummary(new RoomSummary(null, oldestEvent, state, myUserId));
}
}
}
// Force a fetch of the loaded members the next time they will be requested
state.forceMembersRequest();
}
// if the prev batch is set to null
// it implies there is no more data on server side.
if (mRoomSync.timeline.prevBatch == null) {
mRoomSync.timeline.prevBatch = Event.PAGINATE_BACK_TOKEN_END;
}
// In case of limited timeline, update token where to start back pagination
store.storeBackToken(roomId, mRoomSync.timeline.prevBatch);
// reset the state back token
// because it does not make anymore sense
// by setting at null, the events cache will be cleared when a requesthistory will be called
mTimelineStateHolder.getBackState().setToken(null);
// reset the back paginate lock
mEventTimeline.setCanBackPaginate(true);
}
// any event ?
if (mRoomSync.timeline.events != null && !mRoomSync.timeline.events.isEmpty()) {
final List<Event> events = mRoomSync.timeline.events;
// save the back token
events.get(0).mToken = mRoomSync.timeline.prevBatch;
// Here the events are handled in forward direction (see [handleLiveEvent:]).
// They will be added at the end of the stored events, so we keep the chronological order.
for (Event event : events) {
// the roomId is not defined.
event.roomId = roomId;
try {
boolean isLimited = mRoomSync.timeline != null && mRoomSync.timeline.limited;
// digest the forward event
mTimelineLiveEventHandler.handleLiveEvent(event, !isLimited && !mIsGlobalInitialSync, !mIsGlobalInitialSync && !isRoomInitialSync);
} catch (Exception e) {
Log.e(LOG_TAG, "timeline event failed " + e.getMessage(), e);
}
}
}
}
private void handleLiveTimeline(@NonNull final MXDataHandler dataHandler,
@NonNull final IMXStore store,
@NonNull final String roomId,
@NonNull final String myUserId,
@Nullable final RoomSummary currentSummary) {
final RoomState state = mTimelineStateHolder.getState();
// check if the summary is defined
// after a sync, the room summary might not be defined because the latest message did not generate a room summary/
if (null != store.getRoom(roomId)) {
RoomSummary summary = store.getSummary(roomId);
// if there is no defined summary
// we have to create a new one
if (summary == null) {
// define a summary if some messages are left
// the unsent messages are often displayed messages.
final Event oldestEvent = store.getOldestEvent(roomId);
// if there is an oldest event, use it to set a summary
if (oldestEvent != null) {
// always defined a room summary else the room won't be displayed in the recents
store.storeSummary(new RoomSummary(null, oldestEvent, state, myUserId));
store.commit();
// if the event is not displayable
// back paginate until to find a valid one
if (!RoomSummary.isSupportedEvent(oldestEvent)) {
Log.e(LOG_TAG, "the room " + roomId + " has no valid summary, back paginate once to find a valid one");
}
}
// use the latest known event
else if (currentSummary != null) {
currentSummary.setLatestReceivedEvent(currentSummary.getLatestReceivedEvent(), state);
store.storeSummary(currentSummary);
store.commit();
}
// try to build a summary from the state events
else if (mRoomSync.state != null && mRoomSync.state.events != null && mRoomSync.state.events.size() > 0) {
final List<Event> events = new ArrayList<>(mRoomSync.state.events);
Collections.reverse(events);
for (Event event : events) {
event.roomId = roomId;
if (RoomSummary.isSupportedEvent(event)) {
summary = new RoomSummary(store.getSummary(roomId), event, state, myUserId);
store.storeSummary(summary);
store.commit();
break;
}
}
}
}
}
if (null != mRoomSync.unreadNotifications) {
int notifCount = 0;
int highlightCount = 0;
if (null != mRoomSync.unreadNotifications.highlightCount) {
highlightCount = mRoomSync.unreadNotifications.highlightCount;
}
if (null != mRoomSync.unreadNotifications.notificationCount) {
notifCount = mRoomSync.unreadNotifications.notificationCount;
}
if (notifCount != state.getNotificationCount() || state.getHighlightCount() != highlightCount) {
Log.d(LOG_TAG, "## handleJoinedRoomSync() : update room state notifs count for room id " + roomId
+ ": highlightCount " + highlightCount + " - notifCount " + notifCount);
state.setNotificationCount(notifCount);
state.setHighlightCount(highlightCount);
store.storeLiveStateForRoom(roomId);
dataHandler.onNotificationCountUpdate(roomId);
}
// some users reported that the summary notification counts were sometimes invalid
// so check roomstates and summaries separately
final RoomSummary summary = store.getSummary(roomId);
if (summary != null && (notifCount != summary.getNotificationCount() || summary.getHighlightCount() != highlightCount)) {
Log.d(LOG_TAG, "## handleJoinedRoomSync() : update room summary notifs count for room id " + roomId
+ ": highlightCount " + highlightCount + " - notifCount " + notifCount);
summary.setNotificationCount(notifCount);
summary.setHighlightCount(highlightCount);
store.flushSummary(summary);
dataHandler.onNotificationCountUpdate(roomId);
}
}
// TODO LazyLoading, maybe this should be done earlier, because nb of members can be usefull in the instruction above.
if (mRoomSync.roomSyncSummary != null) {
RoomSummary summary = store.getSummary(roomId);
if (summary == null) {
// Should never happen here
Log.e(LOG_TAG, "!!!!!!!!!!!!!!!!!!!!! RoomSummary is null !!!!!!!!!!!!!!!!!!!!!");
} else {
summary.setRoomSyncSummary(mRoomSync.roomSyncSummary);
store.flushSummary(summary);
}
}
}
}

View file

@ -0,0 +1,285 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.data.timeline;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import im.vector.matrix.android.internal.legacy.MXDataHandler;
import im.vector.matrix.android.internal.legacy.data.MyUser;
import im.vector.matrix.android.internal.legacy.data.Room;
import im.vector.matrix.android.internal.legacy.data.RoomState;
import im.vector.matrix.android.internal.legacy.data.RoomSummary;
import im.vector.matrix.android.internal.legacy.data.store.IMXStore;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import im.vector.matrix.android.internal.legacy.rest.model.EventContent;
import im.vector.matrix.android.internal.legacy.rest.model.RoomMember;
import im.vector.matrix.android.internal.legacy.util.EventDisplay;
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nonnull;
/**
* This class is responsible for handling live event
*/
class TimelineLiveEventHandler {
private static final String LOG_TAG = TimelineLiveEventHandler.class.getSimpleName();
private final MXEventTimeline mEventTimeline;
private final TimelineEventSaver mTimelineEventSaver;
private final StateEventRedactionChecker mStateEventRedactionChecker;
private final TimelinePushWorker mTimelinePushWorker;
private final TimelineStateHolder mTimelineStateHolder;
private final TimelineEventListeners mEventListeners;
TimelineLiveEventHandler(@Nonnull final MXEventTimeline eventTimeline,
@Nonnull final TimelineEventSaver timelineEventSaver,
@Nonnull final StateEventRedactionChecker stateEventRedactionChecker,
@Nonnull final TimelinePushWorker timelinePushWorker,
@NonNull final TimelineStateHolder timelineStateHolder,
@NonNull final TimelineEventListeners eventListeners) {
mEventTimeline = eventTimeline;
mTimelineEventSaver = timelineEventSaver;
mStateEventRedactionChecker = stateEventRedactionChecker;
mTimelinePushWorker = timelinePushWorker;
mTimelineStateHolder = timelineStateHolder;
mEventListeners = eventListeners;
}
/**
* Handle events coming down from the event stream.
*
* @param event the live event
* @param checkRedactedStateEvent set to true to check if it triggers a state event redaction
* @param withPush set to true to trigger pushes when it is required
*/
public void handleLiveEvent(@NonNull final Event event,
final boolean checkRedactedStateEvent,
final boolean withPush) {
final IMXStore store = mEventTimeline.getStore();
final Room room = mEventTimeline.getRoom();
final MXDataHandler dataHandler = room.getDataHandler();
final String timelineId = mEventTimeline.getTimelineId();
final MyUser myUser = dataHandler.getMyUser();
// Decrypt event if necessary
dataHandler.decryptEvent(event, timelineId);
// dispatch the call events to the calls manager
if (event.isCallEvent()) {
final RoomState roomState = mTimelineStateHolder.getState();
dataHandler.getCallsManager().handleCallEvent(store, event);
storeLiveRoomEvent(dataHandler, store, event, false);
// the candidates events are not tracked
// because the users don't need to see the peer exchanges.
if (!TextUtils.equals(event.getType(), Event.EVENT_TYPE_CALL_CANDIDATES)) {
// warn the listeners
// general listeners
dataHandler.onLiveEvent(event, roomState);
// timeline listeners
mEventListeners.onEvent(event, EventTimeline.Direction.FORWARDS, roomState);
}
// trigger pushes when it is required
if (withPush) {
mTimelinePushWorker.triggerPush(roomState, event);
}
} else {
final Event storedEvent = store.getEvent(event.eventId, event.roomId);
// avoid processing event twice
if (storedEvent != null) {
// an event has been echoed
if (storedEvent.getAge() == Event.DUMMY_EVENT_AGE) {
store.deleteEvent(storedEvent);
store.storeLiveRoomEvent(event);
store.commit();
Log.d(LOG_TAG, "handleLiveEvent : the event " + event.eventId + " in " + event.roomId + " has been echoed");
} else {
Log.d(LOG_TAG, "handleLiveEvent : the event " + event.eventId + " in " + event.roomId + " already exist.");
return;
}
}
// Room event
if (event.roomId != null) {
// check if the room has been joined
// the initial sync + the first requestHistory call is done here
// instead of being done in the application
if (Event.EVENT_TYPE_STATE_ROOM_MEMBER.equals(event.getType()) && TextUtils.equals(event.getSender(), dataHandler.getUserId())) {
EventContent eventContent = JsonUtils.toEventContent(event.getContentAsJsonObject());
EventContent prevEventContent = event.getPrevContent();
String prevMembership = null;
if (prevEventContent != null) {
prevMembership = prevEventContent.membership;
}
// if the membership keeps the same value "join".
// it should mean that the user profile has been updated.
if (!event.isRedacted() && TextUtils.equals(prevMembership, eventContent.membership)
&& TextUtils.equals(RoomMember.MEMBERSHIP_JOIN, eventContent.membership)) {
// check if the user updates his profile from another device.
boolean hasAccountInfoUpdated = false;
if (!TextUtils.equals(eventContent.displayname, myUser.displayname)) {
hasAccountInfoUpdated = true;
myUser.displayname = eventContent.displayname;
store.setDisplayName(myUser.displayname, event.getOriginServerTs());
}
if (!TextUtils.equals(eventContent.avatar_url, myUser.getAvatarUrl())) {
hasAccountInfoUpdated = true;
myUser.setAvatarUrl(eventContent.avatar_url);
store.setAvatarURL(myUser.avatar_url, event.getOriginServerTs());
}
if (hasAccountInfoUpdated) {
dataHandler.onAccountInfoUpdate(myUser);
}
}
}
final RoomState previousState = mTimelineStateHolder.getState();
if (event.stateKey != null) {
// copy the live state before applying any update
mTimelineStateHolder.deepCopyState(EventTimeline.Direction.FORWARDS);
// check if the event has been processed
if (!mTimelineStateHolder.processStateEvent(event, EventTimeline.Direction.FORWARDS)) {
// not processed -> do not warn the application
// assume that the event is a duplicated one.
return;
}
}
storeLiveRoomEvent(dataHandler, store, event, checkRedactedStateEvent);
// warn the listeners
// general listeners
dataHandler.onLiveEvent(event, previousState);
// timeline listeners
mEventListeners.onEvent(event, EventTimeline.Direction.FORWARDS, previousState);
// trigger pushes when it is required
if (withPush) {
mTimelinePushWorker.triggerPush(mTimelineStateHolder.getState(), event);
}
} else {
Log.e(LOG_TAG, "Unknown live event type: " + event.getType());
}
}
}
/**
* Store a live room event.
*
* @param event The event to be stored.
* @param checkRedactedStateEvent true to check if this event redacts a state event
*/
private void storeLiveRoomEvent(@NonNull final MXDataHandler dataHandler,
@NonNull final IMXStore store,
@NonNull Event event,
final boolean checkRedactedStateEvent) {
boolean shouldBeSaved = false;
String myUserId = dataHandler.getCredentials().userId;
if (Event.EVENT_TYPE_REDACTION.equals(event.getType())) {
if (event.getRedactedEventId() != null) {
Event eventToPrune = store.getEvent(event.getRedactedEventId(), event.roomId);
// when an event is redacted, some fields must be kept.
if (eventToPrune != null) {
shouldBeSaved = true;
// remove expected keys
eventToPrune.prune(event);
// store the prune event
mTimelineEventSaver.storeEvent(eventToPrune);
// store the redaction event too (for the read markers management)
mTimelineEventSaver.storeEvent(event);
// the redaction check must not be done during an initial sync
// or the redacted event is received with roomSync.timeline.limited
if (checkRedactedStateEvent && eventToPrune.stateKey != null) {
mStateEventRedactionChecker.checkStateEventRedaction(event);
}
// search the latest displayable event
// to replace the summary text
final List<Event> events = new ArrayList<>(store.getRoomMessages(event.roomId));
for (int index = events.size() - 1; index >= 0; index--) {
final Event indexedEvent = events.get(index);
if (RoomSummary.isSupportedEvent(indexedEvent)) {
// Decrypt event if necessary
if (TextUtils.equals(indexedEvent.getType(), Event.EVENT_TYPE_MESSAGE_ENCRYPTED)) {
if (null != dataHandler.getCrypto()) {
dataHandler.decryptEvent(indexedEvent, mEventTimeline.getTimelineId());
}
}
final RoomState state = mTimelineStateHolder.getState();
final EventDisplay eventDisplay = new EventDisplay(store.getContext(), indexedEvent, state);
// ensure that message can be displayed
if (!TextUtils.isEmpty(eventDisplay.getTextualDisplay())) {
event = indexedEvent;
break;
}
}
}
} else if (checkRedactedStateEvent) {
// the redaction check must not be done during an initial sync
// or the redacted event is received with roomSync.timeline.limited
mStateEventRedactionChecker.checkStateEventRedaction(event);
}
}
} else {
// the candidate events are not stored.
shouldBeSaved = !event.isCallEvent() || !Event.EVENT_TYPE_CALL_CANDIDATES.equals(event.getType());
// thread issue
// if the user leaves a room,
if (Event.EVENT_TYPE_STATE_ROOM_MEMBER.equals(event.getType()) && myUserId.equals(event.stateKey)) {
final String membership = event.getContentAsJsonObject().getAsJsonPrimitive("membership").getAsString();
if (RoomMember.MEMBERSHIP_LEAVE.equals(membership) || RoomMember.MEMBERSHIP_BAN.equals(membership)) {
shouldBeSaved = mEventTimeline.isHistorical();
// delete the room and warn the listener of the leave event only at the end of the events chunk processing
}
}
}
if (shouldBeSaved) {
mTimelineEventSaver.storeEvent(event);
}
// warn the listener that a new room has been created
if (Event.EVENT_TYPE_STATE_ROOM_CREATE.equals(event.getType())) {
dataHandler.onNewRoom(event.roomId);
}
// warn the listeners that a room has been joined
if (Event.EVENT_TYPE_STATE_ROOM_MEMBER.equals(event.getType()) && myUserId.equals(event.stateKey)) {
final String membership = event.getContentAsJsonObject().getAsJsonPrimitive("membership").getAsString();
if (RoomMember.MEMBERSHIP_JOIN.equals(membership)) {
dataHandler.onJoinRoom(event.roomId);
} else if (RoomMember.MEMBERSHIP_INVITE.equals(membership)) {
dataHandler.onNewRoom(event.roomId);
}
}
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,168 @@
/*
* Copyright 2015 OpenMarket Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.db;
import android.content.Context;
import android.text.TextUtils;
import im.vector.matrix.android.internal.legacy.util.ContentUtils;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.Map;
public class MXLatestChatMessageCache {
private static final String LOG_TAG = MXLatestChatMessageCache.class.getSimpleName();
private static final String FILENAME = "ConsoleLatestChatMessageCache";
final String MXLATESTMESSAGES_STORE_FOLDER = "MXLatestMessagesStore";
private Map<String, String> mLatestMesssageByRoomId = null;
private String mUserId = null;
private File mLatestMessagesDirectory = null;
private File mLatestMessagesFile = null;
/**
* Constructor
*
* @param userId the user id
*/
public MXLatestChatMessageCache(String userId) {
mUserId = userId;
}
/**
* Clear the text caches.
*
* @param context The application context to use.
*/
public void clearCache(Context context) {
ContentUtils.deleteDirectory(mLatestMessagesDirectory);
mLatestMesssageByRoomId = null;
}
/**
* Open the texts cache file.
*
* @param context the context.
*/
private void openLatestMessagesDict(Context context) {
// already checked
if (null != mLatestMesssageByRoomId) {
return;
}
mLatestMesssageByRoomId = new HashMap<>();
try {
mLatestMessagesDirectory = new File(context.getApplicationContext().getFilesDir(), MXLATESTMESSAGES_STORE_FOLDER);
mLatestMessagesDirectory = new File(mLatestMessagesDirectory, mUserId);
mLatestMessagesFile = new File(mLatestMessagesDirectory, FILENAME.hashCode() + "");
if (!mLatestMessagesDirectory.exists()) {
// create dir tree
mLatestMessagesDirectory.mkdirs();
File oldFile = new File(context.getApplicationContext().getFilesDir(), FILENAME.hashCode() + "");
// backward compatibility
if (oldFile.exists()) {
oldFile.renameTo(mLatestMessagesFile);
}
}
if (mLatestMessagesFile.exists()) {
FileInputStream fis = new FileInputStream(mLatestMessagesFile);
ObjectInputStream ois = new ObjectInputStream(fis);
mLatestMesssageByRoomId = (Map) ois.readObject();
ois.close();
fis.close();
}
} catch (Exception e) {
Log.e(LOG_TAG, "## openLatestMessagesDict failed " + e.getMessage(), e);
}
}
/**
* Get the latest written text for a dedicated room.
*
* @param context the context.
* @param roomId the roomId
* @return the latest message
*/
public String getLatestText(Context context, String roomId) {
if (null == mLatestMesssageByRoomId) {
openLatestMessagesDict(context);
}
if (TextUtils.isEmpty(roomId)) {
return "";
}
if (mLatestMesssageByRoomId.containsKey(roomId)) {
return mLatestMesssageByRoomId.get(roomId);
}
return "";
}
/**
* Update the latest message dictionnary.
*
* @param context the context.
*/
private void saveLatestMessagesDict(Context context) {
try {
FileOutputStream fos = new FileOutputStream(mLatestMessagesFile);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(mLatestMesssageByRoomId);
oos.close();
fos.close();
} catch (Exception e) {
Log.e(LOG_TAG, "## saveLatestMessagesDict() failed " + e.getMessage(), e);
}
}
/**
* Update the latest message for a dedicated roomId.
*
* @param context the context.
* @param roomId the roomId.
* @param message the message.
*/
public void updateLatestMessage(Context context, String roomId, String message) {
if (null == mLatestMesssageByRoomId) {
openLatestMessagesDict(context);
}
if (TextUtils.isEmpty(message)) {
mLatestMesssageByRoomId.remove(roomId);
}
mLatestMesssageByRoomId.put(roomId, message);
saveLatestMessagesDict(context);
}
}

View file

@ -0,0 +1,569 @@
/*
* Copyright 2015 OpenMarket Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.db;
import android.os.AsyncTask;
import android.util.Pair;
import org.json.JSONException;
import org.json.JSONObject;
import im.vector.matrix.android.internal.legacy.RestClient;
import im.vector.matrix.android.internal.legacy.listeners.IMXMediaUploadListener;
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
import im.vector.matrix.android.internal.legacy.rest.model.ContentResponse;
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
import im.vector.matrix.android.internal.legacy.ssl.CertUtil;
import im.vector.matrix.android.internal.legacy.util.ContentManager;
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager;
/**
* Private AsyncTask used to upload files.
*/
public class MXMediaUploadWorkerTask extends AsyncTask<Void, Void, String> {
private static final String LOG_TAG = MXMediaUploadWorkerTask.class.getSimpleName();
// upload ID -> task
private static final Map<String, MXMediaUploadWorkerTask> mPendingUploadByUploadId = new HashMap<>();
// progress listener
private final List<IMXMediaUploadListener> mUploadListeners = new ArrayList<>();
// the upload stats
private IMXMediaUploadListener.UploadStats mUploadStats;
// the media mimeType
private final String mMimeType;
// the media to upload
private final InputStream mContentStream;
// its unique identifier
private final String mUploadId;
// store the server response to provide it the listeners
private String mResponseFromServer;
// tells if the current upload has been cancelled.
private boolean mIsCancelled;
/**
* Tells if the upload has been completed
*/
private boolean mIsDone;
// upload const
private static final int UPLOAD_BUFFER_READ_SIZE = 1024 * 32;
// dummy ApiCallback uses to be warned when the upload must be declared as "undeliverable".
private final ApiCallback mApiCallback = new ApiCallback() {
@Override
public void onSuccess(Object info) {
}
@Override
public void onNetworkError(Exception e) {
}
@Override
public void onMatrixError(MatrixError e) {
}
@Override
public void onUnexpectedError(Exception e) {
dispatchResult(mResponseFromServer);
}
};
// the upload server HTTP response code
private int mResponseCode = -1;
// the media file name
private String mFilename;
// the content manager
private final ContentManager mContentManager;
/**
* Check if there is a pending download for the url.
*
* @param uploadId The id to check the existence
* @return the dedicated BitmapWorkerTask if it exists.
*/
public static MXMediaUploadWorkerTask getMediaUploadWorkerTask(String uploadId) {
if (uploadId != null) {
MXMediaUploadWorkerTask task = null;
synchronized (mPendingUploadByUploadId) {
if (mPendingUploadByUploadId.containsKey(uploadId)) {
task = mPendingUploadByUploadId.get(uploadId);
}
}
return task;
}
return null;
}
/**
* Cancel the pending uploads.
*/
public static void cancelPendingUploads() {
Collection<MXMediaUploadWorkerTask> tasks = mPendingUploadByUploadId.values();
// cancels the running task
for (MXMediaUploadWorkerTask task : tasks) {
try {
task.cancelUpload();
task.cancel(true);
} catch (Exception e) {
Log.e(LOG_TAG, "cancelPendingUploads " + e.getMessage(), e);
}
}
mPendingUploadByUploadId.clear();
}
/**
* Constructor
*
* @param contentManager the content manager
* @param contentStream the stream to upload
* @param mimeType the mime type
* @param uploadId the upload id
* @param filename the dest filename
* @param listener the upload listener
*/
public MXMediaUploadWorkerTask(ContentManager contentManager,
InputStream contentStream,
String mimeType,
String uploadId,
String filename,
IMXMediaUploadListener listener) {
if (contentStream.markSupported()) {
try {
contentStream.reset();
} catch (Exception e) {
Log.e(LOG_TAG, "MXMediaUploadWorkerTask " + e.getMessage(), e);
}
} else {
Log.w(LOG_TAG, "Warning, reset() is not supported for this stream");
}
mContentManager = contentManager;
mContentStream = contentStream;
mMimeType = mimeType;
mUploadId = uploadId;
mFilename = filename;
addListener(listener);
if (null != uploadId) {
mPendingUploadByUploadId.put(uploadId, this);
}
}
/**
* Add an upload listener
*
* @param aListener the listener to add.
*/
public void addListener(IMXMediaUploadListener aListener) {
if (null != aListener && mUploadListeners.indexOf(aListener) < 0) {
mUploadListeners.add(aListener);
}
}
/**
* @return the upload progress
*/
public int getProgress() {
if (null != mUploadStats) {
return mUploadStats.mProgress;
}
return -1;
}
/**
* @return the upload stats
*/
public IMXMediaUploadListener.UploadStats getStats() {
return mUploadStats;
}
/**
* @return true if the current upload has been cancelled.
*/
private synchronized boolean isUploadCancelled() {
return mIsCancelled;
}
/**
* Cancel the current upload.
*/
public synchronized void cancelUpload() {
mIsCancelled = true;
}
/**
* refresh the progress info
*/
private void publishProgress(long startUploadTime) {
mUploadStats.mElapsedTime = (int) ((System.currentTimeMillis() - startUploadTime) / 1000);
if (0 != mUploadStats.mFileSize) {
// Uploading data is 90% of the job
// the other 10% is the end of the connection related actions
mUploadStats.mProgress = (int) (((long) mUploadStats.mUploadedSize) * 96 / mUploadStats.mFileSize);
}
// avoid zero div
if (System.currentTimeMillis() != startUploadTime) {
mUploadStats.mBitRate = (int) (((long) mUploadStats.mUploadedSize) * 1000 / (System.currentTimeMillis() - startUploadTime) / 1024);
} else {
mUploadStats.mBitRate = 0;
}
if (0 != mUploadStats.mBitRate) {
mUploadStats.mEstimatedRemainingTime = (mUploadStats.mFileSize - mUploadStats.mUploadedSize) / 1024 / mUploadStats.mBitRate;
} else {
mUploadStats.mEstimatedRemainingTime = -1;
}
publishProgress();
}
@Override
protected String doInBackground(Void... params) {
HttpURLConnection conn;
DataOutputStream dos;
mResponseCode = -1;
int bytesRead, bytesAvailable;
int totalWritten, totalSize;
int bufferSize;
byte[] buffer;
String serverResponse = null;
String urlString = mContentManager.getHsConfig().getHomeserverUri().toString() + ContentManager.URI_PREFIX_CONTENT_API + "upload";
if (null != mFilename) {
try {
String utf8Filename = URLEncoder.encode(mFilename, "utf-8");
urlString += "?filename=" + utf8Filename;
} catch (Exception e) {
Log.e(LOG_TAG, "doInBackground " + e.getMessage(), e);
}
}
try {
URL url = new URL(urlString);
conn = (HttpURLConnection) url.openConnection();
if (RestClient.getUserAgent() != null) {
conn.setRequestProperty("User-Agent", RestClient.getUserAgent());
}
conn.setRequestProperty("Authorization", "Bearer " + mContentManager.getHsConfig().getCredentials().accessToken);
conn.setDoInput(true);
conn.setDoOutput(true);
conn.setUseCaches(false);
conn.setRequestMethod("POST");
if (conn instanceof HttpsURLConnection) {
// Add SSL Socket factory.
HttpsURLConnection sslConn = (HttpsURLConnection) conn;
try {
Pair<SSLSocketFactory, X509TrustManager> pair = CertUtil.newPinnedSSLSocketFactory(mContentManager.getHsConfig());
sslConn.setSSLSocketFactory(pair.first);
sslConn.setHostnameVerifier(CertUtil.newHostnameVerifier(mContentManager.getHsConfig()));
} catch (Exception e) {
Log.e(LOG_TAG, "sslConn " + e.getMessage(), e);
}
}
conn.setRequestProperty("Content-Type", mMimeType);
conn.setRequestProperty("Content-Length", Integer.toString(mContentStream.available()));
// avoid caching data before really sending them.
conn.setFixedLengthStreamingMode(mContentStream.available());
conn.connect();
dos = new DataOutputStream(conn.getOutputStream());
// create a buffer of maximum size
totalSize = bytesAvailable = mContentStream.available();
totalWritten = 0;
bufferSize = Math.min(bytesAvailable, UPLOAD_BUFFER_READ_SIZE);
buffer = new byte[bufferSize];
mUploadStats = new IMXMediaUploadListener.UploadStats();
mUploadStats.mUploadId = mUploadId;
mUploadStats.mProgress = 0;
mUploadStats.mUploadedSize = 0;
mUploadStats.mFileSize = totalSize;
mUploadStats.mElapsedTime = 0;
mUploadStats.mEstimatedRemainingTime = -1;
mUploadStats.mBitRate = 0;
final long startUploadTime = System.currentTimeMillis();
Log.d(LOG_TAG, "doInBackground : start Upload (" + totalSize + " bytes)");
// read file and write it into form...
bytesRead = mContentStream.read(buffer, 0, bufferSize);
dispatchOnUploadStart();
final Timer refreshTimer = new Timer();
// Publish progress every 100ms
refreshTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
if (!mIsDone) {
publishProgress(startUploadTime);
}
}
}, new Date(), 100);
while ((bytesRead > 0) && !isUploadCancelled()) {
dos.write(buffer, 0, bytesRead);
totalWritten += bytesRead;
bytesAvailable = mContentStream.available();
bufferSize = Math.min(bytesAvailable, UPLOAD_BUFFER_READ_SIZE);
Log.d(LOG_TAG, "doInBackground : totalWritten " + totalWritten + " / totalSize " + totalSize);
mUploadStats.mUploadedSize = totalWritten;
bytesRead = mContentStream.read(buffer, 0, bufferSize);
}
mIsDone = true;
refreshTimer.cancel();
if (!isUploadCancelled()) {
mUploadStats.mProgress = 96;
publishProgress(startUploadTime);
dos.flush();
mUploadStats.mProgress = 97;
publishProgress(startUploadTime);
dos.close();
mUploadStats.mProgress = 98;
publishProgress(startUploadTime);
try {
// Read the SERVER RESPONSE
mResponseCode = conn.getResponseCode();
} catch (EOFException eofEx) {
mResponseCode = HttpURLConnection.HTTP_INTERNAL_ERROR;
}
mUploadStats.mProgress = 99;
publishProgress(startUploadTime);
Log.d(LOG_TAG, "doInBackground : Upload is done with response code " + mResponseCode);
InputStream is;
if (mResponseCode == HttpURLConnection.HTTP_OK) {
is = conn.getInputStream();
} else {
is = conn.getErrorStream();
}
int ch;
StringBuffer b = new StringBuffer();
while ((ch = is.read()) != -1) {
b.append((char) ch);
}
serverResponse = b.toString();
is.close();
// the server should provide an error description
if (mResponseCode != HttpURLConnection.HTTP_OK) {
try {
JSONObject responseJSON = new JSONObject(serverResponse);
serverResponse = responseJSON.getString("error");
} catch (JSONException e) {
Log.e(LOG_TAG, "doInBackground : Error parsing " + e.getMessage(), e);
}
}
} else {
dos.flush();
dos.close();
}
if (null != conn) {
conn.disconnect();
}
} catch (Exception e) {
serverResponse = e.getLocalizedMessage();
Log.e(LOG_TAG, "doInBackground ; failed with error " + e.getClass() + " - " + e.getMessage(), e);
}
mResponseFromServer = serverResponse;
return serverResponse;
}
@Override
protected void onProgressUpdate(Void... aVoid) {
super.onProgressUpdate();
Log.d(LOG_TAG, "Upload " + this + " : " + mUploadStats.mProgress);
dispatchOnUploadProgress(mUploadStats);
}
/**
* Dispatch the result to the callbacks
*
* @param serverResponse the server response
*/
private void dispatchResult(final String serverResponse) {
if (null != mUploadId) {
mPendingUploadByUploadId.remove(mUploadId);
}
mContentManager.getUnsentEventsManager().onEventSent(mApiCallback);
// close the source stream
try {
mContentStream.close();
} catch (Exception e) {
Log.e(LOG_TAG, "dispatchResult " + e.getMessage(), e);
}
if (isUploadCancelled()) {
dispatchOnUploadCancel();
} else {
ContentResponse uploadResponse = (mResponseCode != 200 || serverResponse == null) ? null : JsonUtils.toContentResponse(serverResponse);
if (null == uploadResponse || null == uploadResponse.contentUri) {
dispatchOnUploadError(mResponseCode, serverResponse);
} else {
dispatchOnUploadComplete(uploadResponse.contentUri);
}
}
}
@Override
protected void onPostExecute(final String serverResponseMessage) {
// do not call the callback if cancelled.
if (!isCancelled()) {
dispatchResult(serverResponseMessage);
}
}
//==============================================================================================================
// Dispatchers
//==============================================================================================================
/**
* Dispatch Upload start
*/
private void dispatchOnUploadStart() {
for (IMXMediaUploadListener listener : mUploadListeners) {
try {
listener.onUploadStart(mUploadId);
} catch (Exception e) {
Log.e(LOG_TAG, "dispatchOnUploadStart failed " + e.getMessage(), e);
}
}
}
/**
* Dispatch Upload start
*
* @param stats the upload stats
*/
private void dispatchOnUploadProgress(IMXMediaUploadListener.UploadStats stats) {
for (IMXMediaUploadListener listener : mUploadListeners) {
try {
listener.onUploadProgress(mUploadId, stats);
} catch (Exception e) {
Log.e(LOG_TAG, "dispatchOnUploadProgress failed " + e.getMessage(), e);
}
}
}
/**
* Dispatch Upload cancel.
*/
private void dispatchOnUploadCancel() {
for (IMXMediaUploadListener listener : mUploadListeners) {
try {
listener.onUploadCancel(mUploadId);
} catch (Exception e) {
Log.e(LOG_TAG, "listener failed " + e.getMessage(), e);
}
}
}
/**
* Dispatch Upload error.
*
* @param serverResponseCode the server response code.
* @param serverErrorMessage the server error message
*/
private void dispatchOnUploadError(int serverResponseCode, String serverErrorMessage) {
for (IMXMediaUploadListener listener : mUploadListeners) {
try {
listener.onUploadError(mUploadId, serverResponseCode, serverErrorMessage);
} catch (Exception e) {
Log.e(LOG_TAG, "dispatchOnUploadError failed " + e.getMessage(), e);
}
}
}
/**
* Dispatch Upload complete.
*
* @param contentUri the media uri.
*/
private void dispatchOnUploadComplete(String contentUri) {
for (IMXMediaUploadListener listener : mUploadListeners) {
try {
listener.onUploadComplete(mUploadId, contentUri);
} catch (Exception e) {
Log.e(LOG_TAG, "dispatchOnUploadComplete failed " + e.getMessage(), e);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show more