/* * 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.data; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.ExifInterface; import android.media.MediaMetadataRetriever; import android.media.MediaPlayer; import android.net.Uri; import android.os.AsyncTask; import android.text.TextUtils; import org.matrix.androidsdk.util.Log; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; import org.matrix.androidsdk.MXDataHandler; import org.matrix.androidsdk.call.MXCallsManager; import org.matrix.androidsdk.crypto.MXCryptoError; import org.matrix.androidsdk.crypto.data.MXEncryptEventContentResult; import org.matrix.androidsdk.data.store.IMXStore; import org.matrix.androidsdk.db.MXMediasCache; import org.matrix.androidsdk.listeners.IMXEventListener; import org.matrix.androidsdk.listeners.MXEventListener; import org.matrix.androidsdk.rest.callback.ApiCallback; import org.matrix.androidsdk.rest.callback.SimpleApiCallback; import org.matrix.androidsdk.rest.client.RoomsRestClient; import org.matrix.androidsdk.rest.client.UrlPostTask; import org.matrix.androidsdk.rest.model.BannedUser; import org.matrix.androidsdk.rest.model.Event; import org.matrix.androidsdk.rest.model.FileInfo; import org.matrix.androidsdk.rest.model.FileMessage; import org.matrix.androidsdk.rest.model.ImageInfo; import org.matrix.androidsdk.rest.model.ImageMessage; import org.matrix.androidsdk.rest.model.LocationMessage; import org.matrix.androidsdk.rest.model.MatrixError; import org.matrix.androidsdk.rest.model.PowerLevels; import org.matrix.androidsdk.rest.model.ReceiptData; import org.matrix.androidsdk.rest.model.RoomMember; import org.matrix.androidsdk.rest.model.RoomResponse; import org.matrix.androidsdk.rest.model.Sync.RoomSync; import org.matrix.androidsdk.rest.model.Sync.InvitedRoomSync; import org.matrix.androidsdk.rest.model.ThumbnailInfo; import org.matrix.androidsdk.rest.model.TokensChunkResponse; import org.matrix.androidsdk.rest.model.User; import org.matrix.androidsdk.rest.model.VideoInfo; import org.matrix.androidsdk.rest.model.VideoMessage; import org.matrix.androidsdk.util.ImageUtils; import org.matrix.androidsdk.util.JsonUtils; import java.io.File; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.RejectedExecutionException; /** * Class representing a room and the interactions we have with it. */ public class Room { private static final String LOG_TAG = "Room"; // Account data private RoomAccountData mAccountData = new RoomAccountData(); // handler private MXDataHandler mDataHandler; // store private IMXStore mStore; private String mMyUserId = null; // Map to keep track of the listeners the client adds vs. the ones we actually register to the global data handler. // This is needed to find the right one when removing the listener. private final Map<IMXEventListener, IMXEventListener> mEventListeners = new HashMap<>(); // the user is leaving the room private boolean mIsLeaving = false; // the room is syncing private boolean mIsSyncing; // the unread messages count must be refreshed when the current sync is done. private boolean mRefreshUnreadAfterSync = false; // the time line private EventTimeline mLiveTimeline; // initial sync callback. private ApiCallback<Void> mOnInitialSyncCallback; // gson parser private final Gson gson = new GsonBuilder().create(); // This is used to block live events and history requests until the state is fully processed and ready private boolean mIsReady = false; // call conference user id private String mCallConferenceUserId; /** * Default room creator */ public Room() { mLiveTimeline = new EventTimeline(this, true); } /** * Init the room fields. * * @param roomId the room id * @param dataHandler the data handler */ public void init(String roomId, MXDataHandler dataHandler) { mLiveTimeline.setRoomId(roomId); mDataHandler = dataHandler; if (null != mDataHandler) { mStore = mDataHandler.getStore(); mMyUserId = mDataHandler.getUserId(); mLiveTimeline.setDataHandler(dataHandler); } } /** * @return the used datahandler */ public MXDataHandler getDataHandler() { return mDataHandler; } /** * Tells if the room is a call conference one * i.e. this room has been created to manage the call conference * * @return true if it is a call conference room. */ public boolean isConferenceUserRoom() { return getLiveState().isConferenceUserRoom(); } /** * Set this room as a conference user room * * @param isConferenceUserRoom true when it is an user conference room. */ public void setIsConferenceUserRoom(boolean isConferenceUserRoom) { getLiveState().setIsConferenceUserRoom(isConferenceUserRoom); } /** * Test if there is an ongoing conference call. * * @return true if there is one. */ public boolean isOngoingConferenceCall() { RoomMember conferenceUser = getLiveState().getMember(MXCallsManager.getConferenceUserId(getRoomId())); return (null != conferenceUser) && TextUtils.equals(conferenceUser.membership, RoomMember.MEMBERSHIP_JOIN); } //================================================================================ // Sync events //================================================================================ /** * Manage list of ephemeral events * * @param events the ephemeral events */ private void handleEphemeralEvents(List<Event> events) { for (Event event : events) { // ensure that the room Id is defined event.roomId = getRoomId(); try { if (Event.EVENT_TYPE_RECEIPT.equals(event.getType())) { if (event.roomId != null) { List<String> senders = handleReceiptEvent(event); if ((null != senders) && (senders.size() > 0)) { mDataHandler.onReceiptEvent(event.roomId, senders); } } } else if (Event.EVENT_TYPE_TYPING.equals(event.getType())) { mDataHandler.onLiveEvent(event, getState()); } } catch (Exception e) { Log.e(LOG_TAG, "ephemeral event failed " + e.getLocalizedMessage()); } } } /** * Handle the events of a joined room. * * @param roomSync the sync events list. * @param isInitialSync true if the room is initialized by a global initial sync. */ public void handleJoinedRoomSync(RoomSync roomSync, boolean isInitialSync) { if (null != mOnInitialSyncCallback) { Log.d(LOG_TAG, "initial sync handleJoinedRoomSync " + getRoomId()); } else { Log.d(LOG_TAG, "handleJoinedRoomSync " + getRoomId()); } mIsSyncing = true; synchronized (this) { mLiveTimeline.handleJoinedRoomSync(roomSync, isInitialSync); // ephemeral events if ((null != roomSync.ephemeral) && (null != roomSync.ephemeral.events)) { handleEphemeralEvents(roomSync.ephemeral.events); } // Handle account data events (if any) if (null != roomSync.accountData) { handleAccountDataEvents(roomSync.accountData.events); } } // the user joined the room // With V2 sync, the server sends the events to init the room. if (null != mOnInitialSyncCallback) { try { Log.d(LOG_TAG, "handleJoinedRoomSync " + getRoomId() + " : the initial sync is done"); mOnInitialSyncCallback.onSuccess(null); } catch (Exception e) { Log.e(LOG_TAG, "handleJoinedRoomSync : onSuccess failed" + e.getLocalizedMessage()); } mOnInitialSyncCallback = null; } mIsSyncing = false; if (mRefreshUnreadAfterSync) { refreshUnreadCounter(); mRefreshUnreadAfterSync = false; } } /** * Handle the invitation room events * * @param invitedRoomSync the invitation room events. */ public void handleInvitedRoomSync(InvitedRoomSync invitedRoomSync) { mLiveTimeline.handleInvitedRoomSync(invitedRoomSync); } /** * Store an outgoing event. * * @param event the event. */ public void storeOutgoingEvent(Event event) { mLiveTimeline.storeOutgoingEvent(event); } /** * Request events to the server. The local cache is not used. * The events will not be saved in the local storage. * * @param token the token to go back from. * @param paginationCount the number of events to retrieve. * @param callback the onComplete callback */ public void requestServerRoomHistory(final String token, final int paginationCount, final ApiCallback<TokensChunkResponse<Event>> callback) { mDataHandler.getDataRetriever().requestServerRoomHistory(getRoomId(), token, paginationCount, new SimpleApiCallback<TokensChunkResponse<Event>>(callback) { @Override public void onSuccess(TokensChunkResponse<Event> info) { callback.onSuccess(info); } }); } /** * cancel any remote request */ public void cancelRemoteHistoryRequest() { mDataHandler.getDataRetriever().cancelRemoteHistoryRequest(getRoomId()); } //================================================================================ // Getters / setters //================================================================================ public String getRoomId() { return mLiveTimeline.getState().roomId; } public void setAccountData(RoomAccountData accountData) { this.mAccountData = accountData; } public RoomAccountData getAccountData() { return this.mAccountData; } public RoomState getState() { return mLiveTimeline.getState(); } public RoomState getLiveState() { return getState(); } public boolean isLeaving() { return mIsLeaving; } public Collection<RoomMember> getMembers() { return getState().getMembers(); } public EventTimeline getLiveTimeLine() { return mLiveTimeline; } public void setLiveTimeline(EventTimeline eventTimeline) { mLiveTimeline = eventTimeline; } public void setReadyState(boolean isReady) { mIsReady = isReady; } public boolean isReady() { return mIsReady; } /** * @return the list of active members in a room ie joined or invited ones. */ public Collection<RoomMember> getActiveMembers() { Collection<RoomMember> members = getState().getMembers(); List<RoomMember> activeMembers = new ArrayList<>(); String conferenceUserId = MXCallsManager.getConferenceUserId(getRoomId()); for (RoomMember member : members) { if (!TextUtils.equals(member.getUserId(), conferenceUserId)) { if (TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_JOIN) || TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_INVITE)) { activeMembers.add(member); } } } return activeMembers; } /** * Get the list of the members who have joined the room. * * @return the list the joined members of the room. */ public Collection<RoomMember> getJoinedMembers() { Collection<RoomMember> membersList = getState().getMembers(); List<RoomMember> joinedMembersList = new ArrayList<>(); for (RoomMember member : membersList) { if (TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_JOIN)) { joinedMembersList.add(member); } } return joinedMembersList; } public RoomMember getMember(String userId) { return getState().getMember(userId); } public String getTopic() { return this.getState().topic; } public String getName(String selfUserId) { return getState().getDisplayName(selfUserId); } public String getVisibility() { return getState().visibility; } /** * @return true if the user is invited to the room */ public boolean isInvited() { // Is it an initial sync for this room ? RoomState state = getState(); String membership = null; RoomMember selfMember = state.getMember(mMyUserId); if (null != selfMember) { membership = selfMember.membership; } return TextUtils.equals(membership, RoomMember.MEMBERSHIP_INVITE); } /** * @return true if the user is invited in a direct chat room */ public boolean isDirectChatInvitation() { if (isInvited()) { // Is it an initial sync for this room ? RoomState state = getState(); RoomMember selfMember = state.getMember(mMyUserId); if ((null != selfMember) && (null != selfMember.is_direct)) { return selfMember.is_direct; } } return false; } //================================================================================ // Join //================================================================================ /** * Defines the initial sync callback * * @param callback the new callback. */ public void setOnInitialSyncCallback(ApiCallback<Void> callback) { mOnInitialSyncCallback = callback; } /** * Join a room with an url to post before joined the room. * * @param alias the room alias * @param thirdPartySignedUrl the thirdPartySigned url * @param callback the callback */ public void joinWithThirdPartySigned(final String alias, final String thirdPartySignedUrl, final ApiCallback<Void> callback) { if (null == thirdPartySignedUrl) { join(alias, callback); } else { String url = thirdPartySignedUrl + "&mxid=" + mMyUserId; UrlPostTask task = new UrlPostTask(); task.setListener(new UrlPostTask.IPostTaskListener() { @Override public void onSucceed(JsonObject object) { HashMap<String, Object> map = null; try { map = new Gson().fromJson(object, new TypeToken<HashMap<String, Object>>() { }.getType()); } catch (Exception e) { Log.e(LOG_TAG, "joinWithThirdPartySigned : Gson().fromJson failed" + e.getLocalizedMessage()); } if (null != map) { HashMap<String, Object> joinMap = new HashMap<>(); joinMap.put("third_party_signed", map); join(alias, joinMap, callback); } else { join(callback); } } @Override public void onError(String errorMessage) { Log.d(LOG_TAG, "joinWithThirdPartySigned failed " + errorMessage); // cannot validate the url // try without validating the url join(callback); } }); // avoid crash if there are too many running task try { task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url); } catch (RejectedExecutionException rejectedExecutionException) { Log.e(LOG_TAG, "joinWithThirdPartySigned : task.executeOnExecutor failed" + rejectedExecutionException.getLocalizedMessage()); } catch (Exception e) { Log.e(LOG_TAG, "joinWithThirdPartySigned : task.executeOnExecutor failed" + e.getLocalizedMessage()); } } } /** * Join the room. If successful, the room's current state will be loaded before calling back onComplete. * * @param callback the callback for when done */ public void join(final ApiCallback<Void> callback) { join(null, null, callback); } /** * Join the room. If successful, the room's current state will be loaded before calling back onComplete. * * @param roomAlias the room alias * @param callback the callback for when done */ private void join(String roomAlias, ApiCallback<Void> callback) { join(roomAlias, null, callback); } /** * Join the room. If successful, the room's current state will be loaded before calling back onComplete. * * @param roomAlias the room alias * @param extraParams the join extra params * @param callback the callback for when done */ private void join(String roomAlias, HashMap<String, Object> extraParams, final ApiCallback<Void> callback) { Log.d(LOG_TAG, "Join the room " + getRoomId() + " with alias " + roomAlias); mDataHandler.getDataRetriever().getRoomsRestClient().joinRoom((null != roomAlias) ? roomAlias : getRoomId(), extraParams, new SimpleApiCallback<RoomResponse>(callback) { @Override public void onSuccess(final RoomResponse aResponse) { try { boolean isRoomMember; synchronized (this) { isRoomMember = (getState().getMember(mMyUserId) != null); } // the join request did not get the room initial history if (!isRoomMember) { Log.d(LOG_TAG, "the room " + getRoomId() + " is joined but wait after initial sync"); // wait the server sends the events chunk before calling the callback setOnInitialSyncCallback(callback); } else { Log.d(LOG_TAG, "the room " + getRoomId() + " is joined : the initial sync has been done"); // already got the initial sync callback.onSuccess(null); } } catch (Exception e) { Log.e(LOG_TAG, "join exception " + e.getMessage()); } } @Override public void onNetworkError(Exception e) { Log.e(LOG_TAG, "join onNetworkError " + e.getLocalizedMessage()); callback.onNetworkError(e); } @Override public void onMatrixError(MatrixError e) { Log.e(LOG_TAG, "join onMatrixError " + e.getLocalizedMessage()); if (MatrixError.UNKNOWN.equals(e.errcode) && TextUtils.equals("No known servers", e.error)) { // minging kludge until https://matrix.org/jira/browse/SYN-678 is fixed // 'Error when trying to join an empty room should be more explicit e.error = mStore.getContext().getString(org.matrix.androidsdk.R.string.room_error_join_failed_empty_room); } callback.onMatrixError(e); } @Override public void onUnexpectedError(Exception e) { Log.e(LOG_TAG, "join onUnexpectedError " + e.getLocalizedMessage()); callback.onUnexpectedError(e); } }); } /** * @return true if the user joined the room */ private boolean selfJoined() { RoomMember roomMember = getMember(mMyUserId); // send the event only if the user has joined the room. return ((null != roomMember) && RoomMember.MEMBERSHIP_JOIN.equals(roomMember.membership)); } //================================================================================ // Room info (liveState) update //================================================================================ /** * This class dispatches the error to the dedicated callbacks. * If the operation succeeds, the room state is saved because calling the callback. */ private class RoomInfoUpdateCallback<T> implements ApiCallback<T> { private final ApiCallback<T> mCallback; /** * Constructor */ public RoomInfoUpdateCallback(ApiCallback<T> callback) { mCallback = callback; } @Override public void onSuccess(T info) { mStore.storeLiveStateForRoom(getRoomId()); if (null != mCallback) { mCallback.onSuccess(info); } } @Override public void onNetworkError(Exception e) { if (null != mCallback) { mCallback.onNetworkError(e); } } @Override public void onMatrixError(final MatrixError e) { if (null != mCallback) { mCallback.onMatrixError(e); } } @Override public void onUnexpectedError(final Exception e) { if (null != mCallback) { mCallback.onUnexpectedError(e); } } } /** * Update the power level of the user userId * * @param userId the user id * @param powerLevel the new power level * @param callback the callback with the created event */ public void updateUserPowerLevels(String userId, int powerLevel, ApiCallback<Void> callback) { PowerLevels powerLevels = getState().getPowerLevels().deepCopy(); powerLevels.setUserPowerLevel(userId, powerLevel); mDataHandler.getDataRetriever().getRoomsRestClient().updatePowerLevels(getRoomId(), powerLevels, callback); } /** * Update the room's name. * * @param name the new name * @param callback the async callback */ public void updateName(final String name, final ApiCallback<Void> callback) { mDataHandler.getDataRetriever().getRoomsRestClient().updateRoomName(getRoomId(), name, new RoomInfoUpdateCallback<Void>(callback) { @Override public void onSuccess(Void info) { getState().name = name; super.onSuccess(info); } }); } /** * Update the room's topic. * * @param topic the new topic * @param callback the async callback */ public void updateTopic(final String topic, final ApiCallback<Void> callback) { mDataHandler.getDataRetriever().getRoomsRestClient().updateTopic(getRoomId(), topic, new RoomInfoUpdateCallback<Void>(callback) { @Override public void onSuccess(Void info) { getState().topic = topic; super.onSuccess(info); } }); } /** * Update the room's main alias. * * @param canonicalAlias the canonical alias * @param callback the async callback */ public void updateCanonicalAlias(final String canonicalAlias, final ApiCallback<Void> callback) { mDataHandler.getDataRetriever().getRoomsRestClient().updateCanonicalAlias(getRoomId(), canonicalAlias, new RoomInfoUpdateCallback<Void>(callback) { @Override public void onSuccess(Void info) { getState().roomAliasName = canonicalAlias; super.onSuccess(info); } }); } /** * Provides the room aliases list. * The result is never null. * * @return the room aliases list. */ public List<String> getAliases() { return getLiveState().getAliases(); } /** * Remove a room alias. * * @param alias the alias to remove * @param callback the async callback */ public void removeAlias(final String alias, final ApiCallback<Void> callback) { final List<String> updatedAliasesList = new ArrayList<>(getAliases()); // nothing to do if (TextUtils.isEmpty(alias) || (updatedAliasesList.indexOf(alias) < 0)) { if (null != callback) { callback.onSuccess(null); } return; } mDataHandler.getDataRetriever().getRoomsRestClient().removeRoomAlias(alias, new RoomInfoUpdateCallback<Void>(callback) { @Override public void onSuccess(Void info) { getState().removeAlias(alias); super.onSuccess(info); } }); } /** * Try to add an alias to the aliases list. * * @param alias the alias to add. * @param callback the the async callback */ public void addAlias(final String alias, final ApiCallback<Void> callback) { final List<String> updatedAliasesList = new ArrayList<>(getAliases()); // nothing to do if (TextUtils.isEmpty(alias) || (updatedAliasesList.indexOf(alias) >= 0)) { if (null != callback) { callback.onSuccess(null); } return; } mDataHandler.getDataRetriever().getRoomsRestClient().setRoomIdByAlias(getRoomId(), alias, new RoomInfoUpdateCallback<Void>(callback) { @Override public void onSuccess(Void info) { getState().addAlias(alias); super.onSuccess(info); } }); } /** * @return the room avatar URL. If there is no defined one, use the members one (1:1 chat only). */ public String getAvatarUrl() { String res = getState().getAvatarUrl(); // detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat) if (null == res) { List<RoomMember> members = new ArrayList<>(getState().getMembers()); if (members.size() == 1) { res = members.get(0).avatarUrl; } else if (members.size() == 2) { RoomMember m1 = members.get(0); RoomMember m2 = members.get(1); res = TextUtils.equals(m1.getUserId(), mMyUserId) ? m2.avatarUrl : m1.avatarUrl; } } return res; } /** * The call avatar is the same as the room avatar except there are only 2 JOINED members. * In this case, it returns the avtar of the other joined member. * * @return the call avatar URL. */ public String getCallAvatarUrl() { String avatarURL; List<RoomMember> joinedMembers = new ArrayList<>(getJoinedMembers()); // 2 joined members case if (2 == joinedMembers.size()) { // use other member avatar. if (TextUtils.equals(mMyUserId, joinedMembers.get(0).getUserId())) { avatarURL = joinedMembers.get(1).avatarUrl; } else { avatarURL = joinedMembers.get(0).avatarUrl; } } else { // avatarURL = getAvatarUrl(); } return avatarURL; } /** * Update the room avatar URL. * * @param avatarUrl the new avatar URL * @param callback the async callback */ public void updateAvatarUrl(final String avatarUrl, final ApiCallback<Void> callback) { mDataHandler.getDataRetriever().getRoomsRestClient().updateAvatarUrl(getRoomId(), avatarUrl, new RoomInfoUpdateCallback<Void>(callback) { @Override public void onSuccess(Void info) { getState().url = avatarUrl; super.onSuccess(info); } }); } /** * Update the room's history visibility * * @param historyVisibility the visibility (should be one of RoomState.HISTORY_VISIBILITY_XX values) * @param callback the async callback */ public void updateHistoryVisibility(final String historyVisibility, final ApiCallback<Void> callback) { mDataHandler.getDataRetriever().getRoomsRestClient().updateHistoryVisibility(getRoomId(), historyVisibility, new RoomInfoUpdateCallback<Void>(callback) { @Override public void onSuccess(Void info) { getState().history_visibility = historyVisibility; super.onSuccess(info); } }); } /** * Update the directory's visibility * * @param visibility the visibility (should be one of RoomState.HISTORY_VISIBILITY_XX values) * @param callback the async callback */ public void updateDirectoryVisibility(final String visibility, final ApiCallback<Void> callback) { mDataHandler.getDataRetriever().getRoomsRestClient().updateDirectoryVisibility(getRoomId(), visibility, new RoomInfoUpdateCallback<Void>(callback) { @Override public void onSuccess(Void info) { getState().visibility = visibility; super.onSuccess(info); } }); } /** * Get the directory visibility of the room (see {@link #updateDirectoryVisibility(String, ApiCallback)}). * The directory visibility indicates if the room is listed among the directory list. * * @param roomId the user Id. * @param callback the callback returning the visibility response value. */ public void getDirectoryVisibility(final String roomId, final ApiCallback<String> callback) { RoomsRestClient roomRestApi = mDataHandler.getDataRetriever().getRoomsRestClient(); if (null != roomRestApi) { roomRestApi.getDirectoryVisibility(roomId, new ApiCallback<RoomState>() { @Override public void onSuccess(RoomState roomState) { RoomState currentRoomState = getState(); if (null != currentRoomState) { currentRoomState.visibility = roomState.visibility; } if (null != callback) { callback.onSuccess(roomState.visibility); } } @Override public void onNetworkError(Exception e) { if (null != callback) { callback.onNetworkError(e); } } @Override public void onMatrixError(MatrixError e) { if (null != callback) { callback.onMatrixError(e); } } @Override public void onUnexpectedError(Exception e) { if (null != callback) { callback.onUnexpectedError(e); } } }); } } /** * Update the join rule of the room. * * @param aRule the join rule: {@link RoomState#JOIN_RULE_PUBLIC} or {@link RoomState#JOIN_RULE_INVITE} * @param aCallBackResp the async callback */ public void updateJoinRules(final String aRule, final ApiCallback<Void> aCallBackResp) { mDataHandler.getDataRetriever().getRoomsRestClient().updateJoinRules(getRoomId(), aRule, new RoomInfoUpdateCallback<Void>(aCallBackResp) { @Override public void onSuccess(Void info) { getState().join_rule = aRule; super.onSuccess(info); } }); } /** * Update the guest access rule of the room. * To deny guest access to the room, aGuestAccessRule must be set to {@link RoomState#GUEST_ACCESS_FORBIDDEN}. * * @param aGuestAccessRule the guest access rule: {@link RoomState#GUEST_ACCESS_CAN_JOIN} or {@link RoomState#GUEST_ACCESS_FORBIDDEN} * @param callback the async callback */ public void updateGuestAccess(final String aGuestAccessRule, final ApiCallback<Void> callback) { mDataHandler.getDataRetriever().getRoomsRestClient().updateGuestAccess(getRoomId(), aGuestAccessRule, new RoomInfoUpdateCallback<Void>(callback) { @Override public void onSuccess(Void info) { getState().guest_access = aGuestAccessRule; super.onSuccess(info); } }); } //================================================================================ // Read receipts events //================================================================================ /** * @return the call conference user id */ private String getCallConferenceUserId() { if (null == mCallConferenceUserId) { mCallConferenceUserId = MXCallsManager.getConferenceUserId(getRoomId()); } return mCallConferenceUserId; } /** * Handle a receiptData. * * @param receiptData the receiptData. * @return true if there a store update. */ public boolean handleReceiptData(ReceiptData receiptData) { if (!TextUtils.equals(receiptData.userId, getCallConferenceUserId())) { boolean isUpdated = mStore.storeReceipt(receiptData, getRoomId()); // check oneself receipts // if there is an update, it means that the messages have been read from another client // it requires to update the summary to display valid information. if (isUpdated && TextUtils.equals(mMyUserId, receiptData.userId)) { RoomSummary summary = mStore.getSummary(getRoomId()); if (null != summary) { summary.setLatestReadEventId(receiptData.eventId); } refreshUnreadCounter(); } return isUpdated; } else { return false; } } /** * Handle receipt event. * * @param event the event receipts. * @return the sender user IDs list. */ private List<String> handleReceiptEvent(Event event) { List<String> senderIDs = new ArrayList<>(); try { // the receipts dictionnaries // key : $EventId // value : dict key $UserId // value dict key ts // dict value ts value Type type = new TypeToken<HashMap<String, HashMap<String, HashMap<String, HashMap<String, Object>>>>>() { }.getType(); HashMap<String, HashMap<String, HashMap<String, HashMap<String, Object>>>> receiptsDict = gson.fromJson(event.getContent(), type); for (String eventId : receiptsDict.keySet()) { HashMap<String, HashMap<String, HashMap<String, Object>>> receiptDict = receiptsDict.get(eventId); for (String receiptType : receiptDict.keySet()) { // only the read receipts are managed if (TextUtils.equals(receiptType, "m.read")) { HashMap<String, HashMap<String, Object>> userIdsDict = receiptDict.get(receiptType); for (String userID : userIdsDict.keySet()) { HashMap<String, Object> paramsDict = userIdsDict.get(userID); for (String paramName : paramsDict.keySet()) { if (TextUtils.equals("ts", paramName)) { Double value = (Double) paramsDict.get(paramName); long ts = value.longValue(); if (handleReceiptData(new ReceiptData(userID, eventId, ts))) { senderIDs.add(userID); } } } } } } } } catch (Exception e) { Log.e(LOG_TAG, "handleReceiptEvent : failed" + e.getLocalizedMessage()); } return senderIDs; } /** * Clear the unread message counters * * @param summary the room summary */ private void clearUnreadCounters(RoomSummary summary) { // reset the notification count getLiveState().setHighlightCount(0); getLiveState().setNotificationCount(0); mStore.storeLiveStateForRoom(getRoomId()); // flush the summary if (null != summary) { summary.setUnreadEventsCount(0); mStore.flushSummary(summary); } mStore.commit(); } /** * Send the read receipt to the latest room message id. * * @param aRespCallback asynchronous response callback * @return true if the read receipt has been sent, false otherwise */ public boolean sendReadReceipt(final ApiCallback<Void> aRespCallback) { boolean res = sendReadReceipt(null, aRespCallback); // if the request is not sent, ensure that the counters are cleared if (!res) { RoomSummary summary = mDataHandler.getStore().getSummary(getRoomId()); if ((null != summary) && (0 != summary.getUnreadEventsCount())) { Log.e(LOG_TAG, "## sendReadReceipt() : the unread message count for " + getRoomId() + " should have been cleared"); summary.setUnreadEventsCount(0); } if ((0 != getLiveState().getNotificationCount()) || (0 != getLiveState().getHighlightCount())) { Log.e(LOG_TAG, "## sendReadReceipt() : the notification messages count for " + getRoomId() + " should have been cleared"); getLiveState().setNotificationCount(0); getLiveState().setHighlightCount(0); mDataHandler.getStore().storeLiveStateForRoom(getRoomId()); } } return res; } /** * Send the read receipt to a dedicated event. * * @param anEvent the event to acknowledge * @param aRespCallback asynchronous response callback * @return true if the read receipt request is sent, false otherwise */ public boolean sendReadReceipt(Event anEvent, final ApiCallback<Void> aRespCallback) { final Event lastEvent = mStore.getLatestEvent(getRoomId()); final Event fEvent; // the event is provided if (null != anEvent) { Log.d(LOG_TAG, "## sendReadReceipt(): roomId=" + getRoomId() + " to " + anEvent.eventId); // test if the message has already be read if (getDataHandler().getStore().isEventRead(getRoomId(), getDataHandler().getUserId(), anEvent.eventId)) { Log.d(LOG_TAG, "## sendReadReceipt(): the message was already read"); return false; } else { fEvent = anEvent; } } else { Log.d(LOG_TAG, "## sendReadReceipt(): roomId=" + getRoomId() + " to the latest event"); fEvent = lastEvent; } if (null == fEvent) { Log.e(LOG_TAG, "## sendReadReceipt(): there is no latest message"); return false; } boolean isSendReadReceiptSent = false; // save the up to date status // don't wait that the operation is done // because it could display invalid unread messages counters // while sending it. if (handleReceiptData(new ReceiptData(mMyUserId, fEvent.eventId, System.currentTimeMillis()))) { Log.d(LOG_TAG, "## sendReadReceipt(): send the read receipt"); isSendReadReceiptSent = true; mDataHandler.getDataRetriever().getRoomsRestClient().sendReadReceipt(getRoomId(), fEvent.eventId, new ApiCallback<Void>() { @Override public void onSuccess(Void info) { Log.d(LOG_TAG, "## sendReadReceipt(): succeeds - eventId " + fEvent.eventId); if (null != aRespCallback) { aRespCallback.onSuccess(info); } } @Override public void onNetworkError(Exception e) { Log.e(LOG_TAG, "sendReadReceipt - eventId " + fEvent.eventId + " failed " + e.getLocalizedMessage()); if (null != aRespCallback) { aRespCallback.onNetworkError(e); } } @Override public void onMatrixError(MatrixError e) { Log.e(LOG_TAG, "sendReadReceipt - eventId " + fEvent.eventId + " failed " + e.getLocalizedMessage()); if (null != aRespCallback) { aRespCallback.onMatrixError(e); } } @Override public void onUnexpectedError(Exception e) { Log.e(LOG_TAG, "sendReadReceipt - eventId " + fEvent.eventId + " failed " + e.getLocalizedMessage()); if (null != aRespCallback) { aRespCallback.onUnexpectedError(e); } } }); // Clear the unread counters if the latest message is displayed // We don't try to compute the unread counters for oldest messages : // ---> it would require too much time. // The counters are cleared to avoid displaying invalid values // when the device is offline. // The read receipts will be sent later // (asap there is a valid network connection) if (TextUtils.equals(lastEvent.eventId, fEvent.eventId)) { clearUnreadCounters(mStore.getSummary(getRoomId())); } } else { Log.d(LOG_TAG, "## sendReadReceipt(): don't send the read receipt"); } return isSendReadReceiptSent; } /** * Check if an event has been read. * * @param eventId the event id * @return true if the message has been read */ public boolean isEventRead(String eventId) { return mStore.isEventRead(getRoomId(), mMyUserId, eventId); } //================================================================================ // Unread event count management //================================================================================ /** * @return the number of unread messages that match the push notification rules. */ public int getNotificationCount() { return getState().getNotificationCount(); } /** * @return the number of highlighted events. */ public int getHighlightCount() { return getState().getHighlightCount(); } /** * refresh the unread events counts. */ public void refreshUnreadCounter() { // avoid refreshing the unread counter while processing a bunch of messages. if (!mIsSyncing) { RoomSummary summary = mStore.getSummary(getRoomId()); if (null != summary) { int prevValue = summary.getUnreadEventsCount(); int newValue = mStore.eventsCountAfter(getRoomId(), summary.getLatestReadEventId()); if (prevValue != newValue) { summary.setUnreadEventsCount(newValue); mStore.flushSummary(summary); mStore.commit(); } } } else { // wait the sync end before computing is again mRefreshUnreadAfterSync = true; } } //================================================================================ // typing events //================================================================================ // userIds list private List<String> mTypingUsers = new ArrayList<>(); /** * Get typing users * * @return the userIds list */ public List<String> getTypingUsers() { List<String> typingUsers; synchronized (Room.this) { typingUsers = (null == mTypingUsers) ? new ArrayList<String>() : new ArrayList<>(mTypingUsers); } return typingUsers; } /** * Send a typing notification * * @param isTyping typing status * @param timeout the typing timeout */ public void sendTypingNotification(boolean isTyping, int timeout, ApiCallback<Void> callback) { // send the event only if the user has joined the room. if (selfJoined()) { mDataHandler.getDataRetriever().getRoomsRestClient().sendTypingNotification(getRoomId(), mMyUserId, isTyping, timeout, callback); } } //================================================================================ // Medias events //================================================================================ /** * Fill the locationInfo * * @param context the context * @param locationMessage the location message * @param thumbnailUri the thumbnail uri * @param thumbMimeType the thumbnail mime type */ public static void fillLocationInfo(Context context, LocationMessage locationMessage, Uri thumbnailUri, String thumbMimeType) { if (null != thumbnailUri) { try { locationMessage.thumbnail_url = thumbnailUri.toString(); ThumbnailInfo thumbInfo = new ThumbnailInfo(); File thumbnailFile = new File(thumbnailUri.getPath()); ExifInterface exifMedia = new ExifInterface(thumbnailUri.getPath()); String sWidth = exifMedia.getAttribute(ExifInterface.TAG_IMAGE_WIDTH); String sHeight = exifMedia.getAttribute(ExifInterface.TAG_IMAGE_LENGTH); if (null != sWidth) { thumbInfo.w = Integer.parseInt(sWidth); } if (null != sHeight) { thumbInfo.h = Integer.parseInt(sHeight); } thumbInfo.size = Long.valueOf(thumbnailFile.length()); thumbInfo.mimetype = thumbMimeType; locationMessage.thumbnail_info = thumbInfo; } catch (Exception e) { Log.e(LOG_TAG, "fillLocationInfo : failed" + e.getLocalizedMessage()); } } } /** * Fills the VideoMessage info. * * @param context Application context for the content resolver. * @param videoMessage The VideoMessage to fill. * @param fileUri The file uri. * @param videoMimeType The mimeType * @param thumbnailUri the thumbnail uri * @param thumbMimeType the thumbnail mime type */ public static void fillVideoInfo(Context context, VideoMessage videoMessage, Uri fileUri, String videoMimeType, Uri thumbnailUri, String thumbMimeType) { try { VideoInfo videoInfo = new VideoInfo(); File file = new File(fileUri.getPath()); MediaMetadataRetriever retriever = new MediaMetadataRetriever(); retriever.setDataSource(file.getAbsolutePath()); Bitmap bmp = retriever.getFrameAtTime(); videoInfo.h = bmp.getHeight(); videoInfo.w = bmp.getWidth(); videoInfo.mimetype = videoMimeType; try { MediaPlayer mp = MediaPlayer.create(context, fileUri); if (null != mp) { videoInfo.duration = Long.valueOf(mp.getDuration()); mp.release(); } } catch (Exception e) { Log.e(LOG_TAG, "fillVideoInfo : MediaPlayer.create failed" + e.getLocalizedMessage()); } videoInfo.size = file.length(); // thumbnail if (null != thumbnailUri) { videoInfo.thumbnail_url = thumbnailUri.toString(); ThumbnailInfo thumbInfo = new ThumbnailInfo(); File thumbnailFile = new File(thumbnailUri.getPath()); ExifInterface exifMedia = new ExifInterface(thumbnailUri.getPath()); String sWidth = exifMedia.getAttribute(ExifInterface.TAG_IMAGE_WIDTH); String sHeight = exifMedia.getAttribute(ExifInterface.TAG_IMAGE_LENGTH); if (null != sWidth) { thumbInfo.w = Integer.parseInt(sWidth); } if (null != sHeight) { thumbInfo.h = Integer.parseInt(sHeight); } thumbInfo.size = Long.valueOf(thumbnailFile.length()); thumbInfo.mimetype = thumbMimeType; videoInfo.thumbnail_info = thumbInfo; } videoMessage.info = videoInfo; } catch (Exception e) { Log.e(LOG_TAG, "fillVideoInfo : failed" + e.getLocalizedMessage()); } } /** * Fills the fileMessage fileInfo. * * @param context Application context for the content resolver. * @param fileMessage The fileMessage to fill. * @param fileUri The file uri. * @param mimeType The mimeType */ public static void fillFileInfo(Context context, FileMessage fileMessage, Uri fileUri, String mimeType) { try { FileInfo fileInfo = new FileInfo(); String filename = fileUri.getPath(); File file = new File(filename); fileInfo.mimetype = mimeType; fileInfo.size = file.length(); fileMessage.info = fileInfo; } catch (Exception e) { Log.e(LOG_TAG, "fillFileInfo : failed" + e.getLocalizedMessage()); } } /** * Define ImageInfo for an image uri * * @param context Application context for the content resolver. * @param imageUri The full size image uri. * @param mimeType The image mimeType */ public static ImageInfo getImageInfo(Context context, Uri imageUri, String mimeType) { ImageInfo imageInfo = new ImageInfo(); try { String filename = imageUri.getPath(); File file = new File(filename); ExifInterface exifMedia = new ExifInterface(filename); String sWidth = exifMedia.getAttribute(ExifInterface.TAG_IMAGE_WIDTH); String sHeight = exifMedia.getAttribute(ExifInterface.TAG_IMAGE_LENGTH); // the image rotation is replaced by orientation // imageInfo.rotation = ImageUtils.getRotationAngleForBitmap(context, imageUri); imageInfo.orientation = ImageUtils.getOrientationForBitmap(context, imageUri); int width = 0; int height = 0; // extract the Exif info if ((null != sWidth) && (null != sHeight)) { if ((imageInfo.orientation == ExifInterface.ORIENTATION_TRANSPOSE) || (imageInfo.orientation == ExifInterface.ORIENTATION_ROTATE_90) || (imageInfo.orientation == ExifInterface.ORIENTATION_TRANSVERSE) || (imageInfo.orientation == ExifInterface.ORIENTATION_ROTATE_270)) { height = Integer.parseInt(sWidth); width = Integer.parseInt(sHeight); } else { width = Integer.parseInt(sWidth); height = Integer.parseInt(sHeight); } } // there is no exif info or the size is invalid if ((0 == width) || (0 == height)) { try { BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inJustDecodeBounds = true; BitmapFactory.decodeFile(imageUri.getPath(), opts); // don't need to load the bitmap in memory if ((opts.outHeight > 0) && (opts.outWidth > 0)) { width = opts.outWidth; height = opts.outHeight; } } catch (Exception e) { Log.e(LOG_TAG, "fillImageInfo : failed" + e.getLocalizedMessage()); } catch (OutOfMemoryError oom) { Log.e(LOG_TAG, "fillImageInfo : oom"); } } // valid image size ? if ((0 != width) || (0 != height)) { imageInfo.w = width; imageInfo.h = height; } imageInfo.mimetype = mimeType; imageInfo.size = file.length(); } catch (Exception e) { Log.e(LOG_TAG, "fillImageInfo : failed" + e.getLocalizedMessage()); imageInfo = null; } return imageInfo; } /** * Fills the imageMessage imageInfo. * * @param context Application context for the content resolver. * @param imageMessage The imageMessage to fill. * @param imageUri The full size image uri. * @param mimeType The image mimeType */ public static void fillImageInfo(Context context, ImageMessage imageMessage, Uri imageUri, String mimeType) { imageMessage.info = getImageInfo(context, imageUri, mimeType); } /** * Fills the imageMessage imageInfo. * * @param context Application context for the content resolver. * @param imageMessage The imageMessage to fill. * @param imageUri The full size image uri. * @param mimeType The image mimeType */ public static void fillThumbnailInfo(Context context, ImageMessage imageMessage, Uri thumbUri, String mimeType) { imageMessage.thumbnailInfo = getImageInfo(context, thumbUri, mimeType); } //================================================================================ // Call //================================================================================ /** * Test if a call can be performed in this room. * * @return true if a call can be performed. */ public boolean canPerformCall() { return getActiveMembers().size() > 1; } /** * @return a list of callable members. */ public List<RoomMember> callees() { List<RoomMember> res = new ArrayList<>(); Collection<RoomMember> members = getMembers(); for (RoomMember m : members) { if (RoomMember.MEMBERSHIP_JOIN.equals(m.membership) && !mMyUserId.equals(m.getUserId())) { res.add(m); } } return res; } //================================================================================ // Account data management //================================================================================ /** * Handle private user data events. * * @param accountDataEvents the account events. */ private void handleAccountDataEvents(List<Event> accountDataEvents) { if ((null != accountDataEvents) && (accountDataEvents.size() > 0)) { // manage the account events for (Event accountDataEvent : accountDataEvents) { mAccountData.handleEvent(accountDataEvent); if (accountDataEvent.getType().equals(Event.EVENT_TYPE_TAGS)) { mDataHandler.onRoomTagEvent(getRoomId()); } } mStore.storeAccountData(getRoomId(), mAccountData); } } /** * Add a tag to a room. * Use this method to update the order of an existing tag. * * @param tag the new tag to add to the room. * @param order the order. * @param callback the operation callback */ private void addTag(String tag, Double order, final ApiCallback<Void> callback) { // sanity check if ((null != tag) && (null != order)) { mDataHandler.getDataRetriever().getRoomsRestClient().addTag(getRoomId(), tag, order, callback); } else { if (null != callback) { callback.onSuccess(null); } } } /** * Remove a tag to a room. * * @param tag the new tag to add to the room. * @param callback the operation callback. */ private void removeTag(String tag, final ApiCallback<Void> callback) { // sanity check if (null != tag) { mDataHandler.getDataRetriever().getRoomsRestClient().removeTag(getRoomId(), tag, callback); } else { if (null != callback) { callback.onSuccess(null); } } } /** * Remove a tag and add another one. * * @param oldTag the tag to remove. * @param newTag the new tag to add. Nil can be used. Then, no new tag will be added. * @param newTagOrder the order of the new tag. * @param callback the operation callback. */ public void replaceTag(final String oldTag, final String newTag, final Double newTagOrder, final ApiCallback<Void> callback) { // remove tag if ((null != oldTag) && (null == newTag)) { removeTag(oldTag, callback); } // define a tag or define a new order else if (((null == oldTag) && (null != newTag)) || TextUtils.equals(oldTag, newTag)) { addTag(newTag, newTagOrder, callback); } else { removeTag(oldTag, new ApiCallback<Void>() { @Override public void onSuccess(Void info) { addTag(newTag, newTagOrder, callback); } @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); } }); } } //============================================================================================================== // Room events dispatcher //============================================================================================================== /** * Add an event listener to this room. Only events relative to the room will come down. * * @param eventListener the event listener to add */ public void addEventListener(final IMXEventListener eventListener) { // sanity check if (null == eventListener) { Log.e(LOG_TAG, "addEventListener : eventListener is null"); return; } // GA crash : should never happen but got it. if (null == mDataHandler) { Log.e(LOG_TAG, "addEventListener : mDataHandler is null"); return; } // Create a global listener that we'll add to the data handler IMXEventListener globalListener = new MXEventListener() { @Override public void onPresenceUpdate(Event event, User user) { // Only pass event through if the user is a member of the room if (getMember(user.user_id) != null) { try { eventListener.onPresenceUpdate(event, user); } catch (Exception e) { Log.e(LOG_TAG, "onPresenceUpdate exception " + e.getMessage()); } } } @Override public void onLiveEvent(Event event, RoomState roomState) { // Filter out events for other rooms and events while we are joining (before the room is ready) if (TextUtils.equals(getRoomId(), event.roomId) && mIsReady) { if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_TYPING)) { // Typing notifications events are not room messages nor room state events // They are just volatile information JsonObject eventContent = event.getContentAsJsonObject(); if (eventContent.has("user_ids")) { synchronized (Room.this) { mTypingUsers = null; try { mTypingUsers = (new Gson()).fromJson(eventContent.get("user_ids"), new TypeToken<List<String>>() { }.getType()); } catch (Exception e) { Log.e(LOG_TAG, "onLiveEvent exception " + e.getMessage()); } // avoid null list if (null == mTypingUsers) { mTypingUsers = new ArrayList<>(); } } } } try { eventListener.onLiveEvent(event, roomState); } catch (Exception e) { Log.e(LOG_TAG, "onLiveEvent exception " + e.getMessage()); } } } @Override public void onLiveEventsChunkProcessed(String fromToken, String toToken) { try { eventListener.onLiveEventsChunkProcessed(fromToken, toToken); } catch (Exception e) { Log.e(LOG_TAG, "onLiveEventsChunkProcessed exception " + e.getMessage()); } } @Override public void onEventEncrypted(Event event) { // Filter out events for other rooms if (TextUtils.equals(getRoomId(), event.roomId)) { try { eventListener.onEventEncrypted(event); } catch (Exception e) { Log.e(LOG_TAG, "onEventEncrypted exception " + e.getMessage()); } } } @Override public void onEventDecrypted(Event event) { // Filter out events for other rooms if (TextUtils.equals(getRoomId(), event.roomId)) { try { eventListener.onEventDecrypted(event); } catch (Exception e) { Log.e(LOG_TAG, "onDecryptedEvent exception " + e.getMessage()); } } } @Override public void onSentEvent(Event event) { // Filter out events for other rooms if (TextUtils.equals(getRoomId(), event.roomId)) { try { eventListener.onSentEvent(event); } catch (Exception e) { Log.e(LOG_TAG, "onSentEvent exception " + e.getMessage()); } } } @Override public void onFailedSendingEvent(Event event) { // Filter out events for other rooms if (TextUtils.equals(getRoomId(), event.roomId)) { try { eventListener.onFailedSendingEvent(event); } catch (Exception e) { Log.e(LOG_TAG, "onFailedSendingEvent exception " + e.getMessage()); } } } @Override public void onRoomInitialSyncComplete(String roomId) { // Filter out events for other rooms if (TextUtils.equals(getRoomId(), roomId)) { try { eventListener.onRoomInitialSyncComplete(roomId); } catch (Exception e) { Log.e(LOG_TAG, "onRoomInitialSyncComplete exception " + e.getMessage()); } } } @Override public void onRoomInternalUpdate(String roomId) { // Filter out events for other rooms if (TextUtils.equals(getRoomId(), roomId)) { try { eventListener.onRoomInternalUpdate(roomId); } catch (Exception e) { Log.e(LOG_TAG, "onRoomInternalUpdate exception " + e.getMessage()); } } } @Override public void onNewRoom(String roomId) { // Filter out events for other rooms if (TextUtils.equals(getRoomId(), roomId)) { try { eventListener.onNewRoom(roomId); } catch (Exception e) { Log.e(LOG_TAG, "onNewRoom exception " + e.getMessage()); } } } @Override public void onJoinRoom(String roomId) { // Filter out events for other rooms if (TextUtils.equals(getRoomId(), roomId)) { try { eventListener.onJoinRoom(roomId); } catch (Exception e) { Log.e(LOG_TAG, "onJoinRoom exception " + e.getMessage()); } } } @Override public void onReceiptEvent(String roomId, List<String> senderIds) { // Filter out events for other rooms if (TextUtils.equals(getRoomId(), roomId)) { try { eventListener.onReceiptEvent(roomId, senderIds); } catch (Exception e) { Log.e(LOG_TAG, "onReceiptEvent exception " + e.getMessage()); } } } @Override public void onRoomTagEvent(String roomId) { // Filter out events for other rooms if (TextUtils.equals(getRoomId(), roomId)) { try { eventListener.onRoomTagEvent(roomId); } catch (Exception e) { Log.e(LOG_TAG, "onRoomTagEvent exception " + e.getMessage()); } } } @Override public void onRoomFlush(String roomId) { // Filter out events for other rooms if (TextUtils.equals(getRoomId(), roomId)) { try { eventListener.onRoomFlush(roomId); } catch (Exception e) { Log.e(LOG_TAG, "onRoomFlush exception " + e.getMessage()); } } } @Override public void onLeaveRoom(String roomId) { // Filter out events for other rooms if (TextUtils.equals(getRoomId(), roomId)) { try { eventListener.onLeaveRoom(roomId); } catch (Exception e) { Log.e(LOG_TAG, "onLeaveRoom exception " + e.getMessage()); } } } }; mEventListeners.put(eventListener, globalListener); // GA crash if (null != mDataHandler) { mDataHandler.addListener(globalListener); } } /** * Remove an event listener. * * @param eventListener the event listener to remove */ public void removeEventListener(IMXEventListener eventListener) { // sanity check if ((null != eventListener) && (null != mDataHandler)) { mDataHandler.removeListener(mEventListeners.get(eventListener)); mEventListeners.remove(eventListener); } } //============================================================================================================== // Send methods //============================================================================================================== /** * Send an event content to the room. * The event is updated with the data provided by the server * The provided event contains the error description. * * @param event the message * @param callback the callback with the created event */ public void sendEvent(final Event event, final ApiCallback<Void> callback) { // wait that the room is synced before sending messages if (!mIsReady || !selfJoined()) { event.mSentState = Event.SentState.WAITING_RETRY; try { callback.onNetworkError(null); } catch (Exception e) { Log.e(LOG_TAG, "sendEvent exception " + e.getMessage()); } return; } final ApiCallback<Event> localCB = new ApiCallback<Event>() { @Override public void onSuccess(final Event serverResponseEvent) { // remove the tmp event mStore.deleteEvent(event); // update the event with the server response event.mSentState = Event.SentState.SENT; event.eventId = serverResponseEvent.eventId; event.originServerTs = System.currentTimeMillis(); // the message echo is not yet echoed if (!mStore.doesEventExist(serverResponseEvent.eventId, getRoomId())) { mStore.storeLiveRoomEvent(event); } // send the dedicated read receipt asap sendReadReceipt(null); mStore.commit(); mDataHandler.onSentEvent(event); try { callback.onSuccess(null); } catch (Exception e) { Log.e(LOG_TAG, "sendEvent exception " + e.getMessage()); } } @Override public void onNetworkError(Exception e) { event.mSentState = Event.SentState.UNDELIVERABLE; event.unsentException = e; try { callback.onNetworkError(e); } catch (Exception anException) { Log.e(LOG_TAG, "sendEvent exception " + anException.getMessage()); } } @Override public void onMatrixError(MatrixError e) { event.mSentState = Event.SentState.UNDELIVERABLE; event.unsentMatrixError = e; if (TextUtils.equals(MatrixError.UNKNOWN_TOKEN, e.errcode)) { mDataHandler.onInvalidToken(); } else { try { callback.onMatrixError(e); } catch (Exception anException) { Log.e(LOG_TAG, "sendEvent exception " + anException.getMessage()); } } } @Override public void onUnexpectedError(Exception e) { event.mSentState = Event.SentState.UNDELIVERABLE; event.unsentException = e; try { callback.onUnexpectedError(e); } catch (Exception anException) { Log.e(LOG_TAG, "sendEvent exception " + anException.getMessage()); } } }; if (isEncrypted() && (null != mDataHandler.getCrypto())) { event.mSentState = Event.SentState.ENCRYPTING; // Encrypt the content before sending mDataHandler.getCrypto().encryptEventContent(event.getContent().getAsJsonObject(), event.getType(), this, new ApiCallback<MXEncryptEventContentResult>() { @Override public void onSuccess(MXEncryptEventContentResult encryptEventContentResult) { // update the event content with the encrypted data event.type = encryptEventContentResult.mEventType; event.updateContent(encryptEventContentResult.mEventContent.getAsJsonObject()); mDataHandler.getCrypto().decryptEvent(event, null); // warn the upper layer mDataHandler.onEventEncrypted(event); // sending in progress event.mSentState = Event.SentState.SENDING; mDataHandler.getDataRetriever().getRoomsRestClient().sendEventToRoom(event.originServerTs + "", getRoomId(), encryptEventContentResult.mEventType, encryptEventContentResult.mEventContent.getAsJsonObject(), localCB); } @Override public void onNetworkError(Exception e) { event.mSentState = Event.SentState.UNDELIVERABLE; event.unsentException = e; if (null != callback) { callback.onNetworkError(e); } } @Override public void onMatrixError(MatrixError e) { // update the sent state if the message encryption failed because there are unknown devices. if ((e instanceof MXCryptoError) && TextUtils.equals(((MXCryptoError) e).errcode, MXCryptoError.UNKNOWN_DEVICES_CODE)) { event.mSentState = Event.SentState.FAILED_UNKNOWN_DEVICES; } else { event.mSentState = Event.SentState.UNDELIVERABLE; } event.unsentMatrixError = e; if (null != callback) { callback.onMatrixError(e); } } @Override public void onUnexpectedError(Exception e) { event.mSentState = Event.SentState.UNDELIVERABLE; event.unsentException = e; if (null != callback) { callback.onUnexpectedError(e); } } }); } else { event.mSentState = Event.SentState.SENDING; if (Event.EVENT_TYPE_MESSAGE.equals(event.getType())) { mDataHandler.getDataRetriever().getRoomsRestClient().sendMessage(event.originServerTs + "", getRoomId(), JsonUtils.toMessage(event.getContent()), localCB); } else { mDataHandler.getDataRetriever().getRoomsRestClient().sendEventToRoom(event.originServerTs + "", getRoomId(), event.getType(), event.getContent().getAsJsonObject(), localCB); } } } /** * Cancel the event sending. * Any media upload will be cancelled too. * The event becomes undeliverable. * * @param event the message */ public void cancelEventSending(final Event event) { if (null != event) { if ((Event.SentState.UNSENT == event.mSentState) || (Event.SentState.SENDING == event.mSentState) || (Event.SentState.WAITING_RETRY == event.mSentState) || (Event.SentState.ENCRYPTING == event.mSentState)) { // the message cannot be sent anymore event.mSentState = Event.SentState.UNDELIVERABLE; } List<String> urls = event.getMediaUrls(); MXMediasCache cache = mDataHandler.getMediasCache(); for (String url : urls) { cache.cancelUpload(url); cache.cancelDownload(cache.downloadIdFromUrl(url)); } } } /** * Redact an event from the room. * * @param eventId the event's id * @param callback the callback with the redacted event */ public void redact(final String eventId, final ApiCallback<Event> callback) { mDataHandler.getDataRetriever().getRoomsRestClient().redactEvent(getRoomId(), eventId, new ApiCallback<Event>() { @Override public void onSuccess(Event event) { Event redactedEvent = mStore.getEvent(eventId, getRoomId()); // test if the redacted event has been echoed // it it was not echoed, the event must be pruned to remove useless data // the room summary will be updated when the server will echo the redacted event if ((null != redactedEvent) && ((null == redactedEvent.unsigned) || (null == redactedEvent.unsigned.redacted_because))) { redactedEvent.prune(null); mStore.storeLiveRoomEvent(redactedEvent); mStore.commit(); } if (null != callback) { callback.onSuccess(redactedEvent); } } @Override public void onNetworkError(Exception e) { if (null != callback) { callback.onNetworkError(e); } } @Override public void onMatrixError(MatrixError e) { if (null != callback) { callback.onMatrixError(e); } } @Override public void onUnexpectedError(Exception e) { if (null != callback) { callback.onUnexpectedError(e); } } }); } /** * Redact an event from the room. * * @param eventId the event's id * @param callback the callback with the created event */ public void report(String eventId, int score, String reason, ApiCallback<Void> callback) { mDataHandler.getDataRetriever().getRoomsRestClient().reportEvent(getRoomId(), eventId, score, reason, callback); } //================================================================================ // Member actions //================================================================================ /** * Invite an user to this room. * * @param userId the user id * @param callback the callback for when done */ public void invite(String userId, ApiCallback<Void> callback) { mDataHandler.getDataRetriever().getRoomsRestClient().inviteUserToRoom(getRoomId(), userId, callback); } /** * Invite an user to a room based on their email address to this room. * * @param email the email address * @param callback the callback for when done */ public void inviteByEmail(String email, ApiCallback<Void> callback) { mDataHandler.getDataRetriever().getRoomsRestClient().inviteByEmailToRoom(getRoomId(), email, callback); } /** * Invite some users to this room. * * @param userIds the user ids * @param callback the callback for when done */ public void invite(List<String> userIds, ApiCallback<Void> callback) { invite(userIds, 0, callback); } /** * Invite an indexed user to this room. * * @param userIds the user ids list * @param index the user id index * @param callback the callback for when done */ private void invite(final List<String> userIds, final int index, final ApiCallback<Void> callback) { // add sanity checks if ((null == userIds) || (index >= userIds.size())) { return; } mDataHandler.getDataRetriever().getRoomsRestClient().inviteUserToRoom(getRoomId(), userIds.get(index), new ApiCallback<Void>() { @Override public void onSuccess(Void info) { // invite the last user if ((index + 1) == userIds.size()) { try { callback.onSuccess(info); } catch (Exception e) { Log.e(LOG_TAG, "invite exception " + e.getMessage()); } } else { invite(userIds, index + 1, callback); } } @Override public void onNetworkError(Exception e) { try { callback.onNetworkError(e); } catch (Exception anException) { Log.e(LOG_TAG, "invite exception " + anException.getMessage()); } } @Override public void onMatrixError(MatrixError e) { try { callback.onMatrixError(e); } catch (Exception anException) { Log.e(LOG_TAG, "invite exception " + anException.getMessage()); } } @Override public void onUnexpectedError(Exception e) { try { callback.onUnexpectedError(e); } catch (Exception anException) { Log.e(LOG_TAG, "invite exception " + anException.getMessage()); } } }); } /** * Leave the room. * * @param callback the callback for when done */ public void leave(final ApiCallback<Void> callback) { this.mIsLeaving = true; mDataHandler.onRoomInternalUpdate(getRoomId()); mDataHandler.getDataRetriever().getRoomsRestClient().leaveRoom(getRoomId(), new ApiCallback<Void>() { @Override public void onSuccess(Void info) { if (mDataHandler.isAlive()) { Room.this.mIsLeaving = false; // delete references to the room mStore.deleteRoom(getRoomId()); Log.d(LOG_TAG, "leave : commit"); mStore.commit(); try { callback.onSuccess(info); } catch (Exception e) { Log.e(LOG_TAG, "leave exception " + e.getMessage()); } mDataHandler.onLeaveRoom(getRoomId()); } } @Override public void onNetworkError(Exception e) { Room.this.mIsLeaving = false; try { callback.onNetworkError(e); } catch (Exception anException) { Log.e(LOG_TAG, "leave exception " + anException.getMessage()); } mDataHandler.onRoomInternalUpdate(getRoomId()); } @Override public void onMatrixError(MatrixError e) { Room.this.mIsLeaving = false; try { callback.onMatrixError(e); } catch (Exception anException) { Log.e(LOG_TAG, "leave exception " + anException.getMessage()); } mDataHandler.onRoomInternalUpdate(getRoomId()); } @Override public void onUnexpectedError(Exception e) { Room.this.mIsLeaving = false; try { callback.onUnexpectedError(e); } catch (Exception anException) { Log.e(LOG_TAG, "leave exception " + anException.getMessage()); } mDataHandler.onRoomInternalUpdate(getRoomId()); } }); } /** * Kick a user from the room. * * @param userId the user id * @param callback the async callback */ public void kick(String userId, ApiCallback<Void> callback) { mDataHandler.getDataRetriever().getRoomsRestClient().kickFromRoom(getRoomId(), userId, callback); } /** * Ban a user from the room. * * @param userId the user id * @param reason ban reason * @param callback the async callback */ public void ban(String userId, String reason, ApiCallback<Void> callback) { BannedUser user = new BannedUser(); user.userId = userId; if (!TextUtils.isEmpty(reason)) { user.reason = reason; } mDataHandler.getDataRetriever().getRoomsRestClient().banFromRoom(getRoomId(), user, callback); } /** * Unban a user. * * @param userId the user id * @param callback the async callback */ public void unban(String userId, ApiCallback<Void> callback) { BannedUser user = new BannedUser(); user.userId = userId; mDataHandler.getDataRetriever().getRoomsRestClient().unbanFromRoom(getRoomId(), user, callback); } //================================================================================ // Encryption //================================================================================ private ApiCallback<Void> mRoomEncryptionCallback; private MXEventListener mEncryptionListener = new MXEventListener() { @Override public void onLiveEvent(Event event, RoomState roomState) { if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_MESSAGE_ENCRYPTION)) { if (null != mRoomEncryptionCallback) { mRoomEncryptionCallback.onSuccess(null); mRoomEncryptionCallback = null; } } } }; /** * @return if the room content is encrypted */ public boolean isEncrypted() { return !TextUtils.isEmpty(getLiveState().algorithm); } /** * Enable the encryption. * * @param algorithm the used algorithm * @param callback the asynchronous callback */ public void enableEncryptionWithAlgorithm(final String algorithm, final ApiCallback<Void> callback) { // ensure that the crypto has been update if (null != mDataHandler.getCrypto() && !TextUtils.isEmpty(algorithm)) { HashMap<String, Object> params = new HashMap<>(); params.put("algorithm", algorithm); if (null != callback) { mRoomEncryptionCallback = callback; addEventListener(mEncryptionListener); } mDataHandler.getDataRetriever().getRoomsRestClient().sendStateEvent(getRoomId(), Event.EVENT_TYPE_MESSAGE_ENCRYPTION, params, new ApiCallback<Void>() { @Override public void onSuccess(Void info) { // Wait for the event coming back from the hs } @Override public void onNetworkError(Exception e) { if (null != callback) { callback.onNetworkError(e); removeEventListener(mEncryptionListener); } } @Override public void onMatrixError(MatrixError e) { if (null != callback) { callback.onMatrixError(e); removeEventListener(mEncryptionListener); } } @Override public void onUnexpectedError(Exception e) { if (null != callback) { callback.onUnexpectedError(e); removeEventListener(mEncryptionListener); } } }); } else if (null != callback) { if (null == mDataHandler.getCrypto()) { callback.onMatrixError(new MXCryptoError(MXCryptoError.ENCRYPTING_NOT_ENABLED_ERROR_CODE, MXCryptoError.ENCRYPTING_NOT_ENABLED_REASON, MXCryptoError.ENCRYPTING_NOT_ENABLED_REASON)); } else { callback.onMatrixError(new MXCryptoError(MXCryptoError.MISSING_FIELDS_ERROR_CODE, MXCryptoError.UNABLE_TO_ENCRYPT, MXCryptoError.MISSING_FIELDS_REASON)); } } } }