org.matrix.androidsdk.MXDataHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.matrix.androidsdk.MXDataHandler.java

Source

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

}