/*
* Copyright 2014 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.androidsdk;
import android.os.HandlerThread;
import android.os.Looper;
import android.text.TextUtils;
import org.matrix.androidsdk.util.Log;
import org.matrix.androidsdk.call.MXCallsManager;
import org.matrix.androidsdk.crypto.MXCrypto;
import org.matrix.androidsdk.crypto.MXCryptoError;
import org.matrix.androidsdk.data.DataRetriever;
import org.matrix.androidsdk.data.store.IMXStore;
import org.matrix.androidsdk.data.MyUser;
import org.matrix.androidsdk.data.Room;
import org.matrix.androidsdk.data.RoomState;
import org.matrix.androidsdk.data.RoomSummary;
import org.matrix.androidsdk.db.MXMediasCache;
import org.matrix.androidsdk.listeners.IMXEventListener;
import org.matrix.androidsdk.network.NetworkConnectivityReceiver;
import org.matrix.androidsdk.rest.callback.ApiCallback;
import org.matrix.androidsdk.rest.callback.SimpleApiCallback;
import org.matrix.androidsdk.rest.client.AccountDataRestClient;
import org.matrix.androidsdk.rest.client.PresenceRestClient;
import org.matrix.androidsdk.rest.client.ProfileRestClient;
import org.matrix.androidsdk.rest.client.RoomsRestClient;
import org.matrix.androidsdk.rest.client.ThirdPidRestClient;
import org.matrix.androidsdk.rest.json.ConditionDeserializer;
import org.matrix.androidsdk.rest.model.Event;
import org.matrix.androidsdk.rest.model.MatrixError;
import org.matrix.androidsdk.rest.model.RoomAliasDescription;
import org.matrix.androidsdk.rest.model.RoomMember;
import org.matrix.androidsdk.rest.model.Sync.SyncResponse;
import org.matrix.androidsdk.rest.model.User;
import org.matrix.androidsdk.rest.model.bingrules.BingRule;
import org.matrix.androidsdk.rest.model.bingrules.BingRuleSet;
import org.matrix.androidsdk.rest.model.bingrules.BingRulesResponse;
import org.matrix.androidsdk.rest.model.bingrules.Condition;
import org.matrix.androidsdk.rest.model.login.Credentials;
import org.matrix.androidsdk.util.BingRulesManager;
import org.matrix.androidsdk.util.JsonUtils;
import org.matrix.androidsdk.util.MXOsHandler;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import android.os.Handler;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
/**
* The data handler provides a layer to help manage matrix input and output.
* <ul>
* <li>Handles events</li>
* <li>Stores the data in its storage layer</li>
* <li>Provides the means for an app to get callbacks for data changes</li>
* </ul>
*/
public class MXDataHandler implements IMXEventListener {
private static final String LOG_TAG = "MXData";
public interface InvalidTokenListener {
/**
* Call when the access token is corrupted
*/
void onTokenCorrupted();
}
private IMXEventListener mCryptoEventsListener = null;
private final List<IMXEventListener> mEventListeners = new ArrayList<>();
private final IMXStore mStore;
private final Credentials mCredentials;
private volatile String mInitialSyncToToken = null;
private DataRetriever mDataRetriever;
private BingRulesManager mBingRulesManager;
private MXCallsManager mCallsManager;
private MXMediasCache mMediasCache;
private ProfileRestClient mProfileRestClient;
private PresenceRestClient mPresenceRestClient;
private ThirdPidRestClient mThirdPidRestClient;
private RoomsRestClient mRoomsRestClient;
private NetworkConnectivityReceiver mNetworkConnectivityReceiver;
private MyUser mMyUser;
private HandlerThread mSyncHandlerThread;
private final MXOsHandler mSyncHandler;
private final MXOsHandler mUiHandler;
// list of ignored users
// null -> not initialized
// should be retrieved from the store
private List<String> mIgnoredUserIdsList;
private boolean mIsAlive = true;
private final InvalidTokenListener mInvalidTokenListener;
// e2e decoder
private MXCrypto mCrypto;
// the crypto is only started when the sync did not retrieve new device
private boolean mIsStartingCryptoWithInitialSync = false;
/**
* Default constructor.
* @param store the data storage implementation.
*/
public MXDataHandler(IMXStore store, Credentials credentials,InvalidTokenListener invalidTokenListener) {
mStore = store;
mCredentials = credentials;
mUiHandler = new MXOsHandler(Looper.getMainLooper());
mSyncHandlerThread = new HandlerThread("MXDataHandler" + mCredentials.userId, Thread.MIN_PRIORITY);
mSyncHandlerThread.start();
mSyncHandler = new MXOsHandler(mSyncHandlerThread.getLooper());
mInvalidTokenListener = invalidTokenListener;
}
public Credentials getCredentials() {
return mCredentials;
}
// setters / getters
public void setProfileRestClient(ProfileRestClient profileRestClient) {
mProfileRestClient = profileRestClient;
}
public ProfileRestClient getProfileRestClient() {
return mProfileRestClient;
}
public void setPresenceRestClient(PresenceRestClient presenceRestClient) {
mPresenceRestClient = presenceRestClient;
}
public PresenceRestClient getPresenceRestClient() {
return mPresenceRestClient;
}
public void setThirdPidRestClient(ThirdPidRestClient thirdPidRestClient) {
mThirdPidRestClient = thirdPidRestClient;
}
public ThirdPidRestClient getThirdPidRestClient() {
return mThirdPidRestClient;
}
public void setRoomsRestClient(RoomsRestClient roomsRestClient) {
mRoomsRestClient = roomsRestClient;
}
public void setNetworkConnectivityReceiver(NetworkConnectivityReceiver networkConnectivityReceiver) {
mNetworkConnectivityReceiver = networkConnectivityReceiver;
if (null != getCrypto()) {
getCrypto().setNetworkConnectivityReceiver(mNetworkConnectivityReceiver);
}
}
public MXCrypto getCrypto() {
return mCrypto;
}
public void setCrypto(MXCrypto crypto) {
mCrypto = crypto;
}
/**
* Provide the list of user Ids to ignore.
* The result cannot be null.
* @return the user Ids list
*/
public List<String> getIgnoredUserIds() {
if (null == mIgnoredUserIdsList) {
mIgnoredUserIdsList = mStore.getIgnoredUserIdsList();
}
// avoid the null case
if (null == mIgnoredUserIdsList) {
mIgnoredUserIdsList = new ArrayList<>();
}
return mIgnoredUserIdsList;
}
/**
* Test if the current instance is still active.
* When the session is closed, many objects keep a reference to this class
* to dispatch events : isAlive() should be called before calling a method of this class.
*/
private void checkIfAlive() {
synchronized (this) {
if (!mIsAlive) {
Log.e(LOG_TAG, "use of a released dataHandler");
//throw new AssertionError("Should not used a MXDataHandler");
}
}
}
/**
* Tell if the current instance is still active.
* When the session is closed, many objects keep a reference to this class
* to dispatch events : isAlive() should be called before calling a method of this class.
* @return true if it is active.
*/
public boolean isAlive() {
synchronized (this) {
return mIsAlive;
}
}
/**
* The current token is not anymore valid
*/
public void onInvalidToken() {
if (null != mInvalidTokenListener) {
mInvalidTokenListener.onTokenCorrupted();
}
}
/**
* Get the session's current user. The MyUser object provides methods for updating user properties which are not possible for other users.
* @return the session's MyUser object
*/
public MyUser getMyUser() {
checkIfAlive();
IMXStore store = getStore();
// MyUser is initialized as late as possible to have a better chance at having the info in storage,
// which should be the case if this is called after the initial sync
if (mMyUser == null) {
mMyUser = new MyUser(store.getUser(mCredentials.userId));
mMyUser.setDataHandler(this);
// assume the profile is not yet initialized
if (null == store.displayName()) {
store.setAvatarURL(mMyUser.getAvatarUrl());
store.setDisplayName(mMyUser.displayname);
} else {
// use the latest user information
// The user could have updated his profile in offline mode and kill the application.
mMyUser.displayname = store.displayName();
mMyUser.setAvatarUrl(store.avatarURL());
}
// Handle the case where the user is null by loading the user information from the server
mMyUser.user_id = mCredentials.userId;
} else if (null != store) {
// assume the profile is not yet initialized
if ((null == store.displayName()) && (null != mMyUser.displayname)) {
// setAvatarURL && setDisplayName perform a commit if it is required.
store.setAvatarURL(mMyUser.getAvatarUrl());
store.setDisplayName(mMyUser.displayname);
} else if (!TextUtils.equals(mMyUser.displayname, store.displayName())) {
mMyUser.displayname = store.displayName();
mMyUser.setAvatarUrl(store.avatarURL());
}
}
// check if there is anything to refresh
mMyUser.refreshUserInfos(null);
return mMyUser;
}
/**
* @return true if the initial sync is completed.
*/
public boolean isInitialSyncComplete() {
checkIfAlive();
return (null != mInitialSyncToToken);
}
/**
* @return the DataRetriever.
*/
public DataRetriever getDataRetriever() {
checkIfAlive();
return mDataRetriever;
}
/**
* Update the dataRetriever.
* @param dataRetriever the dataRetriever.
*/
public void setDataRetriever(DataRetriever dataRetriever) {
checkIfAlive();
mDataRetriever = dataRetriever;
}
/**
* Update the push rules manager.
* @param bingRulesManager the new push rules manager.
*/
public void setPushRulesManager(BingRulesManager bingRulesManager) {
if (isAlive()) {
mBingRulesManager = bingRulesManager;
mBingRulesManager.loadRules(new SimpleApiCallback<Void>() {
@Override
public void onSuccess(Void info) {
MXDataHandler.this.onBingRulesUpdate();
}
});
}
}
/**
* Update the calls manager.
* @param callsManager the new calls manager.
*/
public void setCallsManager(MXCallsManager callsManager) {
checkIfAlive();
mCallsManager = callsManager;
}
/**
* @return the user calls manager.
*/
public MXCallsManager getCallsManager() {
checkIfAlive();
return mCallsManager;
}
/**
* Update the medias cache.
* @param mediasCache the new medias cache.
*/
public void setMediasCache(MXMediasCache mediasCache) {
checkIfAlive();
mMediasCache = mediasCache;
}
/**
* Retrieve the medias cache.
* @return the used mediasCache
*/
public MXMediasCache getMediasCache() {
checkIfAlive();
return mMediasCache;
}
/**
* @return the used push rules set.
*/
public BingRuleSet pushRules() {
if (isAlive() && (null != mBingRulesManager)) {
return mBingRulesManager.pushRules();
}
return null;
}
/**
* Trigger a push rules refresh.
*/
public void refreshPushRules() {
if (isAlive() && (null != mBingRulesManager)) {
mBingRulesManager.loadRules(new SimpleApiCallback<Void>() {
@Override
public void onSuccess(Void info) {
MXDataHandler.this.onBingRulesUpdate();
}
});
}
}
/**
* @return the used BingRulesManager.
*/
public BingRulesManager getBingRulesManager() {
checkIfAlive();
return mBingRulesManager;
}
/**
* Set the crypto events listener
* @param listener the listener
*/
public void setCryptoEventsListener(IMXEventListener listener) {
mCryptoEventsListener = listener;
}
/**
* Add a listener to the listeners list.
* @param listener the listener to add.
*/
public void addListener(IMXEventListener listener) {
if (isAlive()) {
synchronized (this) {
// avoid adding twice
if (mEventListeners.indexOf(listener) == -1) {
mEventListeners.add(listener);
}
}
if (null != mInitialSyncToToken) {
listener.onInitialSyncComplete(mInitialSyncToToken);
}
}
}
/**
* Remove a listener from the listeners list.
* @param listener to remove.
*/
public void removeListener(IMXEventListener listener) {
if (isAlive()) {
synchronized (this) {
mEventListeners.remove(listener);
}
}
}
/**
* Clear the instance data.
*/
public void clear() {
synchronized (this) {
mIsAlive = false;
// remove any listener
mEventListeners.clear();
}
// clear the store
mStore.close();
mStore.clear();
if (null != mSyncHandlerThread) {
mSyncHandlerThread.quit();
mSyncHandlerThread = null;
}
}
/**
* @return the current user id.
*/
public String getUserId() {
if (isAlive()) {
return mCredentials.userId;
} else {
return "dummy";
}
}
/**
* Update the missing data fields loaded from a permanent storage.
*/
public void checkPermanentStorageData() {
if (!isAlive()) {
Log.e(LOG_TAG, "checkPermanentStorageData : the session is not anymore active");
return;
}
if (mStore.isPermanent()) {
// When the data are extracted from a persistent storage,
// some fields are not retrieved :
// They are used to retrieve some data
// so add the missing links.
Collection<Room> rooms = mStore.getRooms();
for(Room room : rooms) {
room.init(room.getRoomId(), this);
}
Collection<RoomSummary> summaries = mStore.getSummaries();
for(RoomSummary summary : summaries) {
if (null != summary.getLatestRoomState()) {
summary.getLatestRoomState().setDataHandler(this);
}
}
}
}
/**
* @return the used store.
*/
public IMXStore getStore() {
if (isAlive()) {
return mStore;
} else {
Log.e(LOG_TAG, "getStore : the session is not anymore active");
return null;
}
}
/**
* Returns the member with userID;
* @param members the members List
* @param userID the user ID
* @return the roomMember if it exists.
*/
public RoomMember getMember(Collection<RoomMember> members, String userID) {
if (isAlive()) {
for (RoomMember member : members) {
if (TextUtils.equals(userID, member.getUserId())) {
return member;
}
}
} else {
Log.e(LOG_TAG, "getMember : the session is not anymore active");
}
return null;
}
/**
* Check a room exists with the dedicated roomId
* @param roomId the room ID
* @return true it exists.
*/
public boolean doesRoomExist(String roomId) {
return (null != roomId) && (null != mStore.getRoom(roomId));
}
/**
* Get the room object for the corresponding room id. Creates and initializes the object if there is none.
* @param roomId the room id
* @return the corresponding room
*/
public Room getRoom(String roomId) {
return getRoom(roomId, true);
}
/**
* Get the room object for the corresponding room id.
* @param roomId the room id
* @param create create the room it does not exist.
* @return the corresponding room
*/
public Room getRoom(String roomId, boolean create) {
return getRoom(mStore, roomId, create);
}
/**
* Get the room object for the corresponding room id.
* @param store the dedicated store
* @param roomId the room id
* @param create create the room it does not exist.
* @return the corresponding room
*/
public Room getRoom(IMXStore store, String roomId, boolean create) {
if (!isAlive()) {
Log.e(LOG_TAG, "getRoom : the session is not anymore active");
return null;
}
// sanity check
if (TextUtils.isEmpty(roomId)) {
return null;
}
Room room;
synchronized (this) {
room = store.getRoom(roomId);
if ((room == null) && create) {
Log.d(LOG_TAG, "## getRoom() : create the room " + roomId);
room = new Room();
room.init(roomId, this);
store.storeRoom(room);
} else if ((null != room) && (null == room.getDataHandler())) {
// GA reports that some rooms have no data handler
// so ensure that it is not properly set
Log.e(LOG_TAG, "getRoom " + roomId + " was not initialized");
room.init(roomId, this);
store.storeRoom(room);
}
}
return room;
}
/**
* Checks if the room is properly initialized.
* GA reported us that some room fields are not initialized.
* But, i really don't know how it is possible.
* @param room the room check
*/
public void checkRoom(Room room) {
// sanity check
if (null != room) {
if (null == room.getDataHandler()) {
Log.e(LOG_TAG, "checkRoom : the room was not initialized");
room.init(room.getRoomId(), this);
} else if ((null != room.getLiveTimeLine()) && (null == room.getLiveTimeLine().mDataHandler)) {
Log.e(LOG_TAG, "checkRoom : the timeline was not initialized");
room.init(room.getRoomId(), this);
}
}
}
/**
* Retrieve a room Id by its alias.
* @param roomAlias the room alias
* @param callback the asynchronous callback
*/
public void roomIdByAlias(final String roomAlias, final ApiCallback<String> callback) {
String roomId = null;
Collection<Room> rooms = getStore().getRooms();
for(Room room : rooms) {
if (TextUtils.equals(room.getState().alias, roomAlias)) {
roomId = room.getRoomId();
break;
} else {
// getAliases cannot be null
List<String> aliases = room.getState().getAliases();
for(String alias : aliases) {
if (TextUtils.equals(alias, roomAlias)) {
roomId = room.getRoomId();
break;
}
}
// find one matched room id.
if (null != roomId) {
break;
}
}
}
if (null != roomId) {
final String fRoomId = roomId;
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
callback.onSuccess(fRoomId);
}
});
} else {
mRoomsRestClient.getRoomIdByAlias(roomAlias, new ApiCallback<RoomAliasDescription>() {
@Override
public void onSuccess(RoomAliasDescription info) {
callback.onSuccess(info.room_id);
}
@Override
public void onNetworkError(Exception e) {
callback.onNetworkError(e);
}
@Override
public void onMatrixError(MatrixError e) {
callback.onMatrixError(e);
}
@Override
public void onUnexpectedError(Exception e) {
callback.onUnexpectedError(e);
}
});
}
}
/**
* Delete an event.
* @param event The event to be stored.
*/
public void deleteRoomEvent(Event event) {
if (isAlive()) {
Room room = getRoom(event.roomId);
if (null != room) {
mStore.deleteEvent(event);
Event lastEvent = mStore.getLatestEvent(event.roomId);
RoomState beforeLiveRoomState = room.getState().deepCopy();
mStore.storeSummary(event.roomId, lastEvent, beforeLiveRoomState, mCredentials.userId);
}
} else {
Log.e(LOG_TAG, "deleteRoomEvent : the session is not anymore active");
}
}
/**
* Return an user from his id.
* @param userId the user id;.
* @return the user.
*/
public User getUser(String userId) {
if (!isAlive()) {
Log.e(LOG_TAG, "getUser : the session is not anymore active");
return null;
} else {
return mStore.getUser(userId);
}
}
//================================================================================
// Account Data management
//================================================================================
/**
* Manage the sync accountData field
* @param accountData the account data
* @param isInitialSync true if it is an initial sync response
*/
private void manageAccountData(Map<String, Object> accountData, boolean isInitialSync) {
try {
if (accountData.containsKey("events")) {
List<Map<String, Object>> events = (List<Map<String, Object>>) accountData.get("events");
if (0 != events.size()) {
// ignored users list
manageIgnoredUsers(events, isInitialSync);
// push rules
managePushRulesUpdate(events);
// direct messages rooms
manageDirectChatRooms(events, isInitialSync);
}
}
} catch (Exception e) {
Log.e(LOG_TAG, "manageAccountData failed " + e.getMessage());
}
}
/**
* Refresh the push rules from the account data events list
* @param events the account data events.
*/
private void managePushRulesUpdate(List<Map<String, Object>> events) {
for (Map<String, Object> event : events) {
String type = (String) event.get("type");
if (TextUtils.equals(type, "m.push_rules")) {
if (event.containsKey("content")) {
Gson gson = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.excludeFieldsWithModifiers(Modifier.PRIVATE, Modifier.STATIC)
.registerTypeAdapter(Condition.class, new ConditionDeserializer())
.create();
// convert the data to BingRulesResponse
// because BingRulesManager supports only BingRulesResponse
JsonElement element = gson.toJsonTree(event.get("content"));
getBingRulesManager().buildRules(gson.fromJson(element, BingRulesResponse.class));
// warn the client that the push rules have been updated
onBingRulesUpdate();
}
return;
}
}
}
/**
* Check if the ignored users list is updated
* @param events the account data events list
*/
private void manageIgnoredUsers(List<Map<String, Object>> events, boolean isInitialSync) {
List<String> newIgnoredUsers = ignoredUsers(events);
if (null != newIgnoredUsers) {
List<String> curIgnoredUsers = getIgnoredUserIds();
// the both lists are not empty
if ((0 != newIgnoredUsers.size()) || (0 != curIgnoredUsers.size())) {
// check if the ignored users list has been updated
if ((newIgnoredUsers.size() != curIgnoredUsers.size()) || !newIgnoredUsers.containsAll(curIgnoredUsers)) {
// update the store
mStore.setIgnoredUserIdsList(newIgnoredUsers);
mIgnoredUserIdsList = newIgnoredUsers;
if (!isInitialSync) {
// warn there is an update
onIgnoredUsersListUpdate();
}
}
}
}
}
/**
* Extract the ignored users list from the account data events list..
*
* @param events the account data events list.
* @return the ignored users list. null means that there is no defined user ids list.
*/
private List<String> ignoredUsers(List<Map<String, Object>> events) {
List<String> ignoredUsers = null;
if (0 != events.size()) {
for (Map<String, Object> event : events) {
String type = (String) event.get("type");
if (TextUtils.equals(type, AccountDataRestClient.ACCOUNT_DATA_TYPE_IGNORED_USER_LIST)) {
if (event.containsKey("content")) {
Map<String, Object> contentDict = (Map<String, Object>) event.get("content");
if (contentDict.containsKey(AccountDataRestClient.ACCOUNT_DATA_KEY_IGNORED_USERS)) {
Map<String, Object> ignored_users = (Map<String, Object>) contentDict.get(AccountDataRestClient.ACCOUNT_DATA_KEY_IGNORED_USERS);
if (null != ignored_users) {
ignoredUsers = new ArrayList<>(ignored_users.keySet());
}
}
}
}
}
}
return ignoredUsers;
}
/**
* Extract the direct chat rooms list from the dedicated events.
* @param events the account data events list.
*/
private void manageDirectChatRooms(List<Map<String, Object>> events, boolean isInitialSync) {
if (0 != events.size()) {
for (Map<String, Object> event : events) {
String type = (String) event.get("type");
if (TextUtils.equals(type, AccountDataRestClient.ACCOUNT_DATA_TYPE_DIRECT_MESSAGES)) {
if (event.containsKey("content")) {
Map<String, List<String>> contentDict = (Map<String, List<String>>) event.get("content");
mStore.setDirectChatRoomsDict(contentDict);
if (!isInitialSync) {
// warn there is an update
onDirectMessageChatRoomsListUpdate();
}
}
}
}
}
}
//================================================================================
// Sync V2
//================================================================================
/**
* Handle a presence event.
* @param presenceEvent teh presence event.
*/
private void handlePresenceEvent(Event presenceEvent) {
// Presence event
if (Event.EVENT_TYPE_PRESENCE.equals(presenceEvent.getType())) {
User userPresence = JsonUtils.toUser(presenceEvent.getContent());
// use the sender by default
if (!TextUtils.isEmpty(presenceEvent.getSender())) {
userPresence.user_id = presenceEvent.getSender();
}
User user = mStore.getUser(userPresence.user_id);
if (user == null) {
user = userPresence;
user.setDataHandler(this);
}
else {
user.currently_active = userPresence.currently_active;
user.presence = userPresence.presence;
user.lastActiveAgo = userPresence.lastActiveAgo;
}
user.setLatestPresenceTs(System.currentTimeMillis());
// check if the current user has been updated
if (mCredentials.userId.equals(user.user_id)) {
// always use the up-to-date information
getMyUser().displayname = user.displayname;
getMyUser().avatar_url = user.getAvatarUrl();
mStore.setAvatarURL(user.getAvatarUrl());
mStore.setDisplayName(user.displayname);
}
mStore.storeUser(user);
this.onPresenceUpdate(presenceEvent, user);
}
}
/**
* Manage a syncResponse.
* @param syncResponse the syncResponse to manage.
* @param fromToken the start sync token
* @param isCatchingUp true when there is a pending catch-up
*/
public void onSyncResponse(final SyncResponse syncResponse, final String fromToken, final boolean isCatchingUp) {
// perform the sync in background
// to avoid UI thread lags.
mSyncHandler.post(new Runnable() {
@Override
public void run() {
manageResponse(syncResponse, fromToken, isCatchingUp);
}
});
}
/**
* Manage the sync response in the UI thread.
* @param syncResponse the syncResponse to manage.
* @param fromToken the start sync token
* @param isCatchingUp true when there is a pending catch-up
*/
private void manageResponse(final SyncResponse syncResponse, final String fromToken, final boolean isCatchingUp) {
if (!isAlive()) {
Log.e(LOG_TAG, "manageResponse : ignored because the session has been closed");
return;
}
boolean isInitialSync = (null == fromToken);
boolean isEmptyResponse = true;
// sanity check
if (null != syncResponse) {
Log.d(LOG_TAG, "onSyncComplete");
// Handle the to device events before the room ones
// to ensure to decrypt them properly
if ((null != syncResponse.toDevice) &&
(null != syncResponse.toDevice.events) &&
(syncResponse.toDevice.events.size() > 0)) {
Log.d(LOG_TAG, "manageResponse : receives " + syncResponse.toDevice.events.size() + " toDevice events");
for (Event toDeviceEvent : syncResponse.toDevice.events) {
handleToDeviceEvent(toDeviceEvent);
}
}
// sanity check
if (null != syncResponse.rooms) {
// joined rooms events
if ((null != syncResponse.rooms.join) && (syncResponse.rooms.join.size() > 0)) {
Log.d(LOG_TAG, "Received " + syncResponse.rooms.join.size() + " joined rooms");
Set<String> roomIds = syncResponse.rooms.join.keySet();
// Handle first joined rooms
for (String roomId : roomIds) {
getRoom(roomId).handleJoinedRoomSync(syncResponse.rooms.join.get(roomId), isInitialSync);
}
isEmptyResponse = false;
}
// invited room management
if ((null != syncResponse.rooms.invite) && (syncResponse.rooms.invite.size() > 0)) {
Log.d(LOG_TAG, "Received " + syncResponse.rooms.invite.size() + " invited rooms");
Set<String> roomIds = syncResponse.rooms.invite.keySet();
for (String roomId : roomIds) {
Log.d(LOG_TAG, "## manageResponse() : the user has been invited to " + roomId);
getRoom(roomId).handleInvitedRoomSync(syncResponse.rooms.invite.get(roomId));
}
isEmptyResponse = false;
}
// left room management
// it should be done at the end but it seems there is a server issue
// when inviting after leaving a room, the room is defined in the both leave & invite rooms list.
if ((null != syncResponse.rooms.leave) && (syncResponse.rooms.leave.size() > 0)) {
Log.d(LOG_TAG, "Received " + syncResponse.rooms.leave.size() + " left rooms");
Set<String> roomIds = syncResponse.rooms.leave.keySet();
for (String roomId : roomIds) {
// RoomSync leftRoomSync = syncResponse.rooms.leave.get(roomId);
// Presently we remove the existing room from the rooms list.
// FIXME SYNC V2 Archive/Display the left rooms!
// For that create 'handleArchivedRoomSync' method
Room room = this.getStore().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 able to notify its listeners.
room.handleJoinedRoomSync(syncResponse.rooms.leave.get(roomId), isInitialSync);
Log.d(LOG_TAG, "## manageResponse() : leave the room " + roomId);
this.getStore().deleteRoom(roomId);
onLeaveRoom(roomId);
} else {
Log.d(LOG_TAG, "## manageResponse() : Try to leave an unknown room " + roomId);
}
}
isEmptyResponse = false;
}
}
// Handle presence of other users
if ((null != syncResponse.presence) && (null != syncResponse.presence.events)) {
for (Event presenceEvent : syncResponse.presence.events) {
handlePresenceEvent(presenceEvent);
}
}
// account data
if (null != syncResponse.accountData) {
manageAccountData(syncResponse.accountData, isInitialSync);
}
if (null != mCrypto) {
mCrypto.onSyncCompleted(syncResponse, fromToken, isCatchingUp);
}
IMXStore store = getStore();
if (!isEmptyResponse && (null != store)) {
store.setEventStreamToken(syncResponse.nextBatch);
store.commit();
}
}
if (isInitialSync) {
if (!isCatchingUp) {
startCrypto(true);
} else {
// the events thread sends a dummy initial sync event
// when the application is restarted.
mIsStartingCryptoWithInitialSync = !isEmptyResponse;
}
onInitialSyncComplete((null != syncResponse) ? syncResponse.nextBatch : null);
} else {
if (!isCatchingUp) {
startCrypto(mIsStartingCryptoWithInitialSync);
}
try {
onLiveEventsChunkProcessed(fromToken, (null != syncResponse) ? syncResponse.nextBatch : fromToken);
} catch (Exception e) {
Log.e(LOG_TAG, "onLiveEventsChunkProcessed failed " + e.getMessage());
}
try {
// check if an incoming call has been received
mCallsManager.checkPendingIncomingCalls();
} catch (Exception e) {
Log.e(LOG_TAG, "checkPendingIncomingCalls failed " + e + " " + e.getMessage());
}
}
}
/**
* Refresh the unread summary counters of the updated rooms.
*/
private void refreshUnreadCounters() {
ArrayList<String> roomIdsList;
synchronized (mUpdatedRoomIdList) {
roomIdsList = new ArrayList<>(mUpdatedRoomIdList);
mUpdatedRoomIdList.clear();
}
// refresh the unread counter
for (String roomId : roomIdsList) {
Room room = mStore.getRoom(roomId);
if (null != room) {
room.refreshUnreadCounter();
}
}
}
/**
* Handle a 'toDevice' event
* @param event the event
*/
private void handleToDeviceEvent(Event event) {
// Decrypt event if necessary
decryptEvent(event, null);
if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_MESSAGE) && (null != event.getContent()) && TextUtils.equals(JsonUtils.getMessageMsgType(event.getContent()), "m.bad.encrypted")) {
Log.e(LOG_TAG, "## handleToDeviceEvent() : Warning: Unable to decrypt to-device event : " + event.getContent());
} else {
onToDeviceEvent(event);
}
}
/**
* Decrypt an encrypted event
* @param event the event to decrypt
* @param timelineId the timeline identifier
* @return true if the event has been decrypted
*/
public boolean decryptEvent(Event event, String timelineId) {
if ((null != event) && TextUtils.equals(event.getType(), Event.EVENT_TYPE_MESSAGE_ENCRYPTED)) {
if (null != getCrypto()) {
return getCrypto().decryptEvent(event, timelineId);
} else {
event.setClearEvent(null);
event.setCryptoError(new MXCryptoError(MXCryptoError.ENCRYPTING_NOT_ENABLED_ERROR_CODE, MXCryptoError.ENCRYPTING_NOT_ENABLED_REASON, null));
}
}
return false;
}
/**
* Reset replay attack data for the given timeline.
* @param timelineId the timeline id
*/
public void resetReplayAttackCheckInTimeline(String timelineId) {
if ((null != timelineId) && (null != mCrypto) && (null != mCrypto.getOlmDevice())) {
mCrypto.resetReplayAttackCheckInTimeline(timelineId);
}
}
//================================================================================
// Listeners management
//================================================================================
/**
* @return the current MXEvents listeners .
*/
private List<IMXEventListener> getListenersSnapshot() {
ArrayList<IMXEventListener> eventListeners;
synchronized (this) {
eventListeners = new ArrayList<>(mEventListeners);
}
return eventListeners;
}
/**
* Dispatch that the store is ready.
*/
public void onStoreReady() {
if (null != mCryptoEventsListener) {
mCryptoEventsListener.onStoreReady();
}
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());
}
}
}
});
}
@Override
public void onAccountInfoUpdate(final MyUser myUser) {
if (null != mCryptoEventsListener) {
mCryptoEventsListener.onAccountInfoUpdate(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());
}
}
}
});
}
@Override
public void onPresenceUpdate(final Event event, final User user) {
if (null != mCryptoEventsListener) {
mCryptoEventsListener.onPresenceUpdate(event, 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());
}
}
}
});
}
/**
* Stored the room id of the rooms which have received some events
* which imply an unread messages counter refresh.
*/
private final ArrayList<String> mUpdatedRoomIdList = new ArrayList<>();
@Override
public void onLiveEvent(final Event event, final RoomState roomState) {
String type = event.getType();
if (!TextUtils.equals(Event.EVENT_TYPE_TYPING, type) && !TextUtils.equals(Event.EVENT_TYPE_RECEIPT, type) && !TextUtils.equals(Event.EVENT_TYPE_TYPING, type)) {
synchronized (mUpdatedRoomIdList) {
if (mUpdatedRoomIdList.indexOf(roomState.roomId) < 0) {
mUpdatedRoomIdList.add(roomState.roomId);
}
}
}
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());
}
}
}
});
}
@Override
public void onLiveEventsChunkProcessed(final String startToken, final String toToken) {
refreshUnreadCounters();
if (null != mCryptoEventsListener) {
mCryptoEventsListener.onLiveEventsChunkProcessed(startToken, 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());
}
}
}
});
}
@Override
public void onBingEvent(final Event event, final RoomState roomState, final BingRule bingRule) {
if (null != mCryptoEventsListener) {
mCryptoEventsListener.onBingEvent(event,roomState, bingRule);
}
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());
}
}
}
});
}
@Override
public void onEventEncrypted(final Event event) {
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onEventEncrypted(event);
} catch (Exception e) {
Log.e(LOG_TAG, "onEventEncrypted " + e.getMessage());
}
}
}
});
}
@Override
public void onSentEvent(final Event event) {
if (null != mCryptoEventsListener) {
mCryptoEventsListener.onSentEvent(event);
}
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onSentEvent(event);
} catch (Exception e) {
Log.e(LOG_TAG, "onSentEvent " + e.getMessage());
}
}
}
});
}
@Override
public void onFailedSendingEvent(final Event event) {
if (null != mCryptoEventsListener) {
mCryptoEventsListener.onFailedSendingEvent(event);
}
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onFailedSendingEvent(event);
} catch (Exception e) {
Log.e(LOG_TAG, "onFailedSendingEvent " + e.getMessage());
}
}
}
});
}
@Override
public void onBingRulesUpdate() {
if (null != mCryptoEventsListener) {
mCryptoEventsListener.onBingRulesUpdate();
}
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());
}
}
}
});
}
/**
* Dispatch the onInitialSyncComplete event.
*/
private void dispatchOnInitialSyncComplete(final String toToken) {
mInitialSyncToToken = toToken;
refreshUnreadCounters();
if (null != mCryptoEventsListener) {
mCryptoEventsListener.onInitialSyncComplete(toToken);
}
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onInitialSyncComplete(mInitialSyncToToken);
} catch (Exception e) {
Log.e(LOG_TAG, "onInitialSyncComplete " + e.getMessage());
}
}
}
});
}
/**
* Dispatch the OnCryptoSyncComplete event.
*/
private 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());
}
}
}
});
}
/**
* Start the crypto
*/
private void startCrypto(final boolean isInitialSync) {
if ((null != getCrypto()) && !getCrypto().isStarted() && !getCrypto().isStarting()) {
getCrypto().setNetworkConnectivityReceiver(mNetworkConnectivityReceiver);
getCrypto().start(isInitialSync, new ApiCallback<Void>() {
@Override
public void onSuccess(Void info) {
dispatchOnCryptoSyncComplete();
}
private void onError(String errorMessage) {
Log.e(LOG_TAG, "## onInitialSyncComplete() : getCrypto().start fails " + errorMessage);
}
@Override
public void onNetworkError(Exception e) {
onError(e.getMessage());
}
@Override
public void onMatrixError(MatrixError e) {
onError(e.getMessage());
}
@Override
public void onUnexpectedError(Exception e) {
onError(e.getMessage());
}
});
}
}
@Override
public void onInitialSyncComplete(String toToken) {
dispatchOnInitialSyncComplete(toToken);
}
@Override
public void onCryptoSyncComplete() {
}
@Override
public void onNewRoom(final String roomId) {
if (null != mCryptoEventsListener) {
mCryptoEventsListener.onNewRoom(roomId);
}
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());
}
}
}
});
}
@Override
public void onJoinRoom(final String roomId) {
if (null != mCryptoEventsListener) {
mCryptoEventsListener.onJoinRoom(roomId);
}
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());
}
}
}
});
}
@Override
public void onRoomInitialSyncComplete(final String roomId) {
if (null != mCryptoEventsListener) {
mCryptoEventsListener.onRoomInitialSyncComplete(roomId);
}
final List<IMXEventListener> eventListeners = getListenersSnapshot();
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (IMXEventListener listener : eventListeners) {
try {
listener.onRoomInitialSyncComplete(roomId);
} catch (Exception e) {
Log.e(LOG_TAG, "onRoomInitialSyncComplete " + e.getMessage());
}
}
}
});
}
@Override
public void onRoomInternalUpdate(final String roomId) {
if (null != mCryptoEventsListener) {
mCryptoEventsListener.onRoomInternalUpdate(roomId);
}
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());
}
}
}
});
}
@Override
public void onLeaveRoom(final String roomId) {
if (null != mCryptoEventsListener) {
mCryptoEventsListener.onLeaveRoom(roomId);
}
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());
}
}
}
});
}
@Override
public void onReceiptEvent(final String roomId, final List<String> senderIds) {
synchronized (mUpdatedRoomIdList) {
// refresh the unread countries at the end of the process chunk
if (mUpdatedRoomIdList.indexOf(roomId) < 0) {
mUpdatedRoomIdList.add(roomId);
}
}
if (null != mCryptoEventsListener) {
mCryptoEventsListener.onReceiptEvent(roomId, senderIds);
}
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());
}
}
}
});
}
@Override
public void onRoomTagEvent(final String roomId) {
if (null != mCryptoEventsListener) {
mCryptoEventsListener.onRoomTagEvent(roomId);
}
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());
}
}
}
});
}
@Override
public void onRoomFlush(final String roomId) {
if (null != mCryptoEventsListener) {
mCryptoEventsListener.onRoomFlush(roomId);
}
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());
}
}
}
});
}
@Override
public void onIgnoredUsersListUpdate() {
if (null != mCryptoEventsListener) {
mCryptoEventsListener.onIgnoredUsersListUpdate();
}
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());
}
}
}
});
}
@Override
public void onToDeviceEvent(final Event event) {
if (null != mCryptoEventsListener) {
mCryptoEventsListener.onToDeviceEvent(event);
}
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());
}
}
}
});
}
@Override
public void onDirectMessageChatRoomsListUpdate() {
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());
}
}
}
});
}
@Override
public void onEventDecrypted(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());
}
}
}
});
}
}