BayernMessenger/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomState.java

1334 lines
45 KiB
Java

/*
* Copyright 2014 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.legacy.data;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.google.gson.JsonObject;
import im.vector.matrix.android.internal.legacy.MXDataHandler;
import im.vector.matrix.android.internal.legacy.call.MXCallsManager;
import im.vector.matrix.android.internal.legacy.data.store.IMXStore;
import im.vector.matrix.android.internal.legacy.data.timeline.EventTimeline;
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
import im.vector.matrix.android.internal.legacy.rest.callback.SimpleApiCallback;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import im.vector.matrix.android.internal.legacy.rest.model.PowerLevels;
import im.vector.matrix.android.internal.legacy.rest.model.RoomCreateContent;
import im.vector.matrix.android.internal.legacy.rest.model.RoomDirectoryVisibility;
import im.vector.matrix.android.internal.legacy.rest.model.RoomMember;
import im.vector.matrix.android.internal.legacy.rest.model.RoomPinnedEventsContent;
import im.vector.matrix.android.internal.legacy.rest.model.RoomTombstoneContent;
import im.vector.matrix.android.internal.legacy.rest.model.User;
import im.vector.matrix.android.internal.legacy.rest.model.pid.RoomThirdPartyInvite;
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
import im.vector.matrix.android.internal.legacy.util.Log;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* The state of a room.
*/
public class RoomState implements Externalizable {
private static final String LOG_TAG = RoomState.class.getSimpleName();
private static final long serialVersionUID = -6019932024524988201L;
public static final String JOIN_RULE_PUBLIC = "public";
public static final String JOIN_RULE_INVITE = "invite";
/**
* room access is granted to guests
*/
public static final String GUEST_ACCESS_CAN_JOIN = "can_join";
/**
* room access is denied to guests
*/
public static final String GUEST_ACCESS_FORBIDDEN = "forbidden";
public static final String HISTORY_VISIBILITY_SHARED = "shared";
public static final String HISTORY_VISIBILITY_INVITED = "invited";
public static final String HISTORY_VISIBILITY_JOINED = "joined";
public static final String HISTORY_VISIBILITY_WORLD_READABLE = "world_readable";
// Public members used for JSON mapping
// The room ID
public String roomId;
// The power level of room members
private PowerLevels powerLevels;
// The aliases
public List<String> aliases;
// The room aliases. The key is the domain.
private Map<String, Event> mRoomAliases = new HashMap<>();
// the aliases are defined for each home server url
private Map<String, List<String>> mAliasesByDomain = new HashMap<>();
// merged from mAliasesByHomeServerUrl
private List<String> mMergedAliasesList;
//
private Map<String, List<Event>> mStateEvents = new HashMap<>();
// The canonical alias of the room.
private String canonicalAlias;
// The name of the room as provided by the home server.
public String name;
// The topic of the room.
public String topic;
// The tombstone content if the room has been killed
private RoomTombstoneContent mRoomTombstoneContent;
// The avatar url of the room.
public String url;
public String avatar_url;
// the room create content
private RoomCreateContent mRoomCreateContent;
// the room pinned events content
@Nullable
private RoomPinnedEventsContent mRoomPinnedEventsContent;
// the join rule
public String join_rule;
/**
* the guest access policy of the room
**/
public String guest_access;
// SPEC-134
public String history_visibility;
/**
* the room visibility in the directory list (i.e. public, private...)
**/
public String visibility;
// the encryption algorithm
public String algorithm;
// group ids list which should be displayed
public List<String> groups;
/**
* The number of unread messages that match the push notification rules.
* It is based on the notificationCount field in /sync response.
*/
private int mNotificationCount;
/**
* The number of highlighted unread messages (subset of notifications).
* It is based on the notificationCount field in /sync response.
*/
private int mHighlightCount;
// the associated token
private String token;
// the room members. May be a partial list if all members are not loaded yet, due to lazy loading
private final Map<String, RoomMember> mMembers = new HashMap<>();
// true if all members are loaded
private boolean mAllMembersAreLoaded;
private final List<ApiCallback<List<RoomMember>>> mGetAllMembersCallbacks = new ArrayList<>();
// the third party invite members
private final Map<String, RoomThirdPartyInvite> mThirdPartyInvites = new HashMap<>();
/**
* Cache for [self memberWithThirdPartyInviteToken].
* The key is the 3pid invite token.
*/
private final Map<String, RoomMember> mMembersWithThirdPartyInviteTokenCache = new HashMap<>();
/**
* Tell if the roomstate if a live one.
*/
private boolean mIsLive;
// the unitary tests crash when MXDataHandler type is set.
// TODO Try to avoid this ^^
private transient Object mDataHandler = null;
// member display cache
private transient Map<String, String> mMemberDisplayNameByUserId = new HashMap<>();
// get the guest access
// avoid the null case
public String getGuestAccess() {
if (null != guest_access) {
return guest_access;
}
// retro compliancy
return RoomState.GUEST_ACCESS_FORBIDDEN;
}
// get the history visibility
// avoid the null case
public String getHistoryVisibility() {
if (null != history_visibility) {
return history_visibility;
}
// retro compliancy
return RoomState.HISTORY_VISIBILITY_SHARED;
}
/**
* @return the state token
*/
public String getToken() {
return token;
}
/**
* Update the token.
*
* @param token the new token
*/
public void setToken(String token) {
this.token = token;
}
// avatar Url makes more sense than url.
public String getAvatarUrl() {
if (null != url) {
return url;
} else {
return avatar_url;
}
}
/**
* @return the related group ids list (cannot be null)
*/
public List<String> getRelatedGroups() {
return (null == groups) ? new ArrayList<String>() : groups;
}
/**
* @return a copy of the room members list. May be incomplete if the full list is not loaded yet
*/
public List<RoomMember> getLoadedMembers() {
List<RoomMember> res;
synchronized (this) {
// make a copy to avoid concurrency modifications
res = new ArrayList<>(mMembers.values());
}
return res;
}
/**
* Get the list of all the room members. Fetch from server if the full list is not loaded yet.
*
* @param callback The callback to get a copy of the room members list.
*/
public void getMembersAsync(ApiCallback<List<RoomMember>> callback) {
if (areAllMembersLoaded()) {
List<RoomMember> res;
synchronized (this) {
// make a copy to avoid concurrency modifications
res = new ArrayList<>(mMembers.values());
}
callback.onSuccess(res);
} else {
boolean doTheRequest;
synchronized (mGetAllMembersCallbacks) {
mGetAllMembersCallbacks.add(callback);
doTheRequest = mGetAllMembersCallbacks.size() == 1;
}
if (doTheRequest) {
// Load members from server
getDataHandler().getMembersAsync(roomId, new SimpleApiCallback<List<RoomMember>>(callback) {
@Override
public void onSuccess(List<RoomMember> info) {
Log.d(LOG_TAG, "getMembers has returned " + info.size() + " users.");
IMXStore store = ((MXDataHandler) mDataHandler).getStore();
List<RoomMember> res;
for (RoomMember member : info) {
// Do not erase already known members form the sync
if (getMember(member.getUserId()) == null) {
setMember(member.getUserId(), member);
// Also create a User
if (store != null) {
store.updateUserWithRoomMemberEvent(member);
}
}
}
synchronized (mGetAllMembersCallbacks) {
for (ApiCallback<List<RoomMember>> apiCallback : mGetAllMembersCallbacks) {
// make a copy to avoid concurrency modifications
res = new ArrayList<>(mMembers.values());
apiCallback.onSuccess(res);
}
mGetAllMembersCallbacks.clear();
}
mAllMembersAreLoaded = true;
}
});
}
}
}
/**
* Tell if all members has been loaded
*
* @return true if LazyLoading is Off, or if all members has been loaded
*/
private boolean areAllMembersLoaded() {
return mDataHandler != null
&& (!((MXDataHandler) mDataHandler).isLazyLoadingEnabled() || mAllMembersAreLoaded);
}
/**
* Force a fetch of the loaded members the next time they will be requested
*/
public void forceMembersRequest() {
mAllMembersAreLoaded = false;
}
/**
* Provides the loaded states event list.
* The room member events are NOT included.
*
* @param types the allowed event types.
* @return the filtered state events list.
*/
public List<Event> getStateEvents(final Set<String> types) {
final List<Event> filteredStateEvents = new ArrayList<>();
final List<Event> stateEvents = new ArrayList<>();
// merge the values lists
Collection<List<Event>> currentStateEvents = mStateEvents.values();
for (List<Event> eventsList : currentStateEvents) {
stateEvents.addAll(eventsList);
}
if ((null != types) && !types.isEmpty()) {
for (Event stateEvent : stateEvents) {
if ((null != stateEvent.getType()) && types.contains(stateEvent.getType())) {
filteredStateEvents.add(stateEvent);
}
}
} else {
filteredStateEvents.addAll(stateEvents);
}
return filteredStateEvents;
}
/**
* Provides the state events list.
* It includes the room member creation events (they are not loaded in memory by default).
*
* @param store the store in which the state events must be retrieved
* @param types the allowed event types.
* @param callback the asynchronous callback.
*/
public void getStateEvents(IMXStore store, final Set<String> types, final ApiCallback<List<Event>> callback) {
if (null != store) {
final List<Event> stateEvents = new ArrayList<>();
Collection<List<Event>> currentStateEvents = mStateEvents.values();
for (List<Event> eventsList : currentStateEvents) {
stateEvents.addAll(eventsList);
}
// retrieve the roomMember creation events
store.getRoomStateEvents(roomId, new SimpleApiCallback<List<Event>>() {
@Override
public void onSuccess(List<Event> events) {
stateEvents.addAll(events);
final List<Event> filteredStateEvents = new ArrayList<>();
if ((null != types) && !types.isEmpty()) {
for (Event stateEvent : stateEvents) {
if ((null != stateEvent.getType()) && types.contains(stateEvent.getType())) {
filteredStateEvents.add(stateEvent);
}
}
} else {
filteredStateEvents.addAll(stateEvents);
}
callback.onSuccess(filteredStateEvents);
}
});
}
}
/**
* @return a copy of the displayable members list. May be incomplete if the full list is not loaded yet
*/
public List<RoomMember> getDisplayableLoadedMembers() {
List<RoomMember> res = getLoadedMembers();
RoomMember conferenceUserId = getMember(MXCallsManager.getConferenceUserId(roomId));
if (null != conferenceUserId) {
res.remove(conferenceUserId);
}
return res;
}
/**
* Provides a list of displayable members.
* Some dummy members are created to internal stuff.
*
* @param callback The callback to get a copy of the displayable room members list.
*/
public void getDisplayableMembersAsync(final ApiCallback<List<RoomMember>> callback) {
getMembersAsync(new SimpleApiCallback<List<RoomMember>>(callback) {
@Override
public void onSuccess(List<RoomMember> members) {
RoomMember conferenceUserId = getMember(MXCallsManager.getConferenceUserId(roomId));
if (null != conferenceUserId) {
List<RoomMember> membersList = new ArrayList<>(members);
membersList.remove(conferenceUserId);
callback.onSuccess(membersList);
} else {
callback.onSuccess(members);
}
}
});
}
/**
* Tells if the room is a call conference one
* i.e. this room has been created to manage the call conference
*
* @return true if it is a call conference room.
*/
public boolean isConferenceUserRoom() {
return getDataHandler().getStore().getSummary(roomId).isConferenceUserRoom();
}
/**
* Set this room as a conference user room
*
* @param isConferenceUserRoom true when it is an user conference room.
*/
public void setIsConferenceUserRoom(boolean isConferenceUserRoom) {
getDataHandler().getStore().getSummary(roomId).setIsConferenceUserRoom(isConferenceUserRoom);
}
/**
* Update the room member from its user id.
*
* @param userId the user id.
* @param member the new member value.
*/
private void setMember(String userId, RoomMember member) {
// Populate a basic user object if there is none
if (member.getUserId() == null) {
member.setUserId(userId);
}
synchronized (this) {
if (null != mMemberDisplayNameByUserId) {
mMemberDisplayNameByUserId.remove(userId);
}
mMembers.put(userId, member);
}
}
/**
* Retrieve a room member from its user id.
*
* @param userId the user id.
* @return the linked member it exists.
*/
// TODO Change this? Can return null if all members are not loaded yet
@Nullable
public RoomMember getMember(String userId) {
RoomMember member;
synchronized (this) {
member = mMembers.get(userId);
}
if (member == null) {
// TODO LazyLoading
Log.e(LOG_TAG, "!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Null member '" + userId + "' !!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
if (TextUtils.equals(getDataHandler().getUserId(), userId)) {
// This should never happen
Log.e(LOG_TAG, "!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Null current user '" + userId + "' !!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
}
}
return member;
}
/**
* Retrieve a room member from its original event id.
* It can return null if the lazy loading is enabled and if the member is not loaded yet.
*
* @param eventId the event id.
* @return the linked member if it exists and if it is loaded.
*/
@Nullable
public RoomMember getMemberByEventId(String eventId) {
RoomMember member = null;
synchronized (this) {
for (RoomMember aMember : mMembers.values()) {
if (aMember.getOriginalEventId().equals(eventId)) {
member = aMember;
break;
}
}
}
return member;
}
/**
* Remove a member defines by its user id.
*
* @param userId the user id.
*/
public void removeMember(String userId) {
synchronized (this) {
mMembers.remove(userId);
// remove the cached display name
if (null != mMemberDisplayNameByUserId) {
mMemberDisplayNameByUserId.remove(userId);
}
}
}
/**
* Retrieve a member from an invitation token.
*
* @param thirdPartyInviteToken the third party invitation token.
* @return the member it exists.
*/
public RoomMember memberWithThirdPartyInviteToken(String thirdPartyInviteToken) {
return mMembersWithThirdPartyInviteTokenCache.get(thirdPartyInviteToken);
}
/**
* Retrieve a RoomThirdPartyInvite from its token.
*
* @param thirdPartyInviteToken the third party invitation token.
* @return the linked RoomThirdPartyInvite if it exists
*/
public RoomThirdPartyInvite thirdPartyInviteWithToken(String thirdPartyInviteToken) {
return mThirdPartyInvites.get(thirdPartyInviteToken);
}
/**
* @return the third party invite list.
*/
public Collection<RoomThirdPartyInvite> thirdPartyInvites() {
return mThirdPartyInvites.values();
}
/**
* @return the power levels (it can be null).
*/
public PowerLevels getPowerLevels() {
if (null != powerLevels) {
return powerLevels.deepCopy();
} else {
return null;
}
}
/**
* Update the power levels.
*
* @param powerLevels the new power levels
*/
public void setPowerLevels(PowerLevels powerLevels) {
this.powerLevels = powerLevels;
}
/**
* Update the linked dataHandler.
*
* @param dataHandler the new dataHandler
*/
public void setDataHandler(MXDataHandler dataHandler) {
mDataHandler = dataHandler;
}
/**
* @return the user dataHandler
*/
public MXDataHandler getDataHandler() {
return (MXDataHandler) mDataHandler;
}
/**
* Update the notified messages count.
*
* @param notificationCount the new notified messages count.
*/
public void setNotificationCount(int notificationCount) {
Log.d(LOG_TAG, "## setNotificationCount() : " + notificationCount + " room id " + roomId);
mNotificationCount = notificationCount;
}
/**
* @return the notified messages count.
*/
public int getNotificationCount() {
return mNotificationCount;
}
/**
* Update the highlighted messages count.
*
* @param highlightCount the new highlighted messages count.
*/
public void setHighlightCount(int highlightCount) {
Log.d(LOG_TAG, "## setHighlightCount() : " + highlightCount + " room id " + roomId);
mHighlightCount = highlightCount;
}
/**
* @return the highlighted messages count.
*/
public int getHighlightCount() {
return mHighlightCount;
}
/**
* Check if the user userId can back paginate.
*
* @param isJoined true is user is in the room
* @param isInvited true is user is invited to the room
* @return true if the user can back paginate.
*/
public boolean canBackPaginate(boolean isJoined, boolean isInvited) {
String visibility = TextUtils.isEmpty(history_visibility) ? HISTORY_VISIBILITY_SHARED : history_visibility;
return isJoined
|| visibility.equals(HISTORY_VISIBILITY_WORLD_READABLE)
|| visibility.equals(HISTORY_VISIBILITY_SHARED)
|| (visibility.equals(HISTORY_VISIBILITY_INVITED) && isInvited);
}
/**
* Make a deep copy of this room state object.
*
* @return the copy
*/
public RoomState deepCopy() {
RoomState copy = new RoomState();
copy.roomId = roomId;
copy.setPowerLevels((powerLevels == null) ? null : powerLevels.deepCopy());
copy.aliases = (aliases == null) ? null : new ArrayList<>(aliases);
copy.mAliasesByDomain = new HashMap<>(mAliasesByDomain);
copy.canonicalAlias = canonicalAlias;
copy.name = name;
copy.topic = topic;
copy.url = url;
copy.mRoomCreateContent = mRoomCreateContent != null ? mRoomCreateContent.deepCopy() : null;
copy.mRoomPinnedEventsContent = mRoomPinnedEventsContent != null ? mRoomPinnedEventsContent.deepCopy() : null;
copy.join_rule = join_rule;
copy.guest_access = guest_access;
copy.history_visibility = history_visibility;
copy.visibility = visibility;
copy.token = token;
copy.groups = groups;
copy.mDataHandler = mDataHandler;
copy.mIsLive = mIsLive;
copy.mAllMembersAreLoaded = mAllMembersAreLoaded;
copy.algorithm = algorithm;
copy.mRoomAliases = new HashMap<>(mRoomAliases);
copy.mStateEvents = new HashMap<>(mStateEvents);
copy.mRoomTombstoneContent = mRoomTombstoneContent != null ? mRoomTombstoneContent.deepCopy() : null;
synchronized (this) {
Iterator it = mMembers.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, RoomMember> pair = (Map.Entry<String, RoomMember>) it.next();
copy.setMember(pair.getKey(), pair.getValue().deepCopy());
}
Collection<String> keys = mThirdPartyInvites.keySet();
for (String key : keys) {
copy.mThirdPartyInvites.put(key, mThirdPartyInvites.get(key).deepCopy());
}
keys = mMembersWithThirdPartyInviteTokenCache.keySet();
for (String key : keys) {
copy.mMembersWithThirdPartyInviteTokenCache.put(key, mMembersWithThirdPartyInviteTokenCache.get(key).deepCopy());
}
}
return copy;
}
/**
* @return the room canonical alias
*/
public String getCanonicalAlias() {
return canonicalAlias;
}
/**
* Update the canonical alias of a room
*
* @param newCanonicalAlias the new canonical alias
*/
public void setCanonicalAlias(String newCanonicalAlias) {
canonicalAlias = newCanonicalAlias;
}
/**
* Provides the aliases for any known domains
*
* @return the aliases list
*/
public List<String> getAliases() {
if (null == mMergedAliasesList) {
mMergedAliasesList = new ArrayList<>();
for (String url : mAliasesByDomain.keySet()) {
mMergedAliasesList.addAll(mAliasesByDomain.get(url));
}
// ensure that the current aliases have been added.
// for example for the public rooms because there is no applystate call.
if (null != aliases) {
for (String anAlias : aliases) {
if (mMergedAliasesList.indexOf(anAlias) < 0) {
mMergedAliasesList.add(anAlias);
}
}
}
}
return mMergedAliasesList;
}
/**
* Provides the aliases by domain
*
* @return the aliases list map
*/
public Map<String, List<String>> getAliasesByDomain() {
return new HashMap<>(mAliasesByDomain);
}
/**
* Remove an alias.
*
* @param alias the alias to remove
*/
public void removeAlias(String alias) {
if (getAliases().indexOf(alias) >= 0) {
if (null != aliases) {
aliases.remove(alias);
}
for (String host : mAliasesByDomain.keySet()) {
mAliasesByDomain.get(host).remove(alias);
}
mMergedAliasesList = null;
}
}
/**
* Add an alias.
*
* @param alias the alias to add
*/
public void addAlias(String alias) {
if (getAliases().indexOf(alias) < 0) {
// patch until the server echoes the alias addition.
mMergedAliasesList.add(alias);
}
}
/**
* @return true if the room is encrypted
*/
public boolean isEncrypted() {
// When a client receives an m.room.encryption event as above, it should set a flag to indicate that messages sent in the room should be encrypted.
// This flag should not be cleared if a later m.room.encryption event changes the configuration. This is to avoid a situation where a MITM can simply
// ask participants to disable encryption. In short: once encryption is enabled in a room, it can never be disabled.
return null != algorithm;
}
/**
* @return true if the room is versioned, it means that the room is obsolete.
* You can't interact with it anymore, but you can still browse the past messages.
*/
public boolean isVersioned() {
return mRoomTombstoneContent != null;
}
/**
* @return the room tombstone content
*/
public RoomTombstoneContent getRoomTombstoneContent() {
return mRoomTombstoneContent;
}
/**
* @return true if the room has a predecessor
*/
public boolean hasPredecessor() {
return mRoomCreateContent != null && mRoomCreateContent.hasPredecessor();
}
/**
* @return the room create content
*/
public RoomCreateContent getRoomCreateContent() {
return mRoomCreateContent;
}
/**
* @return the room pinned events content
*/
@Nullable
public RoomPinnedEventsContent getRoomPinnedEventsContent() {
return mRoomPinnedEventsContent;
}
/**
* @return the encryption algorithm
*/
public String encryptionAlgorithm() {
return TextUtils.isEmpty(algorithm) ? null : algorithm;
}
/**
* Apply the given event (relevant for state changes) to our state.
*
* @param store the store to use
* @param event the event
* @param direction how the event should affect the state: Forwards for applying, backwards for un-applying (applying the previous state)
* @return true if the event is managed
*/
public boolean applyState(IMXStore store, Event event, EventTimeline.Direction direction) {
if (event.stateKey == null) {
return false;
}
JsonObject contentToConsider = (direction == EventTimeline.Direction.FORWARDS) ? event.getContentAsJsonObject() : event.getPrevContentAsJsonObject();
String eventType = event.getType();
try {
if (Event.EVENT_TYPE_STATE_ROOM_NAME.equals(eventType)) {
name = JsonUtils.toStateEvent(contentToConsider).name;
} else if (Event.EVENT_TYPE_STATE_ROOM_TOPIC.equals(eventType)) {
topic = JsonUtils.toStateEvent(contentToConsider).topic;
} else if (Event.EVENT_TYPE_STATE_ROOM_CREATE.equals(eventType)) {
mRoomCreateContent = JsonUtils.toRoomCreateContent(contentToConsider);
} else if (Event.EVENT_TYPE_STATE_ROOM_JOIN_RULES.equals(eventType)) {
join_rule = JsonUtils.toStateEvent(contentToConsider).joinRule;
} else if (Event.EVENT_TYPE_STATE_ROOM_GUEST_ACCESS.equals(eventType)) {
guest_access = JsonUtils.toStateEvent(contentToConsider).guestAccess;
} else if (Event.EVENT_TYPE_STATE_ROOM_ALIASES.equals(eventType)) {
if (!TextUtils.isEmpty(event.stateKey)) {
// backward compatibility
aliases = JsonUtils.toStateEvent(contentToConsider).aliases;
// sanity check
if (null != aliases) {
mAliasesByDomain.put(event.stateKey, aliases);
mRoomAliases.put(event.stateKey, event);
} else {
mAliasesByDomain.put(event.stateKey, new ArrayList<String>());
}
}
} else if (Event.EVENT_TYPE_MESSAGE_ENCRYPTION.equals(eventType)) {
algorithm = JsonUtils.toStateEvent(contentToConsider).algorithm;
// When a client receives an m.room.encryption event as above, it should set a flag to indicate that messages sent
// in the room should be encrypted.
// This flag should not be cleared if a later m.room.encryption event changes the configuration. This is to avoid
// a situation where a MITM can simply ask participants to disable encryption. In short: once encryption is enabled
// in a room, it can never be disabled.
if (null == algorithm) {
algorithm = "";
}
} else if (Event.EVENT_TYPE_STATE_CANONICAL_ALIAS.equals(eventType)) {
// SPEC-125
canonicalAlias = JsonUtils.toStateEvent(contentToConsider).canonicalAlias;
} else if (Event.EVENT_TYPE_STATE_HISTORY_VISIBILITY.equals(eventType)) {
// SPEC-134
history_visibility = JsonUtils.toStateEvent(contentToConsider).historyVisibility;
} else if (Event.EVENT_TYPE_STATE_ROOM_AVATAR.equals(eventType)) {
url = JsonUtils.toStateEvent(contentToConsider).url;
} else if (Event.EVENT_TYPE_STATE_RELATED_GROUPS.equals(eventType)) {
groups = JsonUtils.toStateEvent(contentToConsider).groups;
} else if (Event.EVENT_TYPE_STATE_ROOM_MEMBER.equals(eventType)) {
RoomMember member = JsonUtils.toRoomMember(contentToConsider);
String userId = event.stateKey;
if (null == userId) {
Log.e(LOG_TAG, "## applyState() : null stateKey in " + roomId);
} else if (null == member) {
// the member has already been removed
if (null == getMember(userId)) {
Log.e(LOG_TAG, "## applyState() : the user " + userId + " is not anymore a member of " + roomId);
return false;
}
removeMember(userId);
} else {
try {
member.setUserId(userId);
member.setOriginServerTs(event.getOriginServerTs());
member.setOriginalEventId(event.eventId);
member.mSender = event.getSender();
if ((null != store) && (direction == EventTimeline.Direction.FORWARDS)) {
store.storeRoomStateEvent(roomId, event);
}
RoomMember currentMember = getMember(userId);
// check if the member is the same
// duplicated message ?
if (member.equals(currentMember)) {
Log.e(LOG_TAG, "## applyState() : seems being a duplicated event for " + userId + " in room " + roomId);
return false;
}
// when a member leaves a room, his avatar / display name is not anymore provided
if (null != currentMember) {
if (TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_LEAVE)
|| TextUtils.equals(member.membership, (RoomMember.MEMBERSHIP_BAN))) {
if (null == member.getAvatarUrl()) {
member.setAvatarUrl(currentMember.getAvatarUrl());
}
if (null == member.displayname) {
member.displayname = currentMember.displayname;
}
// remove the cached display name
if (null != mMemberDisplayNameByUserId) {
mMemberDisplayNameByUserId.remove(userId);
}
// test if the user has been kicked
if (!TextUtils.equals(event.getSender(), event.stateKey)
&& TextUtils.equals(currentMember.membership, RoomMember.MEMBERSHIP_JOIN)
&& TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_LEAVE)) {
member.membership = RoomMember.MEMBERSHIP_KICK;
}
}
}
if ((direction == EventTimeline.Direction.FORWARDS) && (null != store)) {
store.updateUserWithRoomMemberEvent(member);
}
// Cache room member event that is successor of a third party invite event
if (!TextUtils.isEmpty(member.getThirdPartyInviteToken())) {
mMembersWithThirdPartyInviteTokenCache.put(member.getThirdPartyInviteToken(), member);
}
} catch (Exception e) {
Log.e(LOG_TAG, "## applyState() - EVENT_TYPE_STATE_ROOM_MEMBER failed " + e.getMessage(), e);
}
setMember(userId, member);
}
} else if (Event.EVENT_TYPE_STATE_ROOM_POWER_LEVELS.equals(eventType)) {
powerLevels = JsonUtils.toPowerLevels(contentToConsider);
} else if (Event.EVENT_TYPE_STATE_ROOM_THIRD_PARTY_INVITE.equals(event.getType())) {
if (null != contentToConsider) {
RoomThirdPartyInvite thirdPartyInvite = JsonUtils.toRoomThirdPartyInvite(contentToConsider);
thirdPartyInvite.token = event.stateKey;
if ((direction == EventTimeline.Direction.FORWARDS) && (null != store)) {
store.storeRoomStateEvent(roomId, event);
}
if (!TextUtils.isEmpty(thirdPartyInvite.token)) {
mThirdPartyInvites.put(thirdPartyInvite.token, thirdPartyInvite);
}
}
} else if (Event.EVENT_TYPE_STATE_ROOM_TOMBSTONE.equals(eventType)) {
mRoomTombstoneContent = JsonUtils.toRoomTombstoneContent(contentToConsider);
} else if (Event.EVENT_TYPE_STATE_PINNED_EVENT.equals(eventType)) {
mRoomPinnedEventsContent = JsonUtils.toRoomPinnedEventsContent(contentToConsider);
}
// same the latest room state events
// excepts the membership ones
// they are saved elsewhere
if (!TextUtils.isEmpty(eventType) && !Event.EVENT_TYPE_STATE_ROOM_MEMBER.equals(eventType)) {
List<Event> eventsList = mStateEvents.get(eventType);
if (null == eventsList) {
eventsList = new ArrayList<>();
mStateEvents.put(eventType, eventsList);
}
eventsList.add(event);
}
} catch (Exception e) {
Log.e(LOG_TAG, "applyState failed with error " + e.getMessage(), e);
}
return true;
}
/**
* @return true if the room is a public one
*/
public boolean isPublic() {
return TextUtils.equals((null != visibility) ? visibility : join_rule, RoomDirectoryVisibility.DIRECTORY_VISIBILITY_PUBLIC);
}
/**
* Return an unique display name of the member userId.
*
* @param userId the user id
* @return unique display name
*/
public String getMemberName(String userId) {
// sanity check
if (null == userId) {
return null;
}
String displayName;
synchronized (this) {
if (null == mMemberDisplayNameByUserId) {
mMemberDisplayNameByUserId = new HashMap<>();
}
displayName = mMemberDisplayNameByUserId.get(userId);
}
if (null != displayName) {
return displayName;
}
// Get the user display name from the member list of the room
RoomMember member = getMember(userId);
// Do not consider null display name
if ((null != member) && !TextUtils.isEmpty(member.displayname)) {
displayName = member.displayname;
synchronized (this) {
List<String> matrixIds = new ArrayList<>();
// Disambiguate users who have the same display name in the room
for (RoomMember aMember : mMembers.values()) {
if (displayName.equals(aMember.displayname)) {
matrixIds.add(aMember.getUserId());
}
}
// if several users have the same display name
// index it i.e bob (<Matrix id>)
if (matrixIds.size() > 1) {
displayName += " (" + userId + ")";
}
}
} else if ((null != member) && TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_INVITE)) {
User user = ((MXDataHandler) mDataHandler).getUser(userId);
if (null != user) {
displayName = user.displayname;
}
}
if (null == displayName) {
// By default, use the user ID
displayName = userId;
}
mMemberDisplayNameByUserId.put(userId, displayName);
return displayName;
}
@Override
public void readExternal(ObjectInput input) throws IOException, ClassNotFoundException {
if (input.readBoolean()) {
roomId = input.readUTF();
}
if (input.readBoolean()) {
powerLevels = (PowerLevels) input.readObject();
}
if (input.readBoolean()) {
aliases = (List<String>) input.readObject();
}
List<Event> roomAliasesEvents = (List<Event>) input.readObject();
for (Event e : roomAliasesEvents) {
mRoomAliases.put(e.stateKey, e);
}
mAliasesByDomain = (Map<String, List<String>>) input.readObject();
if (input.readBoolean()) {
mMergedAliasesList = (List<String>) input.readObject();
}
Map<String, List<Event>> stateEvents = (Map<String, List<Event>>) input.readObject();
if (null != stateEvents) {
mStateEvents = new HashMap<>(stateEvents);
}
if (input.readBoolean()) {
canonicalAlias = input.readUTF();
}
if (input.readBoolean()) {
name = input.readUTF();
}
if (input.readBoolean()) {
topic = input.readUTF();
}
if (input.readBoolean()) {
url = input.readUTF();
}
if (input.readBoolean()) {
avatar_url = input.readUTF();
}
if (input.readBoolean()) {
mRoomCreateContent = (RoomCreateContent) input.readObject();
}
if (input.readBoolean()) {
mRoomPinnedEventsContent = (RoomPinnedEventsContent) input.readObject();
}
if (input.readBoolean()) {
join_rule = input.readUTF();
}
if (input.readBoolean()) {
guest_access = input.readUTF();
}
if (input.readBoolean()) {
history_visibility = input.readUTF();
}
if (input.readBoolean()) {
visibility = input.readUTF();
}
if (input.readBoolean()) {
algorithm = input.readUTF();
}
mNotificationCount = input.readInt();
mHighlightCount = input.readInt();
if (input.readBoolean()) {
token = input.readUTF();
}
List<RoomMember> members = (List<RoomMember>) input.readObject();
for (RoomMember r : members) {
mMembers.put(r.getUserId(), r);
}
List<RoomThirdPartyInvite> invites = (List<RoomThirdPartyInvite>) input.readObject();
for (RoomThirdPartyInvite i : invites) {
mThirdPartyInvites.put(i.token, i);
}
List<RoomMember> inviteTokens = (List<RoomMember>) input.readObject();
for (RoomMember r : inviteTokens) {
mMembersWithThirdPartyInviteTokenCache.put(r.getThirdPartyInviteToken(), r);
}
mIsLive = input.readBoolean();
mAllMembersAreLoaded = input.readBoolean();
if (input.readBoolean()) {
groups = (List<String>) input.readObject();
}
if (input.readBoolean()) {
mRoomTombstoneContent = (RoomTombstoneContent) input.readObject();
}
}
@Override
public void writeExternal(ObjectOutput output) throws IOException {
output.writeBoolean(null != roomId);
if (null != roomId) {
output.writeUTF(roomId);
}
output.writeBoolean(null != powerLevels);
if (null != powerLevels) {
output.writeObject(powerLevels);
}
output.writeBoolean(null != aliases);
if (null != aliases) {
output.writeObject(aliases);
}
output.writeObject(new ArrayList<>(mRoomAliases.values()));
output.writeObject(mAliasesByDomain);
output.writeBoolean(null != mMergedAliasesList);
if (null != mMergedAliasesList) {
output.writeObject(mMergedAliasesList);
}
output.writeObject(mStateEvents);
output.writeBoolean(null != canonicalAlias);
if (null != canonicalAlias) {
output.writeUTF(canonicalAlias);
}
output.writeBoolean(null != name);
if (null != name) {
output.writeUTF(name);
}
output.writeBoolean(null != topic);
if (null != topic) {
output.writeUTF(topic);
}
output.writeBoolean(null != url);
if (null != url) {
output.writeUTF(url);
}
output.writeBoolean(null != avatar_url);
if (null != avatar_url) {
output.writeUTF(avatar_url);
}
output.writeBoolean(null != mRoomCreateContent);
if (null != mRoomCreateContent) {
output.writeObject(mRoomCreateContent);
}
output.writeBoolean(null != mRoomPinnedEventsContent);
if (null != mRoomPinnedEventsContent) {
output.writeObject(mRoomPinnedEventsContent);
}
output.writeBoolean(null != join_rule);
if (null != join_rule) {
output.writeUTF(join_rule);
}
output.writeBoolean(null != guest_access);
if (null != guest_access) {
output.writeUTF(guest_access);
}
output.writeBoolean(null != history_visibility);
if (null != history_visibility) {
output.writeUTF(history_visibility);
}
output.writeBoolean(null != visibility);
if (null != visibility) {
output.writeUTF(visibility);
}
output.writeBoolean(null != algorithm);
if (null != algorithm) {
output.writeUTF(algorithm);
}
output.writeInt(mNotificationCount);
output.writeInt(mHighlightCount);
output.writeBoolean(null != token);
if (null != token) {
output.writeUTF(token);
}
output.writeObject(new ArrayList<>(mMembers.values()));
output.writeObject(new ArrayList<>(mThirdPartyInvites.values()));
output.writeObject(new ArrayList<>(mMembersWithThirdPartyInviteTokenCache.values()));
output.writeBoolean(mIsLive);
output.writeBoolean(mAllMembersAreLoaded);
output.writeBoolean(null != groups);
if (null != groups) {
output.writeObject(groups);
}
output.writeBoolean(null != mRoomTombstoneContent);
if (null != mRoomTombstoneContent) {
output.writeObject(mRoomTombstoneContent);
}
}
}