From 11f860533cd7fe1f7021c63248c8021abec98273 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 11 Oct 2018 20:01:08 +0200 Subject: [PATCH] Start working with RoomState (trying approaches for content parsing) --- .../vector/matrix/android/api/events/Event.kt | 32 +- .../matrix/android/api/rooms/PowerLevels.kt | 117 +++ .../api/rooms/RoomDirectoryVisibility.kt | 6 +- .../api/rooms/RoomHistoryVisibility.kt | 2 - .../matrix/android/api/rooms/RoomState.kt | 750 +++++++++++++++++- .../android/internal/di/MoshiProvider.kt | 3 +- .../android/internal/di/NetworkModule.kt | 2 +- .../internal/events/sync/RoomSyncHandler.kt | 9 + .../internal/events/sync/StateEvent.kt | 18 + .../internal/legacy/data/RoomState.java | 8 +- .../internal/legacy/data/store/IMXStore.java | 4 +- .../legacy/data/store/MXFileStore.java | 4 +- .../legacy/data/store/MXMemoryStore.java | 6 +- 13 files changed, 934 insertions(+), 27 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/rooms/PowerLevels.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/events/sync/RoomSyncHandler.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/events/sync/StateEvent.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/events/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/events/Event.kt index 642b24f067..e3caae9d97 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/events/Event.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/events/Event.kt @@ -1,7 +1,10 @@ package im.vector.matrix.android.api.events +import com.google.gson.JsonObject import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.legacy.util.JsonUtils @JsonClass(generateAdapter = true) data class Event( @@ -14,6 +17,33 @@ data class Event( @Json(name = "state_key") val stateKey: String? = null, @Json(name = "room_id") val roomId: String? = null, @Json(name = "unsigned_data") val unsignedData: UnsignedData? = null -) +) { + + val contentAsJsonObject: JsonObject by lazy { + val gson = JsonUtils.getGson(true) + gson.toJsonTree(content).asJsonObject + } + + val prevContentAsJsonObject: JsonObject by lazy { + val gson = JsonUtils.getGson(true) + gson.toJsonTree(prevContent).asJsonObject + } + + inline fun content(): T? { + return toModel(content) + } + + inline fun prevContent(): T? { + return toModel(prevContent) + } + + inline fun toModel(data: Map?): T? { + val moshi = MoshiProvider.providesMoshi() + val moshiAdapter = moshi.adapter(T::class.java) + return moshiAdapter.fromJsonValue(data) + } + + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/rooms/PowerLevels.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/rooms/PowerLevels.kt new file mode 100644 index 0000000000..dc69642c1d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/rooms/PowerLevels.kt @@ -0,0 +1,117 @@ +package im.vector.matrix.android.api.rooms + +import android.text.TextUtils +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.events.EventType +import java.util.* + +@JsonClass(generateAdapter = true) +data class PowerLevels( + @Json(name = "ban") val ban: Int = 50, + @Json(name = "kick") val kick: Int = 50, + @Json(name = "invite") val invite: Int = 50, + @Json(name = "redact") val redact: Int = 50, + @Json(name = "events_default") val eventsDefault: Int = 0, + @Json(name = "events") val events: MutableMap = HashMap(), + @Json(name = "users_default") val usersDefault: Int = 0, + @Json(name = "users") val users: MutableMap = HashMap(), + @Json(name = "state_default") val stateDefault: Int = 50, + @Json(name = "notifications") val notifications: Map = HashMap() +) { + /** + * Returns the user power level of a dedicated user Id + * + * @param userId the user id + * @return the power level + */ + fun getUserPowerLevel(userId: String): Int { + // sanity check + if (!TextUtils.isEmpty(userId)) { + val powerLevel = users[userId] + return powerLevel ?: usersDefault + } + + return usersDefault + } + + /** + * Updates the user power levels of a dedicated user id + * + * @param userId the user + * @param powerLevel the new power level + */ + fun setUserPowerLevel(userId: String?, powerLevel: Int) { + if (null != userId) { + users[userId] = Integer.valueOf(powerLevel) + } + } + + /** + * Tell if an user can send an event of type 'eventTypeString'. + * + * @param eventTypeString the event type (in Event.EVENT_TYPE_XXX values) + * @param userId the user id + * @return true if the user can send the event + */ + fun maySendEventOfType(eventTypeString: String, userId: String): Boolean { + return if (!TextUtils.isEmpty(eventTypeString) && !TextUtils.isEmpty(userId)) { + getUserPowerLevel(userId) >= minimumPowerLevelForSendingEventAsMessage(eventTypeString) + } else false + + } + + /** + * Tells if an user can send a room message. + * + * @param userId the user id + * @return true if the user can send a room message + */ + fun maySendMessage(userId: String): Boolean { + return maySendEventOfType(EventType.MESSAGE, userId) + } + + /** + * Helper to get the minimum power level the user must have to send an event of the given type + * as a message. + * + * @param eventTypeString the type of event (in Event.EVENT_TYPE_XXX values) + * @return the required minimum power level. + */ + fun minimumPowerLevelForSendingEventAsMessage(eventTypeString: String?): Int { + return events[eventTypeString] ?: eventsDefault + } + + /** + * Helper to get the minimum power level the user must have to send an event of the given type + * as a state event. + * + * @param eventTypeString the type of event (in Event.EVENT_TYPE_STATE_ values). + * @return the required minimum power level. + */ + fun minimumPowerLevelForSendingEventAsStateEvent(eventTypeString: String?): Int { + return events[eventTypeString] ?: stateDefault + } + + + /** + * Get the notification level for a dedicated key. + * + * @param key the notification key + * @return the level + */ + fun notificationLevel(key: String?): Int { + if (null != key && notifications.containsKey(key)) { + val valAsVoid = notifications[key] + + // the first implementation was a string value + return if (valAsVoid is String) { + Integer.parseInt(valAsVoid) + } else { + valAsVoid as Int + } + } + + return 50 + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/rooms/RoomDirectoryVisibility.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/rooms/RoomDirectoryVisibility.kt index 0741634573..c6faf2df34 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/rooms/RoomDirectoryVisibility.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/rooms/RoomDirectoryVisibility.kt @@ -3,8 +3,6 @@ package im.vector.matrix.android.api.rooms import com.squareup.moshi.Json enum class RoomDirectoryVisibility { - - @Json(name = "private") PRIVATE, - @Json(name = "public") PUBLIC - + @Json(name = "private") PRIVATE, + @Json(name = "public") PUBLIC } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/rooms/RoomHistoryVisibility.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/rooms/RoomHistoryVisibility.kt index e6aae1ab4b..23d69708d0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/rooms/RoomHistoryVisibility.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/rooms/RoomHistoryVisibility.kt @@ -7,6 +7,4 @@ enum class RoomHistoryVisibility { @Json(name = "invited") INVITED, @Json(name = "joined") JOINED, @Json(name = "word_readable") WORLD_READABLE - - } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/rooms/RoomState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/rooms/RoomState.kt index 30a9eedd33..29ce205544 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/rooms/RoomState.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/rooms/RoomState.kt @@ -1,13 +1,749 @@ package im.vector.matrix.android.api.rooms +import android.text.TextUtils +import im.vector.matrix.android.api.events.Event +import im.vector.matrix.android.api.events.EventType +import im.vector.matrix.android.api.rooms.timeline.EventTimeline +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.rest.callback.ApiCallback +import im.vector.matrix.android.internal.legacy.rest.callback.SimpleApiCallback +import im.vector.matrix.android.internal.legacy.rest.model.* +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.pid.RoomThirdPartyInvite +import im.vector.matrix.android.internal.legacy.util.JsonUtils +import im.vector.matrix.android.internal.legacy.util.Log +import timber.log.Timber +import java.util.* +import kotlin.collections.ArrayList + +/** + * The state of a room. + */ data class RoomState( + var roomId: String? = null, + var powerLevels: PowerLevels? = null, + var canonicalAlias: String? = null, var name: String? = null, var topic: String? = null, + var roomTombstoneContent: RoomTombstoneContent? = null, var url: String? = null, - var avatar_url: String? = null, - var join_rule: String? = null, - var guest_access: String? = null, - var history_visibility: RoomHistoryVisibility? = null, - var visibility: RoomDirectoryVisibility? = null, - var groups: List = emptyList() -) \ No newline at end of file + var avatarUrl: String? = null, + var roomCreateContent: RoomCreateContent? = null, + var roomPinnedEventsContent: RoomPinnedEventsContent? = null, + var joinRule: String? = null, + var guestAccess: String = RoomState.GUEST_ACCESS_FORBIDDEN, + var historyVisibility: String? = RoomState.HISTORY_VISIBILITY_SHARED, + var visibility: String? = null, + var algorithm: String? = null, + var groups: List = emptyList(), + var token: String? = null, + + private var mergedAliasesList: MutableList? = null, + private var currentAliases: MutableList = ArrayList(), + private var roomAliases: MutableMap = HashMap(), + private var aliasesByDomain_: MutableMap> = HashMap(), + private var stateEvents: MutableMap> = HashMap(), + private var notificationCount_: Int = 0, + private var highlightCount_: Int = 0, + private val members: HashMap = HashMap(), + private var allMembersAreLoaded: Boolean = false, + private val getAllMembersCallbacks: ArrayList>> = ArrayList(), + private val thirdPartyInvites: HashMap = HashMap(), + private val membersWithThirdPartyInviteTokenCache: HashMap = HashMap(), + private var isLive: Boolean = false, + private var memberDisplayNameByUserId: MutableMap = HashMap() +) { + + lateinit var dataHandler: MXDataHandler + + /** + * @return a copy of the room members list. May be incomplete if the full list is not loaded yet + */ + // make a copy to avoid concurrency modifications + val loadedMembers: List + get() { + val res: List + synchronized(this) { + res = ArrayList(members.values) + } + return res + } + + /** + * @return a copy of the displayable members list. May be incomplete if the full list is not loaded yet + */ + val displayableLoadedMembers: List + get() { + val conferenceUserId = getMember(MXCallsManager.getConferenceUserId(roomId)) + return loadedMembers.filter { it != conferenceUserId } + } + + /** + * 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. + */ + /** + * Set this room as a conference user room + * + * @param isConferenceUserRoom true when it is an user conference room. + */ + var isConferenceUserRoom: Boolean + get() = dataHandler.store.getSummary(roomId)?.isConferenceUserRoom ?: false + set(isConferenceUserRoom) = dataHandler.store!!.getSummary(roomId)!!.setIsConferenceUserRoom(isConferenceUserRoom) + + /** + * @return the notified messages count. + */ + /** + * Update the notified messages count. + * + * @param notificationCount the new notified messages count. + */ + var notificationCount: Int + get() = notificationCount_ + set(notificationCount) { + Timber.d("## setNotificationCount() : $notificationCount room id $roomId") + notificationCount_ = notificationCount + } + + /** + * @return the highlighted messages count. + */ + /** + * Update the highlighted messages count. + * + * @param highlightCount the new highlighted messages count. + */ + var highlightCount: Int + get() = highlightCount_ + set(highlightCount) { + Timber.d("## setHighlightCount() : $highlightCount room id $roomId") + highlightCount_ = highlightCount + } + + /** + * Provides the currentAliases by domain + * + * @return the currentAliases list map + */ + val aliasesByDomain: Map> + get() = HashMap(aliasesByDomain_) + + /** + * @return true if the room is encrypted + */ + // 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. + val isEncrypted: Boolean + get() = 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. + */ + val isVersioned: Boolean + get() = roomTombstoneContent != null + + /** + * @return true if the room is a public one + */ + val isPublic: Boolean + get() = TextUtils.equals(if (null != visibility) visibility else joinRule, RoomDirectoryVisibility.DIRECTORY_VISIBILITY_PUBLIC) + + /** + * 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. + */ + fun getMembersAsync(callback: ApiCallback>) { + if (areAllMembersLoaded()) { + val res: List + + synchronized(this) { + // make a copy to avoid concurrency modifications + res = ArrayList(members.values) + } + + callback.onSuccess(res) + } else { + val doTheRequest: Boolean + + synchronized(getAllMembersCallbacks) { + getAllMembersCallbacks.add(callback) + + doTheRequest = getAllMembersCallbacks.size == 1 + } + + if (doTheRequest) { + // Load members from server + dataHandler.getMembersAsync(roomId, object : SimpleApiCallback>(callback) { + override fun onSuccess(info: List) { + Timber.d("getMembers has returned " + info.size + " users.") + + val store = (dataHandler as MXDataHandler).store + var res: List + + for (member in info) { + // Do not erase already known members form the sync + if (getMember(member.userId) == null) { + setMember(member.userId, member) + + // Also create a User + store?.updateUserWithRoomMemberEvent(member) + } + } + + synchronized(getAllMembersCallbacks) { + for (apiCallback in getAllMembersCallbacks) { + // make a copy to avoid concurrency modifications + res = ArrayList(members.values) + + apiCallback.onSuccess(res) + } + + getAllMembersCallbacks.clear() + } + + allMembersAreLoaded = true + } + }) + } + } + } + + /** + * Tell if all members has been loaded + * + * @return true if LazyLoading is Off, or if all members has been loaded + */ + private fun areAllMembersLoaded(): Boolean { + return !dataHandler.isLazyLoadingEnabled || allMembersAreLoaded + } + + /** + * Force a fetch of the loaded members the next time they will be requested + */ + fun forceMembersRequest() { + allMembersAreLoaded = 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. + */ + fun getStateEvents(types: Set?): List { + val filteredStateEvents = ArrayList() + val stateEvents = ArrayList() + + // merge the values lists + val currentStateEvents = this.stateEvents.values + for (eventsList in currentStateEvents) { + stateEvents.addAll(eventsList) + } + + if (null != types && !types.isEmpty()) { + for (stateEvent in stateEvents) { + if (types.contains(stateEvent.type)) { + 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. + */ + fun getStateEvents(store: IMXStore?, types: Set?, callback: ApiCallback>) { + if (null != store) { + val stateEvents = ArrayList() + + val currentStateEvents = this.stateEvents.values + + for (eventsList in currentStateEvents) { + stateEvents.addAll(eventsList) + } + + // retrieve the roomMember creation events + store.getRoomStateEvents(roomId, object : SimpleApiCallback>() { + override fun onSuccess(events: List) { + stateEvents.addAll(events) + + val filteredStateEvents = ArrayList() + + if (null != types && !types.isEmpty()) { + for (stateEvent in stateEvents) { + if (types.contains(stateEvent.type)) { + filteredStateEvents.add(stateEvent) + } + } + } else { + filteredStateEvents.addAll(stateEvents) + } + + callback.onSuccess(filteredStateEvents) + } + }) + } + } + + /** + * 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. + */ + fun getDisplayableMembersAsync(callback: ApiCallback>) { + getMembersAsync(object : SimpleApiCallback>(callback) { + override fun onSuccess(members: List) { + val conferenceUserId = getMember(MXCallsManager.getConferenceUserId(roomId)) + + if (null != conferenceUserId) { + val membersList = ArrayList(members) + membersList.remove(conferenceUserId) + callback.onSuccess(membersList) + } else { + callback.onSuccess(members) + } + } + }) + } + + /** + * Update the room member from its user id. + * + * @param userId the user id. + * @param member the new member value. + */ + private fun setMember(userId: String, member: RoomMember) { + // Populate a basic user object if there is none + if (member.userId == null) { + member.userId = userId + } + synchronized(this) { + if (null != memberDisplayNameByUserId) { + memberDisplayNameByUserId!!.remove(userId) + } + members.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 + fun getMember(userId: String): RoomMember? { + val member: RoomMember? + + synchronized(this) { + member = members[userId] + } + + if (member == null) { + // TODO LazyLoading + Log.e(LOG_TAG, "!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Null member '$userId' !!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + + if (TextUtils.equals(dataHandler.userId, 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. + */ + fun getMemberByEventId(eventId: String): RoomMember? { + var member: RoomMember? = null + + synchronized(this) { + for (aMember in members.values) { + if (aMember.originalEventId == eventId) { + member = aMember + break + } + } + } + + return member + } + + /** + * Remove a member defines by its user id. + * + * @param userId the user id. + */ + fun removeMember(userId: String) { + synchronized(this) { + members.remove(userId) + // remove the cached display name + if (null != memberDisplayNameByUserId) { + memberDisplayNameByUserId!!.remove(userId) + } + } + } + + /** + * Retrieve a member from an invitation token. + * + * @param thirdPartyInviteToken the third party invitation token. + * @return the member it exists. + */ + fun memberWithThirdPartyInviteToken(thirdPartyInviteToken: String): RoomMember? { + return membersWithThirdPartyInviteTokenCache[thirdPartyInviteToken] + } + + /** + * Retrieve a RoomThirdPartyInvite from its token. + * + * @param thirdPartyInviteToken the third party invitation token. + * @return the linked RoomThirdPartyInvite if it exists + */ + fun thirdPartyInviteWithToken(thirdPartyInviteToken: String): RoomThirdPartyInvite? { + return thirdPartyInvites[thirdPartyInviteToken] + } + + /** + * @return the third party invite list. + */ + fun thirdPartyInvites(): Collection { + return thirdPartyInvites.values + } + + /** + * 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. + */ + fun canBackPaginate(isJoined: Boolean, isInvited: Boolean): Boolean { + val visibility = if (TextUtils.isEmpty(historyVisibility)) HISTORY_VISIBILITY_SHARED else historyVisibility + return (isJoined + || visibility == HISTORY_VISIBILITY_WORLD_READABLE + || visibility == HISTORY_VISIBILITY_SHARED + || visibility == HISTORY_VISIBILITY_INVITED && isInvited) + } + + /** + * Provides the currentAliases for any known domains + * + * @return the currentAliases list + */ + fun getAliases(): List { + if (mergedAliasesList != null) { + return mergedAliasesList as List + } + val merged = ArrayList() + for (url in aliasesByDomain.keys) { + merged.addAll(aliasesByDomain[url] ?: emptyList()) + } + // ensure that the current currentAliases have been added. + // for example for the public rooms because there is no applystate call. + for (anAlias in currentAliases) { + if (merged.indexOf(anAlias) < 0) { + merged.add(anAlias) + } + } + mergedAliasesList = merged + return merged + } + + /** + * Remove an alias. + * + * @param alias the alias to remove + */ + fun removeAlias(alias: String) { + if (getAliases().indexOf(alias) >= 0) { + currentAliases.remove(alias) + for (host in aliasesByDomain.keys) { + aliasesByDomain_[host]?.remove(alias) + } + } + mergedAliasesList = null + } + + /** + * Add an alias. + * + * @param alias the alias to add + */ + fun addAlias(alias: String) { + if (getAliases().indexOf(alias) < 0) { + // patch until the server echoes the alias addition. + mergedAliasesList?.add(alias) + } + } + + /** + * @return true if the room has a predecessor + */ + fun hasPredecessor(): Boolean { + return roomCreateContent != null && roomCreateContent!!.hasPredecessor() + } + + /** + * @return the encryption algorithm + */ + fun encryptionAlgorithm(): String? { + return if (TextUtils.isEmpty(algorithm)) null else 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 + */ + fun applyState(store: IMXStore?, event: Event, direction: EventTimeline.Direction): Boolean { + if (event.stateKey == null) { + return false + } + val contentToConsider = if (direction == EventTimeline.Direction.FORWARDS) event.contentAsJsonObject else event.prevContentAsJsonObject + val dataToConsider = if (direction == EventTimeline.Direction.FORWARDS) event.content else event.prevContent + + + val eventType = event.type + try { + if (EventType.STATE_ROOM_NAME == eventType) { + name = JsonUtils.toStateEvent(contentToConsider).name + } else if (EventType.STATE_ROOM_TOPIC == eventType) { + topic = JsonUtils.toStateEvent(contentToConsider).topic + } else if (EventType.STATE_ROOM_CREATE == eventType) { + roomCreateContent = JsonUtils.toRoomCreateContent(contentToConsider) + } else if (EventType.STATE_ROOM_JOIN_RULES == eventType) { + joinRule = JsonUtils.toStateEvent(contentToConsider).joinRule + } else if (EventType.STATE_ROOM_GUEST_ACCESS == eventType) { + guestAccess = JsonUtils.toStateEvent(contentToConsider).guestAccess + } else if (EventType.STATE_ROOM_ALIASES == eventType) { + if (!TextUtils.isEmpty(event.stateKey)) { + // backward compatibility + currentAliases = JsonUtils.toStateEvent(contentToConsider).aliases + // sanity check + if (null != currentAliases) { + aliasesByDomain_[event.stateKey] = currentAliases + roomAliases[event.stateKey] = event + } else { + aliasesByDomain_[event.stateKey] = ArrayList() + } + } + } else if (EventType.ENCRYPTION == 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 (algorithm == null) { + algorithm = "" + } + } else if (EventType.STATE_CANONICAL_ALIAS == eventType) { + // SPEC-125 + canonicalAlias = JsonUtils.toStateEvent(contentToConsider).canonicalAlias + } else if (EventType.STATE_HISTORY_VISIBILITY == eventType) { + // SPEC-134 + historyVisibility = JsonUtils.toStateEvent(contentToConsider).historyVisibility + } else if (EventType.STATE_ROOM_AVATAR == eventType) { + url = JsonUtils.toStateEvent(contentToConsider).url + } else if (EventType.STATE_RELATED_GROUPS == eventType) { + groups = JsonUtils.toStateEvent(contentToConsider).groups + } else if (EventType.STATE_ROOM_MEMBER == eventType) { + val member = JsonUtils.toRoomMember(contentToConsider) + val userId = event.stateKey + if (member == null) { + // the member has already been removed + if (getMember(userId) == null) { + Log.e(LOG_TAG, "## applyState() : the user $userId is not anymore a member of $roomId") + return false + } + removeMember(userId) + } else { + try { + member.userId = userId + member.originServerTs = event.originServerTs ?: -1 + member.originalEventId = event.eventId + member.mSender = event.sender + if (null != store && direction == EventTimeline.Direction.FORWARDS) { + store.storeRoomStateEvent(roomId, event) + } + val 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 (currentMember != null && (TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_LEAVE) || TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_BAN))) { + if (member.getAvatarUrl() == null) { + member.setAvatarUrl(currentMember.getAvatarUrl()) + } + if (member.displayname == null) { + member.displayname = currentMember.displayname + } + // remove the cached display name + memberDisplayNameByUserId.remove(userId) + + // test if the user has been kicked + if (!TextUtils.equals(event.sender, 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.thirdPartyInviteToken)) { + membersWithThirdPartyInviteTokenCache[member.thirdPartyInviteToken] = member + } + } catch (e: Exception) { + Log.e(LOG_TAG, "## applyState() - EVENT_TYPE_STATE_ROOM_MEMBER failed " + e.message, e) + } + + setMember(userId, member) + } + } else if (EventType.STATE_ROOM_POWER_LEVELS == eventType) { + powerLevels = event.toModel(dataToConsider) + } else if (EventType.STATE_ROOM_THIRD_PARTY_INVITE == eventType) { + val thirdPartyInvite = JsonUtils.toRoomThirdPartyInvite(contentToConsider) + thirdPartyInvite.token = event.stateKey + if (direction == EventTimeline.Direction.FORWARDS && null != store) { + store.storeRoomStateEvent(roomId, event) + } + if (!TextUtils.isEmpty(thirdPartyInvite.token)) { + thirdPartyInvites[thirdPartyInvite.token] = thirdPartyInvite + } + } else if (EventType.STATE_ROOM_TOMBSTONE == eventType) { + roomTombstoneContent = JsonUtils.toRoomTombstoneContent(contentToConsider) + } else if (EventType.STATE_PINNED_EVENT == eventType) { + roomPinnedEventsContent = JsonUtils.toRoomPinnedEventsContent(contentToConsider) + } + // same the latest room state events + // excepts the membership ones + // they are saved elsewhere + if (!TextUtils.isEmpty(eventType) && EventType.STATE_ROOM_MEMBER != eventType) { + var eventsList: MutableList? = stateEvents[eventType] + if (eventsList == null) { + eventsList = ArrayList() + stateEvents[eventType] = eventsList + } + eventsList.add(event) + } + + } catch (e: Exception) { + Log.e(LOG_TAG, "applyState failed with error " + e.message, e) + } + + return true + } + + /** + * Return an unique display name of the member userId. + * + * @param userId the user id + * @return unique display name + */ + fun getMemberName(userId: String?): String? { + // sanity check + if (userId == null) { + return null + } + var displayName: String? + synchronized(this) { + displayName = memberDisplayNameByUserId[userId] + } + if (displayName != null) { + return displayName + } + // Get the user display name from the member list of the room + val member = getMember(userId) + // Do not consider null display name + if (null != member && !TextUtils.isEmpty(member.displayname)) { + displayName = member.displayname + synchronized(this) { + val matrixIds = ArrayList() + // Disambiguate users who have the same display name in the room + for (aMember in members.values) { + if (displayName == aMember.displayname) { + matrixIds.add(aMember.userId) + } + } + // if several users have the same display name + // index it i.e bob () + if (matrixIds.size > 1) { + displayName += " ($userId)" + } + } + } else if (null != member && TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_INVITE)) { + val user = dataHandler.getUser(userId) + if (null != user) { + displayName = user.displayname + } + } + if (displayName == null) { + // By default, use the user ID + displayName = userId + } + displayName?.let { + memberDisplayNameByUserId[userId] = it + } + return displayName + } + + companion object { + private val LOG_TAG = RoomState::class.java.simpleName + private val serialVersionUID = -6019932024524988201L + + val JOIN_RULE_PUBLIC = "public" + val JOIN_RULE_INVITE = "invite" + + /** + * room access is granted to guests + */ + val GUEST_ACCESS_CAN_JOIN = "can_join" + /** + * room access is denied to guests + */ + val GUEST_ACCESS_FORBIDDEN = "forbidden" + + val HISTORY_VISIBILITY_SHARED = "shared" + val HISTORY_VISIBILITY_INVITED = "invited" + val HISTORY_VISIBILITY_JOINED = "joined" + val HISTORY_VISIBILITY_WORLD_READABLE = "world_readable" + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt index b8f523384d..1a38a172a8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt @@ -1,10 +1,11 @@ package im.vector.matrix.android.internal.di import com.squareup.moshi.Moshi +import im.vector.matrix.android.internal.network.parsing.UriMoshiAdapter object MoshiProvider { - private val moshi: Moshi = Moshi.Builder().build() + private val moshi: Moshi = Moshi.Builder().add(UriMoshiAdapter()).build() fun providesMoshi(): Moshi { return moshi diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt index ebf688d7ca..8fbe902e4f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt @@ -40,7 +40,7 @@ class NetworkModule : Module { } single { - Moshi.Builder().add(UriMoshiAdapter()).build() + MoshiProvider.providesMoshi() } single { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/events/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/events/sync/RoomSyncHandler.kt new file mode 100644 index 0000000000..8d609841a5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/events/sync/RoomSyncHandler.kt @@ -0,0 +1,9 @@ +package im.vector.matrix.android.internal.events.sync + +class RoomSyncHandler() { + + + + + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/events/sync/StateEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/events/sync/StateEvent.kt new file mode 100644 index 0000000000..602f6dcd13 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/events/sync/StateEvent.kt @@ -0,0 +1,18 @@ +package im.vector.matrix.android.internal.events.sync + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class StateEvent( + @Json(name = "name") val name: String? = null, + @Json(name = "topic") val topic: String? = null, + @Json(name = "join_rule") val joinRule: String? = null, + @Json(name = "guest_access") val guestAccess: String? = null, + @Json(name = "alias") val canonicalAlias: String? = null, + @Json(name = "aliases") val aliases: List? = null, + @Json(name = "algorithm") val algorithm: String? = null, + @Json(name = "history_visibility") val historyVisibility: String? = null, + @Json(name = "url") val url: String? = null, + @Json(name = "groups") val groups: List? = null +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomState.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomState.java index 26eef40c47..ce10ab52b1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomState.java +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/RoomState.java @@ -379,7 +379,7 @@ public class RoomState implements Externalizable { * @param callback the asynchronous callback. */ public void getStateEvents(IMXStore store, final Set types, final ApiCallback> callback) { - if (null != store) { + /* if (null != store) { final List stateEvents = new ArrayList<>(); Collection> currentStateEvents = mStateEvents.values(); @@ -409,7 +409,7 @@ public class RoomState implements Externalizable { callback.onSuccess(filteredStateEvents); } }); - } + }*/ } /** @@ -937,7 +937,7 @@ public class RoomState implements Externalizable { member.mSender = event.getSender(); if ((null != store) && (direction == EventTimeline.Direction.FORWARDS)) { - store.storeRoomStateEvent(roomId, event); + //store.storeRoomStateEvent(roomId, event); } RoomMember currentMember = getMember(userId); @@ -998,7 +998,7 @@ public class RoomState implements Externalizable { thirdPartyInvite.token = event.stateKey; if ((direction == EventTimeline.Direction.FORWARDS) && (null != store)) { - store.storeRoomStateEvent(roomId, event); + //store.storeRoomStateEvent(roomId, event); } if (!TextUtils.isEmpty(thirdPartyInvite.token)) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/IMXStore.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/IMXStore.java index 428edcce36..ab390bc2ac 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/IMXStore.java +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/IMXStore.java @@ -430,7 +430,7 @@ public interface IMXStore { * @param roomId the room id * @param event the event */ - void storeRoomStateEvent(String roomId, Event event); + void storeRoomStateEvent(String roomId, im.vector.matrix.android.api.events.Event event); /** * Retrieve the room state creation events @@ -438,7 +438,7 @@ public interface IMXStore { * @param roomId the room id * @param callback the asynchronous callback */ - void getRoomStateEvents(String roomId, ApiCallback> callback); + void getRoomStateEvents(String roomId, ApiCallback> callback); /** * Return the list of latest unsent events. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/MXFileStore.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/MXFileStore.java index db1edda090..38c026d9e0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/MXFileStore.java +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/MXFileStore.java @@ -1369,7 +1369,7 @@ public class MXFileStore extends MXMemoryStore { private Map> mPendingRoomStateEvents = new HashMap<>(); @Override - public void storeRoomStateEvent(final String roomId, final Event event) { + public void storeRoomStateEvent(final String roomId, final im.vector.matrix.android.api.events.Event event) { /*boolean isAlreadyLoaded = true; synchronized (mRoomStateEventsByRoomId) { @@ -1477,7 +1477,7 @@ public class MXFileStore extends MXMemoryStore { } @Override - public void getRoomStateEvents(final String roomId, final ApiCallback> callback) { + public void getRoomStateEvents(final String roomId, final ApiCallback> callback) { boolean isAlreadyLoaded = true; /*synchronized (mRoomStateEventsByRoomId) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/MXMemoryStore.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/MXMemoryStore.java index 69a07ac8fc..1482abfdc2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/MXMemoryStore.java +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/MXMemoryStore.java @@ -871,7 +871,7 @@ public class MXMemoryStore implements IMXStore { } @Override - public void storeRoomStateEvent(String roomId, Event event) { + public void storeRoomStateEvent(String roomId, im.vector.matrix.android.api.events.Event event) { /*synchronized (mRoomStateEventsByRoomId) { Map events = mRoomStateEventsByRoomId.get(roomId); @@ -888,8 +888,8 @@ public class MXMemoryStore implements IMXStore { } @Override - public void getRoomStateEvents(final String roomId, final ApiCallback> callback) { - final List events = new ArrayList<>(); + public void getRoomStateEvents(final String roomId, final ApiCallback> callback) { + final List events = new ArrayList<>(); /*synchronized (mRoomStateEventsByRoomId) { if (mRoomStateEventsByRoomId.containsKey(roomId)) {