From f0dbb92d76dbdb2b52489e3abc0e58cd23351658 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 30 Jun 2020 19:45:17 +0200 Subject: [PATCH 001/122] Attempt to clean db [WIP] --- .../database/helper/ChunkEntityHelper.kt | 1 + .../internal/database/model/ChunkEntity.kt | 1 + .../internal/session/DefaultSession.kt | 55 +++++++++++++++++++ .../internal/session/filter/FilterUtil.kt | 3 +- 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index d86151e694..bc681e4eb8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -115,6 +115,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String, true } } + numberOfTimelineEvents++ timelineEvents.add(timelineEventEntity) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt index 19bf72970c..b1e08fdeea 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt @@ -27,6 +27,7 @@ internal open class ChunkEntity(@Index var prevToken: String? = null, @Index var nextToken: String? = null, var stateEvents: RealmList = RealmList(), var timelineEvents: RealmList = RealmList(), + var numberOfTimelineEvents: Long= 0, // Only one chunk will have isLastForward == true @Index var isLastForward: Boolean = false, @Index var isLastBackward: Boolean = false diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index e32ba7e63c..f8ddd3d345 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -51,9 +51,21 @@ import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.widgets.WidgetService import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.crypto.DefaultCryptoService +import im.vector.matrix.android.internal.database.awaitTransaction +import im.vector.matrix.android.internal.database.helper.nextDisplayIndex +import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.ChunkEntityFields +import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity +import im.vector.matrix.android.internal.database.model.CurrentStateEventEntityFields +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields +import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.WorkManagerProvider import im.vector.matrix.android.internal.session.identity.DefaultIdentityService +import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecryptor import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.session.sync.job.SyncThread @@ -61,6 +73,7 @@ import im.vector.matrix.android.internal.session.sync.job.SyncWorker import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.createUIHandler +import io.realm.RealmConfiguration import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.greenrobot.eventbus.EventBus @@ -77,6 +90,7 @@ internal class DefaultSession @Inject constructor( private val eventBus: EventBus, @SessionId override val sessionId: String, + @SessionDatabase private val realmConfiguration: RealmConfiguration, private val lifecycleObservers: Set<@JvmSuppressWildcards SessionLifecycleObserver>, private val sessionListeners: SessionListeners, private val roomService: Lazy, @@ -151,6 +165,47 @@ internal class DefaultSession @Inject constructor( } eventBus.register(this) timelineEventDecryptor.start() + taskExecutor.executorScope.launch(Dispatchers.Default) { + awaitTransaction(realmConfiguration) { realm -> + val allRooms = realm.where(RoomEntity::class.java).findAll() + val numberOfEvents = realm.where(EventEntity::class.java).findAll().size + val numberOfTimelineEvents = realm.where(TimelineEventEntity::class.java).findAll().size + Timber.v("Number of events in db: $numberOfEvents | Number of timeline events in db: $numberOfTimelineEvents") + Timber.v("Number of rooms in db: ${allRooms.size}") + if (numberOfTimelineEvents < 30_000L) { + Timber.v("Db is low enough") + } else { + + val hugeChunks = realm.where(ChunkEntity::class.java).greaterThan(ChunkEntityFields.NUMBER_OF_TIMELINE_EVENTS, 250).findAll() + Timber.v("There are ${hugeChunks.size} chunks to clean") + for (chunk in hugeChunks) { + val maxDisplayIndex = chunk.nextDisplayIndex(PaginationDirection.FORWARDS) + val thresholdDisplayIndex = maxDisplayIndex - 250 + val eventsToRemove = chunk.timelineEvents.where().lessThan(TimelineEventEntityFields.DISPLAY_INDEX, thresholdDisplayIndex).findAll() + Timber.v("There are ${eventsToRemove.size} events to clean in chunk: ${chunk.identifier()} from room ${chunk.room?.first()?.roomId}") + chunk.numberOfTimelineEvents = chunk.numberOfTimelineEvents - eventsToRemove.size + eventsToRemove.forEach { + val canDeleteRoot = it.root?.stateKey == null + if (canDeleteRoot) { + it.root?.deleteFromRealm() + } + it.readReceipts?.readReceipts?.deleteAllFromRealm() + it.readReceipts?.deleteFromRealm() + it.annotations?.apply { + editSummary?.deleteFromRealm() + pollResponseSummary?.deleteFromRealm() + referencesSummaryEntity?.deleteFromRealm() + reactionsSummary.deleteAllFromRealm() + } + it.annotations?.deleteFromRealm() + it.readReceipts?.deleteFromRealm() + it.deleteFromRealm() + } + } + } + + } + } } override fun requireBackgroundSync() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterUtil.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterUtil.kt index 53ede5ad45..d95a53cb07 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterUtil.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterUtil.kt @@ -98,7 +98,8 @@ internal object FilterUtil { state = filter.room.state?.copy(lazyLoadMembers = true) ?: RoomEventFilter(lazyLoadMembers = true) ) - ?: RoomFilter(state = RoomEventFilter(lazyLoadMembers = true)) + ?: RoomFilter(state = RoomEventFilter(lazyLoadMembers = true), + timeline = RoomEventFilter(limit = 1500)) ) } else { val newRoomEventFilter = filter.room?.state?.copy(lazyLoadMembers = null)?.takeIf { it.hasData() } From 2f6b38eb39f76fc645403b2d3237719b93f34bcb Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 2 Jul 2020 15:32:58 +0200 Subject: [PATCH 002/122] Introduce EventInsertEntity to handle db updates --- .../tasks/RoomVerificationUpdateTask.kt | 161 ----------- .../VerificationMessageLiveObserver.kt | 177 +++++++++--- .../database/EventInsertLiveObserver.kt | 101 +++++++ .../database/RealmLiveEntityObserver.kt | 3 +- .../internal/database/mapper/EventMapper.kt | 1 - .../database/model/EventInsertEntity.kt | 28 ++ .../database/model/SessionRealmModule.kt | 1 + .../database/query/EventEntityQueries.kt | 17 +- .../internal/session/DefaultSession.kt | 6 +- .../session/EventInsertLiveProcessor.kt | 27 ++ .../android/internal/session/SessionModule.kt | 29 +- .../session/call/CallEventObserver.kt | 66 ----- .../session/call/CallEventProcessor.kt | 65 +++++ .../session/call/CallEventsObserverTask.kt | 92 ------- .../internal/session/call/CallModule.kt | 2 - .../session/group/GroupSummaryUpdater.kt | 4 +- ... => EventRelationsAggregationProcessor.kt} | 259 ++++++++---------- .../room/EventRelationsAggregationUpdater.kt | 76 ----- .../internal/session/room/RoomModule.kt | 8 - .../create/RoomCreateEventLiveObserver.kt | 72 ----- .../room/create/RoomCreateEventProcessor.kt | 45 +++ .../session/room/prune/EventsPruner.kt | 56 ---- ...ventTask.kt => RedactionEventProcessor.kt} | 29 +- .../session/room/timeline/DefaultTimeline.kt | 11 - .../timeline/TimelineHiddenReadReceipts.kt | 2 +- .../RoomTombstoneEventLiveObserver.kt | 75 ----- .../tombstone/RoomTombstoneEventProcessor.kt | 48 ++++ 27 files changed, 617 insertions(+), 844 deletions(-) delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/EventInsertLiveObserver.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventInsertEntity.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/EventInsertLiveProcessor.kt delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallEventObserver.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallEventProcessor.kt delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallEventsObserverTask.kt rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/{EventRelationsAggregationTask.kt => EventRelationsAggregationProcessor.kt} (71%) delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventProcessor.kt delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/{PruneEventTask.kt => RedactionEventProcessor.kt} (87%) delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt deleted file mode 100644 index 400febc15f..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright 2019 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.crypto.tasks - -import im.vector.matrix.android.api.session.crypto.CryptoService -import im.vector.matrix.android.api.session.crypto.MXCryptoError -import im.vector.matrix.android.api.session.crypto.verification.VerificationService -import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.message.MessageContent -import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent -import im.vector.matrix.android.api.session.room.model.message.MessageType -import im.vector.matrix.android.api.session.room.model.message.MessageVerificationReadyContent -import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent -import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent -import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult -import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService -import im.vector.matrix.android.internal.di.DeviceId -import im.vector.matrix.android.internal.di.UserId -import im.vector.matrix.android.internal.task.Task -import timber.log.Timber -import java.util.ArrayList -import javax.inject.Inject - -internal interface RoomVerificationUpdateTask : Task { - data class Params( - val events: List, - val verificationService: DefaultVerificationService, - val cryptoService: CryptoService - ) -} - -internal class DefaultRoomVerificationUpdateTask @Inject constructor( - @UserId private val userId: String, - @DeviceId private val deviceId: String?, - private val cryptoService: CryptoService) : RoomVerificationUpdateTask { - - companion object { - // XXX what about multi-account? - private val transactionsHandledByOtherDevice = ArrayList() - } - - override suspend fun execute(params: RoomVerificationUpdateTask.Params) { - // TODO ignore initial sync or back pagination? - - params.events.forEach { event -> - Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}") - - // If the request is in the future by more than 5 minutes or more than 10 minutes in the past, - // the message should be ignored by the receiver. - - if (!VerificationService.isValidRequest(event.ageLocalTs - ?: event.originServerTs)) return@forEach Unit.also { - Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated") - } - - // decrypt if needed? - if (event.isEncrypted() && event.mxDecryptionResult == null) { - // TODO use a global event decryptor? attache to session and that listen to new sessionId? - // for now decrypt sync - try { - val result = cryptoService.decryptEvent(event, "") - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) - } catch (e: MXCryptoError) { - Timber.e("## SAS Failed to decrypt event: ${event.eventId}") - params.verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event) - } - } - Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}") - - // Relates to is not encrypted - val relatesToEventId = event.content.toModel()?.relatesTo?.eventId - - if (event.senderId == userId) { - // If it's send from me, we need to keep track of Requests or Start - // done from another device of mine - - if (EventType.MESSAGE == event.getClearType()) { - val msgType = event.getClearContent().toModel()?.msgType - if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) { - event.getClearContent().toModel()?.let { - if (it.fromDevice != deviceId) { - // The verification is requested from another device - Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ") - event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } - } - } - } - } else if (EventType.KEY_VERIFICATION_START == event.getClearType()) { - event.getClearContent().toModel()?.let { - if (it.fromDevice != deviceId) { - // The verification is started from another device - Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ") - relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } - params.verificationService.onRoomRequestHandledByOtherDevice(event) - } - } - } else if (EventType.KEY_VERIFICATION_READY == event.getClearType()) { - event.getClearContent().toModel()?.let { - if (it.fromDevice != deviceId) { - // The verification is started from another device - Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ") - relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } - params.verificationService.onRoomRequestHandledByOtherDevice(event) - } - } - } else if (EventType.KEY_VERIFICATION_CANCEL == event.getClearType() || EventType.KEY_VERIFICATION_DONE == event.getClearType()) { - relatesToEventId?.let { - transactionsHandledByOtherDevice.remove(it) - params.verificationService.onRoomRequestHandledByOtherDevice(event) - } - } - - Timber.v("## SAS Verification ignoring message sent by me: ${event.eventId} type: ${event.getClearType()}") - return@forEach - } - - if (relatesToEventId != null && transactionsHandledByOtherDevice.contains(relatesToEventId)) { - // Ignore this event, it is directed to another of my devices - Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesToEventId ") - return@forEach - } - when (event.getClearType()) { - EventType.KEY_VERIFICATION_START, - EventType.KEY_VERIFICATION_ACCEPT, - EventType.KEY_VERIFICATION_KEY, - EventType.KEY_VERIFICATION_MAC, - EventType.KEY_VERIFICATION_CANCEL, - EventType.KEY_VERIFICATION_READY, - EventType.KEY_VERIFICATION_DONE -> { - params.verificationService.onRoomEvent(event) - } - EventType.MESSAGE -> { - if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel()?.msgType) { - params.verificationService.onRoomRequestReceived(event) - } - } - } - } - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt index 4eab1748b8..f932dd7a69 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt @@ -15,59 +15,150 @@ */ package im.vector.matrix.android.internal.crypto.verification -import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.crypto.CryptoService +import im.vector.matrix.android.api.session.crypto.MXCryptoError +import im.vector.matrix.android.api.session.crypto.verification.VerificationService +import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.LocalEcho -import im.vector.matrix.android.internal.crypto.tasks.DefaultRoomVerificationUpdateTask -import im.vector.matrix.android.internal.crypto.tasks.RoomVerificationUpdateTask -import im.vector.matrix.android.internal.database.RealmLiveEntityObserver -import im.vector.matrix.android.internal.database.mapper.asDomain -import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.query.whereTypes -import im.vector.matrix.android.internal.di.SessionDatabase -import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.task.configureWith -import io.realm.OrderedCollectionChangeSet -import io.realm.RealmConfiguration -import io.realm.RealmResults +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent +import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationReadyContent +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent +import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult +import im.vector.matrix.android.internal.di.DeviceId +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.session.EventInsertLiveProcessor +import io.realm.Realm +import timber.log.Timber +import java.util.ArrayList import javax.inject.Inject -internal class VerificationMessageLiveObserver @Inject constructor( - @SessionDatabase realmConfiguration: RealmConfiguration, - private val roomVerificationUpdateTask: DefaultRoomVerificationUpdateTask, +internal class VerificationMessageProcessor @Inject constructor( private val cryptoService: CryptoService, private val verificationService: DefaultVerificationService, - private val taskExecutor: TaskExecutor -) : RealmLiveEntityObserver(realmConfiguration) { + @UserId private val userId: String, + @DeviceId private val deviceId: String? +) : EventInsertLiveProcessor { - override val query = Monarchy.Query { - EventEntity.whereTypes(it, listOf( - EventType.KEY_VERIFICATION_START, - EventType.KEY_VERIFICATION_ACCEPT, - EventType.KEY_VERIFICATION_KEY, - EventType.KEY_VERIFICATION_MAC, - EventType.KEY_VERIFICATION_CANCEL, - EventType.KEY_VERIFICATION_DONE, - EventType.KEY_VERIFICATION_READY, - EventType.MESSAGE, - EventType.ENCRYPTED) - ) + private val transactionsHandledByOtherDevice = ArrayList() + + private val allowedTypes = listOf( + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_READY, + EventType.MESSAGE, + EventType.ENCRYPTED + ) + + override fun shouldProcess(eventId: String, eventType: String): Boolean { + return allowedTypes.contains(eventType) && !LocalEcho.isLocalEchoId(eventId) } - override fun onChange(results: RealmResults, changeSet: OrderedCollectionChangeSet) { - // Should we ignore when it's an initial sync? - val events = changeSet.insertions - .asSequence() - .mapNotNull { results[it]?.asDomain() } - .filterNot { - // ignore local echos - LocalEcho.isLocalEchoId(it.eventId ?: "") - } - .toList() + override suspend fun process(realm: Realm, event: Event) { + Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}") - roomVerificationUpdateTask.configureWith( - RoomVerificationUpdateTask.Params(events, verificationService, cryptoService) - ).executeBy(taskExecutor) + // If the request is in the future by more than 5 minutes or more than 10 minutes in the past, + // the message should be ignored by the receiver. + + if (!VerificationService.isValidRequest(event.ageLocalTs + ?: event.originServerTs)) return Unit.also { + Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated") + } + + // decrypt if needed? + if (event.isEncrypted() && event.mxDecryptionResult == null) { + // TODO use a global event decryptor? attache to session and that listen to new sessionId? + // for now decrypt sync + try { + val result = cryptoService.decryptEvent(event, "") + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } catch (e: MXCryptoError) { + Timber.e("## SAS Failed to decrypt event: ${event.eventId}") + verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event) + } + } + Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}") + + // Relates to is not encrypted + val relatesToEventId = event.content.toModel()?.relatesTo?.eventId + + if (event.senderId == userId) { + // If it's send from me, we need to keep track of Requests or Start + // done from another device of mine + + if (EventType.MESSAGE == event.getClearType()) { + val msgType = event.getClearContent().toModel()?.msgType + if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) { + event.getClearContent().toModel()?.let { + if (it.fromDevice != deviceId) { + // The verification is requested from another device + Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ") + event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } + } + } + } + } else if (EventType.KEY_VERIFICATION_START == event.getClearType()) { + event.getClearContent().toModel()?.let { + if (it.fromDevice != deviceId) { + // The verification is started from another device + Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ") + relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } + verificationService.onRoomRequestHandledByOtherDevice(event) + } + } + } else if (EventType.KEY_VERIFICATION_READY == event.getClearType()) { + event.getClearContent().toModel()?.let { + if (it.fromDevice != deviceId) { + // The verification is started from another device + Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ") + relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } + verificationService.onRoomRequestHandledByOtherDevice(event) + } + } + } else if (EventType.KEY_VERIFICATION_CANCEL == event.getClearType() || EventType.KEY_VERIFICATION_DONE == event.getClearType()) { + relatesToEventId?.let { + transactionsHandledByOtherDevice.remove(it) + verificationService.onRoomRequestHandledByOtherDevice(event) + } + } + + Timber.v("## SAS Verification ignoring message sent by me: ${event.eventId} type: ${event.getClearType()}") + return + } + + if (relatesToEventId != null && transactionsHandledByOtherDevice.contains(relatesToEventId)) { + // Ignore this event, it is directed to another of my devices + Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesToEventId ") + return + } + when (event.getClearType()) { + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_DONE -> { + verificationService.onRoomEvent(event) + } + EventType.MESSAGE -> { + if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel()?.msgType) { + verificationService.onRoomRequestReceived(event) + } + } + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/EventInsertLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/EventInsertLiveObserver.kt new file mode 100644 index 0000000000..443ca781bc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/EventInsertLiveObserver.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2020 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.database + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.crypto.CryptoService +import im.vector.matrix.android.api.session.crypto.MXCryptoError +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult +import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.EventInsertEntity +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.SessionDatabase +import im.vector.matrix.android.internal.session.EventInsertLiveProcessor +import io.realm.OrderedCollectionChangeSet +import io.realm.RealmConfiguration +import io.realm.RealmResults +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration, + private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>, + private val cryptoService: CryptoService) + : RealmLiveEntityObserver(realmConfiguration) { + + override val query = Monarchy.Query { + it.where(EventInsertEntity::class.java) + } + + override fun onChange(results: RealmResults) { + if (!results.isLoaded || results.isEmpty()) { + return + } + Timber.v("EventInsertEntity updated with ${results.size} results in db") + val filteredEventIds = results.mapNotNull { + val shouldProcess = shouldProcess(it) + if (shouldProcess) { + it.eventId + } else { + null + } + } + Timber.v("There are ${filteredEventIds.size} events to process") + observerScope.launch { + awaitTransaction(realmConfiguration) { realm -> + filteredEventIds.forEach { eventId -> + val event = EventEntity.where(realm, eventId).findFirst() + if (event == null) { + Timber.v("Event $eventId not found") + return@forEach + } + val domainEvent = event.asDomain() + decryptIfNeeded(domainEvent) + processors.forEach { + it.process(realm, domainEvent) + } + } + realm.where(EventInsertEntity::class.java).findAll().deleteAllFromRealm() + } + } + } + + private fun decryptIfNeeded(event: Event) { + if (event.isEncrypted() && event.mxDecryptionResult == null) { + try { + val result = cryptoService.decryptEvent(event, event.roomId ?: "") + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } catch (e: MXCryptoError) { + Timber.v("Call service: Failed to decrypt event") + // TODO -> we should keep track of this and retry, or aggregation will be broken + } + } + } + + private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean { + return processors.any { + it.shouldProcess(eventInsertEntity.eventId, eventInsertEntity.eventType) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt index c3ace55e1c..af67bae526 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.internal.session.SessionLifecycleObserver import im.vector.matrix.android.internal.util.createBackgroundHandler import io.realm.OrderedRealmCollectionChangeListener import io.realm.Realm +import io.realm.RealmChangeListener import io.realm.RealmConfiguration import io.realm.RealmObject import io.realm.RealmResults @@ -33,7 +34,7 @@ import java.util.concurrent.atomic.AtomicReference internal interface LiveEntityObserver: SessionLifecycleObserver internal abstract class RealmLiveEntityObserver(protected val realmConfiguration: RealmConfiguration) - : LiveEntityObserver, OrderedRealmCollectionChangeListener> { + : LiveEntityObserver, RealmChangeListener> { private companion object { val BACKGROUND_HANDLER = createBackgroundHandler("LIVE_ENTITY_BACKGROUND") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt index 141403b6d4..11a5616bfb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt @@ -45,7 +45,6 @@ internal object EventMapper { eventEntity.redacts = event.redacts eventEntity.age = event.unsignedData?.age ?: event.originServerTs eventEntity.unsignedData = uds - eventEntity.decryptionResultJson = event.mxDecryptionResult?.let { MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(it) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventInsertEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventInsertEntity.kt new file mode 100644 index 0000000000..c2744fc721 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventInsertEntity.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 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.database.model + +import io.realm.RealmObject +import io.realm.annotations.Index + +internal open class EventInsertEntity(var eventId: String = "", + var eventType: String = "" +) : RealmObject() { + + companion object + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt index 9eceb56141..efe4c4955e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt @@ -25,6 +25,7 @@ import io.realm.annotations.RealmModule classes = [ ChunkEntity::class, EventEntity::class, + EventInsertEntity::class, TimelineEventEntity::class, FilterEntity::class, GroupEntity::class, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt index d998c41ccb..1020b2bfaf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt @@ -18,16 +18,25 @@ package im.vector.matrix.android.internal.database.query import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntityFields +import im.vector.matrix.android.internal.database.model.EventInsertEntity import io.realm.Realm import io.realm.RealmList import io.realm.RealmQuery import io.realm.kotlin.where internal fun EventEntity.copyToRealmOrIgnore(realm: Realm): EventEntity { - return realm.where() - .equalTo(EventEntityFields.EVENT_ID, eventId) - .equalTo(EventEntityFields.ROOM_ID, roomId) - .findFirst() ?: realm.copyToRealm(this) + val eventEntity = realm.where() + .equalTo(EventEntityFields.EVENT_ID, eventId) + .equalTo(EventEntityFields.ROOM_ID, roomId) + .findFirst() + return if (eventEntity == null) { + val insertEntity = EventInsertEntity(eventId = eventId, eventType = type) + realm.insert(insertEntity) + // copy this event entity and return it + realm.copyToRealm(this) + } else { + eventEntity + } } internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQuery { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index 5f7f178d8c..72d6f95852 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -166,6 +166,7 @@ internal class DefaultSession @Inject constructor( } eventBus.register(this) timelineEventDecryptor.start() + taskExecutor.executorScope.launch(Dispatchers.Default) { awaitTransaction(realmConfiguration) { realm -> val allRooms = realm.where(RoomEntity::class.java).findAll() @@ -176,9 +177,9 @@ internal class DefaultSession @Inject constructor( if (numberOfTimelineEvents < 30_000L) { Timber.v("Db is low enough") } else { - val hugeChunks = realm.where(ChunkEntity::class.java).greaterThan(ChunkEntityFields.NUMBER_OF_TIMELINE_EVENTS, 250).findAll() Timber.v("There are ${hugeChunks.size} chunks to clean") + /* for (chunk in hugeChunks) { val maxDisplayIndex = chunk.nextDisplayIndex(PaginationDirection.FORWARDS) val thresholdDisplayIndex = maxDisplayIndex - 250 @@ -203,8 +204,9 @@ internal class DefaultSession @Inject constructor( it.deleteFromRealm() } } - } + */ + } } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/EventInsertLiveProcessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/EventInsertLiveProcessor.kt new file mode 100644 index 0000000000..c781d91c04 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/EventInsertLiveProcessor.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 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.session + +import im.vector.matrix.android.api.session.events.model.Event +import io.realm.Realm + +internal interface EventInsertLiveProcessor { + + fun shouldProcess(eventId: String, eventType: String): Boolean + + suspend fun process(realm: Realm, event: Event) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index fb05bc68a2..b8aaddd086 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -39,7 +39,8 @@ import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageSer import im.vector.matrix.android.api.session.typing.TypingUsersTracker import im.vector.matrix.android.internal.crypto.crosssigning.ShieldTrustUpdater import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecretStorageService -import im.vector.matrix.android.internal.crypto.verification.VerificationMessageLiveObserver +import im.vector.matrix.android.internal.crypto.verification.VerificationMessageProcessor +import im.vector.matrix.android.internal.database.EventInsertLiveObserver import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory import im.vector.matrix.android.internal.di.Authenticated import im.vector.matrix.android.internal.di.DeviceId @@ -64,16 +65,16 @@ import im.vector.matrix.android.internal.network.httpclient.addSocketFactory import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor import im.vector.matrix.android.internal.network.token.AccessTokenProvider import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider -import im.vector.matrix.android.internal.session.call.CallEventObserver +import im.vector.matrix.android.internal.session.call.CallEventProcessor import im.vector.matrix.android.internal.session.download.DownloadProgressInterceptor import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater import im.vector.matrix.android.internal.session.homeserver.DefaultHomeServerCapabilitiesService import im.vector.matrix.android.internal.session.identity.DefaultIdentityService import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManager -import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater -import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLiveObserver -import im.vector.matrix.android.internal.session.room.prune.EventsPruner -import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventLiveObserver +import im.vector.matrix.android.internal.session.room.EventRelationsAggregationProcessor +import im.vector.matrix.android.internal.session.room.create.RoomCreateEventProcessor +import im.vector.matrix.android.internal.session.room.prune.RedactionEventProcessor +import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventProcessor import im.vector.matrix.android.internal.session.securestorage.DefaultSecureStorageService import im.vector.matrix.android.internal.session.typing.DefaultTypingUsersTracker import im.vector.matrix.android.internal.session.user.accountdata.DefaultAccountDataService @@ -297,27 +298,31 @@ internal abstract class SessionModule { @Binds @IntoSet - abstract fun bindEventsPruner(pruner: EventsPruner): SessionLifecycleObserver + abstract fun bindEventRedactionProcessor(processor: RedactionEventProcessor): EventInsertLiveProcessor @Binds @IntoSet - abstract fun bindEventRelationsAggregationUpdater(updater: EventRelationsAggregationUpdater): SessionLifecycleObserver + abstract fun bindEventRelationsAggregationProcessor(processor: EventRelationsAggregationProcessor): EventInsertLiveProcessor @Binds @IntoSet - abstract fun bindRoomTombstoneEventLiveObserver(observer: RoomTombstoneEventLiveObserver): SessionLifecycleObserver + abstract fun bindRoomTombstoneEventProcessor(processor: RoomTombstoneEventProcessor): EventInsertLiveProcessor @Binds @IntoSet - abstract fun bindRoomCreateEventLiveObserver(observer: RoomCreateEventLiveObserver): SessionLifecycleObserver + abstract fun bindRoomCreateEventProcessor(processor: RoomCreateEventProcessor): EventInsertLiveProcessor @Binds @IntoSet - abstract fun bindVerificationMessageLiveObserver(observer: VerificationMessageLiveObserver): SessionLifecycleObserver + abstract fun bindVerificationMessageProcessor(processor: VerificationMessageProcessor): EventInsertLiveProcessor @Binds @IntoSet - abstract fun bindCallEventObserver(observer: CallEventObserver): SessionLifecycleObserver + abstract fun bindCallEventProcessor(processor: CallEventProcessor): EventInsertLiveProcessor + + @Binds + @IntoSet + abstract fun bindEventInsertObserver(observer: EventInsertLiveObserver): SessionLifecycleObserver @Binds @IntoSet diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallEventObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallEventObserver.kt deleted file mode 100644 index 585ecb61ca..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallEventObserver.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2020 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.session.call - -import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.internal.database.RealmLiveEntityObserver -import im.vector.matrix.android.internal.database.mapper.asDomain -import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.query.whereTypes -import im.vector.matrix.android.internal.di.SessionDatabase -import im.vector.matrix.android.internal.di.UserId -import io.realm.OrderedCollectionChangeSet -import io.realm.RealmConfiguration -import io.realm.RealmResults -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject - -internal class CallEventObserver @Inject constructor( - @SessionDatabase realmConfiguration: RealmConfiguration, - @UserId private val userId: String, - private val task: CallEventsObserverTask -) : RealmLiveEntityObserver(realmConfiguration) { - - override val query = Monarchy.Query { - EventEntity.whereTypes(it, listOf( - EventType.CALL_ANSWER, - EventType.CALL_CANDIDATES, - EventType.CALL_INVITE, - EventType.CALL_HANGUP, - EventType.ENCRYPTED) - ) - } - - override fun onChange(results: RealmResults, changeSet: OrderedCollectionChangeSet) { - Timber.v("EventRelationsAggregationUpdater called with ${changeSet.insertions.size} insertions") - - val insertedDomains = changeSet.insertions - .asSequence() - .mapNotNull { results[it]?.asDomain() } - .toList() - - val params = CallEventsObserverTask.Params( - insertedDomains, - userId - ) - observerScope.launch { - task.execute(params) - } - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallEventProcessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallEventProcessor.kt new file mode 100644 index 0000000000..8a4a5407d5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallEventProcessor.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2020 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.session.call + +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.session.EventInsertLiveProcessor +import io.realm.Realm +import timber.log.Timber +import javax.inject.Inject + +internal class CallEventProcessor @Inject constructor( + @UserId private val userId: String, + private val callService: DefaultCallSignalingService +) : EventInsertLiveProcessor { + + private val allowedTypes = listOf( + EventType.CALL_ANSWER, + EventType.CALL_CANDIDATES, + EventType.CALL_INVITE, + EventType.CALL_HANGUP, + EventType.ENCRYPTED + ) + + override fun shouldProcess(eventId: String, eventType: String): Boolean { + return allowedTypes.contains(eventType) + } + + override suspend fun process(realm: Realm, event: Event) { + update(realm, event) + } + + private fun update(realm: Realm, event: Event) { + val now = System.currentTimeMillis() + // TODO might check if an invite is not closed (hangup/answsered) in the same event batch? + event.roomId ?: return Unit.also { + Timber.w("Event with no room id ${event.eventId}") + } + val age = now - (event.ageLocalTs ?: now) + if (age > 40_000) { + // To old to ring? + return + } + event.ageLocalTs + if (EventType.isCallEvent(event.getClearType())) { + callService.onCallEvent(event) + } + Timber.v("$realm : $userId") + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallEventsObserverTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallEventsObserverTask.kt deleted file mode 100644 index 2d96bd3b23..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallEventsObserverTask.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (c) 2020 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.session.call - -import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.session.crypto.CryptoService -import im.vector.matrix.android.api.session.crypto.MXCryptoError -import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult -import im.vector.matrix.android.internal.di.SessionDatabase -import im.vector.matrix.android.internal.task.Task -import im.vector.matrix.android.internal.util.awaitTransaction -import io.realm.Realm -import timber.log.Timber -import javax.inject.Inject - -internal interface CallEventsObserverTask : Task { - - data class Params( - val events: List, - val userId: String - ) -} - -internal class DefaultCallEventsObserverTask @Inject constructor( - @SessionDatabase private val monarchy: Monarchy, - private val cryptoService: CryptoService, - private val callService: DefaultCallSignalingService) : CallEventsObserverTask { - - override suspend fun execute(params: CallEventsObserverTask.Params) { - val events = params.events - val userId = params.userId - monarchy.awaitTransaction { realm -> - Timber.v(">>> DefaultCallEventsObserverTask[${params.hashCode()}] called with ${events.size} events") - update(realm, events, userId) - Timber.v("<<< DefaultCallEventsObserverTask[${params.hashCode()}] finished") - } - } - - private fun update(realm: Realm, events: List, userId: String) { - val now = System.currentTimeMillis() - // TODO might check if an invite is not closed (hangup/answsered) in the same event batch? - events.forEach { event -> - event.roomId ?: return@forEach Unit.also { - Timber.w("Event with no room id ${event.eventId}") - } - val age = now - (event.ageLocalTs ?: now) - if (age > 40_000) { - // To old to ring? - return@forEach - } - event.ageLocalTs - decryptIfNeeded(event) - if (EventType.isCallEvent(event.getClearType())) { - callService.onCallEvent(event) - } - } - Timber.v("$realm : $userId") - } - - private fun decryptIfNeeded(event: Event) { - if (event.isEncrypted() && event.mxDecryptionResult == null) { - try { - val result = cryptoService.decryptEvent(event, event.roomId ?: "") - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) - } catch (e: MXCryptoError) { - Timber.v("Call service: Failed to decrypt event") - // TODO -> we should keep track of this and retry, or aggregation will be broken - } - } - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallModule.kt index a25d198e83..5e75738dec 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallModule.kt @@ -42,6 +42,4 @@ internal abstract class CallModule { @Binds abstract fun bindGetTurnServerTask(task: DefaultGetTurnServerTask): GetTurnServerTask - @Binds - abstract fun bindCallEventsObserverTask(task: DefaultCallEventsObserverTask): CallEventsObserverTask } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt index b8f8e84bde..52268ab7de 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt @@ -43,7 +43,8 @@ internal class GroupSummaryUpdater @Inject constructor( override val query = Monarchy.Query { GroupEntity.where(it) } - override fun onChange(results: RealmResults, changeSet: OrderedCollectionChangeSet) { + override fun onChange(results: RealmResults) { + /* // `insertions` for new groups and `changes` to handle left groups val modifiedGroupEntity = (changeSet.insertions + changeSet.changes) .asSequence() @@ -63,6 +64,7 @@ internal class GroupSummaryUpdater @Inject constructor( deleteGroups(it) } } + */ } private fun fetchGroupsData(groupIds: List) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationProcessor.kt similarity index 71% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationProcessor.kt index c84b39118e..9cb9b7254b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationProcessor.kt @@ -15,9 +15,7 @@ */ package im.vector.matrix.android.internal.session.room -import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.crypto.CryptoService -import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.events.model.AggregatedAnnotation import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType @@ -32,7 +30,6 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessagePollResponseContent import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent import im.vector.matrix.android.api.session.room.model.relation.ReactionContent -import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.mapper.EventMapper @@ -47,21 +44,12 @@ import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.create import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.di.SessionDatabase -import im.vector.matrix.android.internal.task.Task -import im.vector.matrix.android.internal.util.awaitTransaction +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.session.EventInsertLiveProcessor import io.realm.Realm import timber.log.Timber import javax.inject.Inject -internal interface EventRelationsAggregationTask : Task { - - data class Params( - val events: List, - val userId: String - ) -} - enum class VerificationState { REQUEST, WAITING, @@ -89,161 +77,146 @@ private fun VerificationState?.toState(newState: VerificationState): Verificatio return newState } -/** - * Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base. - */ -internal class DefaultEventRelationsAggregationTask @Inject constructor( - @SessionDatabase private val monarchy: Monarchy, - private val cryptoService: CryptoService) : EventRelationsAggregationTask { +internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String, + private val cryptoService: CryptoService +) : EventInsertLiveProcessor { - // OPT OUT serer aggregation until API mature enough - private val SHOULD_HANDLE_SERVER_AGREGGATION = false + private val allowedTypes = listOf( + EventType.MESSAGE, + EventType.REDACTION, + EventType.REACTION, + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_MAC, + // TODO Add ? + // EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_KEY, + EventType.ENCRYPTED + ) - override suspend fun execute(params: EventRelationsAggregationTask.Params) { - val events = params.events - val userId = params.userId - monarchy.awaitTransaction { realm -> - Timber.v(">>> DefaultEventRelationsAggregationTask[${params.hashCode()}] called with ${events.size} events") - update(realm, events, userId) - Timber.v("<<< DefaultEventRelationsAggregationTask[${params.hashCode()}] finished") - } + override fun shouldProcess(eventId: String, eventType: String): Boolean { + return allowedTypes.contains(eventType) } - private fun update(realm: Realm, events: List, userId: String) { - events.forEach { event -> - try { // Temporary catch, should be removed - val roomId = event.roomId - if (roomId == null) { - Timber.w("Event has no room id ${event.eventId}") - return@forEach + override suspend fun process(realm: Realm, event: Event) { + try { // Temporary catch, should be removed + val roomId = event.roomId + if (roomId == null) { + Timber.w("Event has no room id ${event.eventId}") + return + } + val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "") + when (event.getClearType()) { + EventType.REACTION -> { + // we got a reaction!! + Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}") + handleReaction(event, roomId, realm, userId, isLocalEcho) } - val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "") - when (event.type) { - EventType.REACTION -> { - // we got a reaction!! - Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}") - handleReaction(event, roomId, realm, userId, isLocalEcho) - } - EventType.MESSAGE -> { - if (event.unsignedData?.relations?.annotations != null) { - Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}") - handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm) + EventType.MESSAGE -> { + if (event.unsignedData?.relations?.annotations != null) { + Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}") + handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm) - EventAnnotationsSummaryEntity.where(realm, event.eventId - ?: "").findFirst()?.let { - TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId - ?: "").findFirst()?.let { tet -> - tet.annotations = it - } - } - } - - val content: MessageContent? = event.content.toModel() - if (content?.relatesTo?.type == RelationType.REPLACE) { - Timber.v("###REPLACE in room $roomId for event ${event.eventId}") - // A replace! - handleReplace(realm, event, content, roomId, isLocalEcho) - } else if (content?.relatesTo?.type == RelationType.RESPONSE) { - Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") - handleResponse(realm, userId, event, content, roomId, isLocalEcho) - } - } - - EventType.KEY_VERIFICATION_DONE, - EventType.KEY_VERIFICATION_CANCEL, - EventType.KEY_VERIFICATION_ACCEPT, - EventType.KEY_VERIFICATION_START, - EventType.KEY_VERIFICATION_MAC, - EventType.KEY_VERIFICATION_READY, - EventType.KEY_VERIFICATION_KEY -> { - Timber.v("## SAS REF in room $roomId for event ${event.eventId}") - event.content.toModel()?.relatesTo?.let { - if (it.type == RelationType.REFERENCE && it.eventId != null) { - handleVerification(realm, event, roomId, isLocalEcho, it.eventId, userId) + EventAnnotationsSummaryEntity.where(realm, event.eventId + ?: "").findFirst()?.let { + TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId + ?: "").findFirst()?.let { tet -> + tet.annotations = it } } } - EventType.ENCRYPTED -> { - // Relation type is in clear - val encryptedEventContent = event.content.toModel() - if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE - || encryptedEventContent?.relatesTo?.type == RelationType.RESPONSE - ) { - // we need to decrypt if needed - decryptIfNeeded(event) - event.getClearContent().toModel()?.let { - if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) { - Timber.v("###REPLACE in room $roomId for event ${event.eventId}") - // A replace! - handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) - } else if (encryptedEventContent.relatesTo.type == RelationType.RESPONSE) { - Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") - handleResponse(realm, userId, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) - } + val content: MessageContent? = event.content.toModel() + if (content?.relatesTo?.type == RelationType.REPLACE) { + Timber.v("###REPLACE in room $roomId for event ${event.eventId}") + // A replace! + handleReplace(realm, event, content, roomId, isLocalEcho) + } else if (content?.relatesTo?.type == RelationType.RESPONSE) { + Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") + handleResponse(realm, userId, event, content, roomId, isLocalEcho) + } + } + + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_KEY -> { + Timber.v("## SAS REF in room $roomId for event ${event.eventId}") + event.content.toModel()?.relatesTo?.let { + if (it.type == RelationType.REFERENCE && it.eventId != null) { + handleVerification(realm, event, roomId, isLocalEcho, it.eventId, userId) + } + } + } + + EventType.ENCRYPTED -> { + // Relation type is in clear + val encryptedEventContent = event.content.toModel() + if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE + || encryptedEventContent?.relatesTo?.type == RelationType.RESPONSE + ) { + // we need to decrypt if needed + event.getClearContent().toModel()?.let { + if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) { + Timber.v("###REPLACE in room $roomId for event ${event.eventId}") + // A replace! + handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) + } else if (encryptedEventContent.relatesTo.type == RelationType.RESPONSE) { + Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") + handleResponse(realm, userId, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) } - } else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) { - decryptIfNeeded(event) - when (event.getClearType()) { - EventType.KEY_VERIFICATION_DONE, - EventType.KEY_VERIFICATION_CANCEL, - EventType.KEY_VERIFICATION_ACCEPT, - EventType.KEY_VERIFICATION_START, - EventType.KEY_VERIFICATION_MAC, - EventType.KEY_VERIFICATION_READY, - EventType.KEY_VERIFICATION_KEY -> { - Timber.v("## SAS REF in room $roomId for event ${event.eventId}") - encryptedEventContent.relatesTo.eventId?.let { - handleVerification(realm, event, roomId, isLocalEcho, it, userId) - } + } + } else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) { + when (event.getClearType()) { + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_KEY -> { + Timber.v("## SAS REF in room $roomId for event ${event.eventId}") + encryptedEventContent.relatesTo.eventId?.let { + handleVerification(realm, event, roomId, isLocalEcho, it, userId) } } } } - EventType.REDACTION -> { - val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } - ?: return@forEach - when (eventToPrune.type) { - EventType.MESSAGE -> { - Timber.d("REDACTION for message ${eventToPrune.eventId}") + } + EventType.REDACTION -> { + val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } + ?: return + when (eventToPrune.type) { + EventType.MESSAGE -> { + Timber.d("REDACTION for message ${eventToPrune.eventId}") // val unsignedData = EventMapper.map(eventToPrune).unsignedData // ?: UnsignedData(null, null) - // was this event a m.replace - val contentModel = ContentMapper.map(eventToPrune.content)?.toModel() - if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { - handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm) - } - } - EventType.REACTION -> { - handleReactionRedact(eventToPrune, realm, userId) + // was this event a m.replace + val contentModel = ContentMapper.map(eventToPrune.content)?.toModel() + if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { + handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm) } } + EventType.REACTION -> { + handleReactionRedact(eventToPrune, realm, userId) + } } - else -> Timber.v("UnHandled event ${event.eventId}") } - } catch (t: Throwable) { - Timber.e(t, "## Should not happen ") + else -> Timber.v("UnHandled event ${event.eventId}") } + } catch (t: Throwable) { + Timber.e(t, "## Should not happen ") } } - private fun decryptIfNeeded(event: Event) { - if (event.mxDecryptionResult == null) { - try { - val result = cryptoService.decryptEvent(event, "") - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) - } catch (e: MXCryptoError) { - Timber.v("Failed to decrypt e2e replace") - // TODO -> we should keep track of this and retry, or aggregation will be broken - } - } - } + // OPT OUT serer aggregation until API mature enough + private val SHOULD_HANDLE_SERVER_AGREGGATION = false private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean, relatedEventId: String? = null) { val eventId = event.eventId ?: return diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt deleted file mode 100644 index 7ddcf3542d..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2019 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.session.room - -import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.internal.database.RealmLiveEntityObserver -import im.vector.matrix.android.internal.database.mapper.asDomain -import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.query.whereTypes -import im.vector.matrix.android.internal.di.SessionDatabase -import im.vector.matrix.android.internal.di.UserId -import io.realm.OrderedCollectionChangeSet -import io.realm.RealmConfiguration -import io.realm.RealmResults -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject - -/** - * Acts as a listener of incoming messages in order to incrementally computes a summary of annotations. - * For reactions will build a EventAnnotationsSummaryEntity, ans for edits a EditAggregatedSummaryEntity. - * The summaries can then be extracted and added (as a decoration) to a TimelineEvent for final display. - */ -internal class EventRelationsAggregationUpdater @Inject constructor( - @SessionDatabase realmConfiguration: RealmConfiguration, - @UserId private val userId: String, - private val task: EventRelationsAggregationTask) : - RealmLiveEntityObserver(realmConfiguration) { - - override val query = Monarchy.Query { - EventEntity.whereTypes(it, listOf( - EventType.MESSAGE, - EventType.REDACTION, - EventType.REACTION, - EventType.KEY_VERIFICATION_DONE, - EventType.KEY_VERIFICATION_CANCEL, - EventType.KEY_VERIFICATION_ACCEPT, - EventType.KEY_VERIFICATION_START, - EventType.KEY_VERIFICATION_MAC, - // TODO Add ? - // EventType.KEY_VERIFICATION_READY, - EventType.KEY_VERIFICATION_KEY, - EventType.ENCRYPTED) - ) - } - - override fun onChange(results: RealmResults, changeSet: OrderedCollectionChangeSet) { - Timber.v("EventRelationsAggregationUpdater called with ${changeSet.insertions.size} insertions") - - val insertedDomains = changeSet.insertions - .asSequence() - .mapNotNull { results[it]?.asDomain() } - .toList() - val params = EventRelationsAggregationTask.Params( - insertedDomains, - userId - ) - observerScope.launch { - task.execute(params) - } - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index 5e84920fbd..ca59f9b7bb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -44,8 +44,6 @@ import im.vector.matrix.android.internal.session.room.membership.joining.InviteT import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.session.room.membership.leaving.DefaultLeaveRoomTask import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask -import im.vector.matrix.android.internal.session.room.prune.DefaultPruneEventTask -import im.vector.matrix.android.internal.session.room.prune.PruneEventTask import im.vector.matrix.android.internal.session.room.read.DefaultMarkAllRoomsReadTask import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask @@ -129,9 +127,6 @@ internal abstract class RoomModule { @Binds abstract fun bindFileService(service: DefaultFileService): FileService - @Binds - abstract fun bindEventRelationsAggregationTask(task: DefaultEventRelationsAggregationTask): EventRelationsAggregationTask - @Binds abstract fun bindCreateRoomTask(task: DefaultCreateRoomTask): CreateRoomTask @@ -156,9 +151,6 @@ internal abstract class RoomModule { @Binds abstract fun bindLoadRoomMembersTask(task: DefaultLoadRoomMembersTask): LoadRoomMembersTask - @Binds - abstract fun bindPruneEventTask(task: DefaultPruneEventTask): PruneEventTask - @Binds abstract fun bindSetReadMarkersTask(task: DefaultSetReadMarkersTask): SetReadMarkersTask diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt deleted file mode 100644 index fb3880e38d..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2019 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.session.room.create - -import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.VersioningState -import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent -import im.vector.matrix.android.internal.database.RealmLiveEntityObserver -import im.vector.matrix.android.internal.database.awaitTransaction -import im.vector.matrix.android.internal.database.mapper.asDomain -import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.RoomSummaryEntity -import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.database.query.whereTypes -import im.vector.matrix.android.internal.di.SessionDatabase -import io.realm.OrderedCollectionChangeSet -import io.realm.RealmConfiguration -import io.realm.RealmResults -import kotlinx.coroutines.launch -import javax.inject.Inject - -internal class RoomCreateEventLiveObserver @Inject constructor(@SessionDatabase - realmConfiguration: RealmConfiguration) - : RealmLiveEntityObserver(realmConfiguration) { - - override val query = Monarchy.Query { - EventEntity.whereTypes(it, listOf(EventType.STATE_ROOM_CREATE)) - } - - override fun onChange(results: RealmResults, changeSet: OrderedCollectionChangeSet) { - changeSet.insertions - .asSequence() - .mapNotNull { - results[it]?.asDomain() - } - .toList() - .also { - observerScope.launch { - handleRoomCreateEvents(it) - } - } - } - - private suspend fun handleRoomCreateEvents(createEvents: List) = awaitTransaction(realmConfiguration) { realm -> - for (event in createEvents) { - val createRoomContent = event.getClearContent().toModel() - val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: continue - - val predecessorRoomSummary = RoomSummaryEntity.where(realm, predecessorRoomId).findFirst() - ?: RoomSummaryEntity(predecessorRoomId) - predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_JOINED - realm.insertOrUpdate(predecessorRoomSummary) - } - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventProcessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventProcessor.kt new file mode 100644 index 0000000000..f2a3da3a30 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventProcessor.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 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.session.room.create + +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.VersioningState +import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.session.EventInsertLiveProcessor +import io.realm.Realm +import javax.inject.Inject + +internal class RoomCreateEventProcessor @Inject constructor() : EventInsertLiveProcessor { + + override suspend fun process(realm: Realm, event: Event) { + val createRoomContent = event.getClearContent().toModel() + val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: return + + val predecessorRoomSummary = RoomSummaryEntity.where(realm, predecessorRoomId).findFirst() + ?: RoomSummaryEntity(predecessorRoomId) + predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_JOINED + realm.insertOrUpdate(predecessorRoomSummary) + } + + override fun shouldProcess(eventId: String, eventType: String): Boolean { + return eventType == EventType.STATE_ROOM_CREATE + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt deleted file mode 100644 index 27e00c75ab..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2019 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.session.room.prune - -import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.internal.database.RealmLiveEntityObserver -import im.vector.matrix.android.internal.database.mapper.asDomain -import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.query.whereTypes -import im.vector.matrix.android.internal.di.SessionDatabase -import io.realm.OrderedCollectionChangeSet -import io.realm.RealmConfiguration -import io.realm.RealmResults -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject - -/** - * Listens to the database for the insertion of any redaction event. - * As it will actually delete the content, it should be called last in the list of listener. - */ -internal class EventsPruner @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration, - private val pruneEventTask: PruneEventTask) : - RealmLiveEntityObserver(realmConfiguration) { - - override val query = Monarchy.Query { EventEntity.whereTypes(it, listOf(EventType.REDACTION)) } - - override fun onChange(results: RealmResults, changeSet: OrderedCollectionChangeSet) { - Timber.v("Event pruner called with ${changeSet.insertions.size} insertions") - - val insertedDomains = changeSet.insertions - .asSequence() - .mapNotNull { results[it]?.asDomain() } - .toList() - - observerScope.launch { - val params = PruneEventTask.Params(insertedDomains) - pruneEventTask.execute(params) - } - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/RedactionEventProcessor.kt similarity index 87% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/RedactionEventProcessor.kt index b801843d18..2f61a98df3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/RedactionEventProcessor.kt @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package im.vector.matrix.android.internal.session.room.prune -import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.LocalEcho @@ -27,28 +27,23 @@ import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.findWithSenderMembershipEvent import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.MoshiProvider -import im.vector.matrix.android.internal.di.SessionDatabase -import im.vector.matrix.android.internal.task.Task -import im.vector.matrix.android.internal.util.awaitTransaction +import im.vector.matrix.android.internal.session.EventInsertLiveProcessor import io.realm.Realm import timber.log.Timber import javax.inject.Inject -internal interface PruneEventTask : Task { +/** + * Listens to the database for the insertion of any redaction event. + * As it will actually delete the content, it should be called last in the list of listener. + */ +internal class RedactionEventProcessor @Inject constructor() : EventInsertLiveProcessor { - data class Params( - val redactionEvents: List - ) -} + override fun shouldProcess(eventId: String, eventType: String): Boolean { + return eventType == EventType.REDACTION + } -internal class DefaultPruneEventTask @Inject constructor(@SessionDatabase private val monarchy: Monarchy) : PruneEventTask { - - override suspend fun execute(params: PruneEventTask.Params) { - monarchy.awaitTransaction { realm -> - params.redactionEvents.forEach { event -> - pruneEvent(realm, event) - } - } + override suspend fun process(realm: Realm, event: Event) { + pruneEvent(realm, event) } private fun pruneEvent(realm: Realm, redactionEvent: Event) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 2b1f50d000..c93d1e7e4b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -174,24 +174,13 @@ internal class DefaultTimeline( backgroundRealm.set(realm) roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() - roomEntity?.sendingTimelineEvents?.addChangeListener { events -> - // Remove in memory as soon as they are known by database - events.forEach { te -> - inMemorySendingEvents.removeAll { te.eventId == it.eventId } - } - postSnapshot() - } - nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() filteredEvents = nonFilteredEvents.where() .filterEventsWithSettings() .findAll() handleInitialLoad() - nonFilteredEvents.addChangeListener(eventsChangeListener) - eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId) .findAllAsync() - .also { it.addChangeListener(relationsListener) } if (settings.shouldHandleHiddenReadReceipts()) { hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt index ddfa7e91fe..ae10ccf80d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -125,7 +125,7 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu .isNotEmpty(ReadReceiptsSummaryEntityFields.READ_RECEIPTS.`$`) .filterReceiptsWithSettings() .findAllAsync() - .also { it.addChangeListener(hiddenReadReceiptsListener) } + //.also { it.addChangeListener(hiddenReadReceiptsListener) } } /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt deleted file mode 100644 index 7ca8aaa1d6..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2019 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.session.room.tombstone - -import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.VersioningState -import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent -import im.vector.matrix.android.internal.database.RealmLiveEntityObserver -import im.vector.matrix.android.internal.database.awaitTransaction -import im.vector.matrix.android.internal.database.mapper.asDomain -import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.RoomSummaryEntity -import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.database.query.whereTypes -import im.vector.matrix.android.internal.di.SessionDatabase -import io.realm.OrderedCollectionChangeSet -import io.realm.RealmConfiguration -import io.realm.RealmResults -import kotlinx.coroutines.launch -import javax.inject.Inject - -internal class RoomTombstoneEventLiveObserver @Inject constructor(@SessionDatabase - realmConfiguration: RealmConfiguration) - : RealmLiveEntityObserver(realmConfiguration) { - - override val query = Monarchy.Query { - EventEntity.whereTypes(it, listOf(EventType.STATE_ROOM_TOMBSTONE)) - } - - override fun onChange(results: RealmResults, changeSet: OrderedCollectionChangeSet) { - changeSet.insertions - .asSequence() - .mapNotNull { - results[it]?.asDomain() - } - .toList() - .also { - observerScope.launch { - handleRoomTombstoneEvents(it) - } - } - } - - private suspend fun handleRoomTombstoneEvents(tombstoneEvents: List) = awaitTransaction(realmConfiguration) { realm -> - for (event in tombstoneEvents) { - if (event.roomId == null) continue - val createRoomContent = event.getClearContent().toModel() - if (createRoomContent?.replacementRoomId == null) continue - - val predecessorRoomSummary = RoomSummaryEntity.where(realm, event.roomId).findFirst() - ?: RoomSummaryEntity(event.roomId) - if (predecessorRoomSummary.versioningState == VersioningState.NONE) { - predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_NOT_JOINED - } - realm.insertOrUpdate(predecessorRoomSummary) - } - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt new file mode 100644 index 0000000000..68ef2981d8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 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.session.room.tombstone + +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.VersioningState +import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.session.EventInsertLiveProcessor +import io.realm.Realm +import javax.inject.Inject + +internal class RoomTombstoneEventProcessor @Inject constructor() : EventInsertLiveProcessor { + + override suspend fun process(realm: Realm, event: Event) { + if (event.roomId == null) return + val createRoomContent = event.getClearContent().toModel() + if (createRoomContent?.replacementRoomId == null) return + + val predecessorRoomSummary = RoomSummaryEntity.where(realm, event.roomId).findFirst() + ?: RoomSummaryEntity(event.roomId) + if (predecessorRoomSummary.versioningState == VersioningState.NONE) { + predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_NOT_JOINED + } + realm.insertOrUpdate(predecessorRoomSummary) + } + + override fun shouldProcess(eventId: String, eventType: String): Boolean { + return eventType == EventType.STATE_ROOM_TOMBSTONE + } +} From 283f32479db0a18f7f746a44c7461a7389108045 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 3 Jul 2020 21:11:54 +0200 Subject: [PATCH 003/122] Rebranch timeline + continue clean up strategy --- .../internal/database/DatabaseCleaner.kt | 98 +++++++++++++++++++ .../internal/session/DefaultSession.kt | 43 -------- .../android/internal/session/SessionModule.kt | 5 + .../internal/session/room/RoomModule.kt | 6 +- .../session/room/timeline/DefaultTimeline.kt | 89 ++++++++--------- .../room/timeline/DefaultTimelineService.kt | 4 +- ...teTask.kt => FetchTokenAndPaginateTask.kt} | 30 ++++-- .../timeline/TimelineHiddenReadReceipts.kt | 2 +- 8 files changed, 169 insertions(+), 108 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/DatabaseCleaner.kt rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/{FetchNextTokenAndPaginateTask.kt => FetchTokenAndPaginateTask.kt} (67%) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/DatabaseCleaner.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/DatabaseCleaner.kt new file mode 100644 index 0000000000..a13cd6e078 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/DatabaseCleaner.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2020 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.database + +import im.vector.matrix.android.internal.database.helper.nextDisplayIndex +import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.ChunkEntityFields +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields +import im.vector.matrix.android.internal.di.SessionDatabase +import im.vector.matrix.android.internal.session.SessionLifecycleObserver +import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection +import im.vector.matrix.android.internal.task.TaskExecutor +import io.realm.Realm +import io.realm.RealmConfiguration +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +private const val MAX_NUMBER_OF_EVENTS = 35_000L +private const val MIN_NUMBER_OF_EVENTS_BY_CHUNK = 300 + +/** + * This class makes sure to stay under a maximum number of events as it makes Realm to be unusable when listening to events + * when the database is getting too big. + */ +internal class DatabaseCleaner @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration, + private val taskExecutor: TaskExecutor) : SessionLifecycleObserver { + + override fun onStart() { + taskExecutor.executorScope.launch(Dispatchers.Default) { + awaitTransaction(realmConfiguration) { realm -> + val allRooms = realm.where(RoomEntity::class.java).findAll() + Timber.v("There are ${allRooms.size} rooms in this session") + cleanUp(realm, MAX_NUMBER_OF_EVENTS / 2L) + } + } + } + + private suspend fun cleanUp(realm: Realm, threshold: Long) { + val numberOfEvents = realm.where(EventEntity::class.java).findAll().size + val numberOfTimelineEvents = realm.where(TimelineEventEntity::class.java).findAll().size + Timber.v("Number of events in db: $numberOfEvents | Number of timeline events in db: $numberOfTimelineEvents") + if (threshold <= MIN_NUMBER_OF_EVENTS_BY_CHUNK || numberOfTimelineEvents < MAX_NUMBER_OF_EVENTS) { + Timber.v("Db is low enough") + } else { + val thresholdChunks = realm.where(ChunkEntity::class.java) + .greaterThan(ChunkEntityFields.NUMBER_OF_TIMELINE_EVENTS, threshold) + .findAll() + + Timber.v("There are ${thresholdChunks.size} chunks to clean with more than $threshold events") + for (chunk in thresholdChunks) { + val maxDisplayIndex = chunk.nextDisplayIndex(PaginationDirection.FORWARDS) + val thresholdDisplayIndex = maxDisplayIndex - threshold + val eventsToRemove = chunk.timelineEvents.where().lessThan(TimelineEventEntityFields.DISPLAY_INDEX, thresholdDisplayIndex).findAll() + Timber.v("There are ${eventsToRemove.size} events to clean in chunk: ${chunk.identifier()} from room ${chunk.room?.first()?.roomId}") + chunk.numberOfTimelineEvents = chunk.numberOfTimelineEvents - eventsToRemove.size + eventsToRemove.forEach { + val canDeleteRoot = it.root?.stateKey == null + if (canDeleteRoot) { + it.root?.deleteFromRealm() + } + it.readReceipts?.readReceipts?.deleteAllFromRealm() + it.readReceipts?.deleteFromRealm() + it.annotations?.apply { + editSummary?.deleteFromRealm() + pollResponseSummary?.deleteFromRealm() + referencesSummaryEntity?.deleteFromRealm() + reactionsSummary.deleteAllFromRealm() + } + it.annotations?.deleteFromRealm() + it.readReceipts?.deleteFromRealm() + it.deleteFromRealm() + } + // We reset the prevToken so we will need to fetch again. + chunk.prevToken = null + } + cleanUp(realm, (threshold / 1.5).toLong()) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index 466ad6fc7d..e7ab2a7926 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -164,49 +164,6 @@ internal class DefaultSession @Inject constructor( } eventBus.register(this) timelineEventDecryptor.start() - - taskExecutor.executorScope.launch(Dispatchers.Default) { - awaitTransaction(realmConfiguration) { realm -> - val allRooms = realm.where(RoomEntity::class.java).findAll() - val numberOfEvents = realm.where(EventEntity::class.java).findAll().size - val numberOfTimelineEvents = realm.where(TimelineEventEntity::class.java).findAll().size - Timber.v("Number of events in db: $numberOfEvents | Number of timeline events in db: $numberOfTimelineEvents") - Timber.v("Number of rooms in db: ${allRooms.size}") - if (numberOfTimelineEvents < 30_000L) { - Timber.v("Db is low enough") - } else { - val hugeChunks = realm.where(ChunkEntity::class.java).greaterThan(ChunkEntityFields.NUMBER_OF_TIMELINE_EVENTS, 250).findAll() - Timber.v("There are ${hugeChunks.size} chunks to clean") - /* - for (chunk in hugeChunks) { - val maxDisplayIndex = chunk.nextDisplayIndex(PaginationDirection.FORWARDS) - val thresholdDisplayIndex = maxDisplayIndex - 250 - val eventsToRemove = chunk.timelineEvents.where().lessThan(TimelineEventEntityFields.DISPLAY_INDEX, thresholdDisplayIndex).findAll() - Timber.v("There are ${eventsToRemove.size} events to clean in chunk: ${chunk.identifier()} from room ${chunk.room?.first()?.roomId}") - chunk.numberOfTimelineEvents = chunk.numberOfTimelineEvents - eventsToRemove.size - eventsToRemove.forEach { - val canDeleteRoot = it.root?.stateKey == null - if (canDeleteRoot) { - it.root?.deleteFromRealm() - } - it.readReceipts?.readReceipts?.deleteAllFromRealm() - it.readReceipts?.deleteFromRealm() - it.annotations?.apply { - editSummary?.deleteFromRealm() - pollResponseSummary?.deleteFromRealm() - referencesSummaryEntity?.deleteFromRealm() - reactionsSummary.deleteAllFromRealm() - } - it.annotations?.deleteFromRealm() - it.readReceipts?.deleteFromRealm() - it.deleteFromRealm() - } - } - - */ - } - } - } } override fun requireBackgroundSync() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index b8aaddd086..8aabd709ab 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -40,6 +40,7 @@ import im.vector.matrix.android.api.session.typing.TypingUsersTracker import im.vector.matrix.android.internal.crypto.crosssigning.ShieldTrustUpdater import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecretStorageService import im.vector.matrix.android.internal.crypto.verification.VerificationMessageProcessor +import im.vector.matrix.android.internal.database.DatabaseCleaner import im.vector.matrix.android.internal.database.EventInsertLiveObserver import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory import im.vector.matrix.android.internal.di.Authenticated @@ -340,6 +341,10 @@ internal abstract class SessionModule { @IntoSet abstract fun bindIdentityService(observer: DefaultIdentityService): SessionLifecycleObserver + @Binds + @IntoSet + abstract fun bindDatabaseCleaner(observer: DatabaseCleaner): SessionLifecycleObserver + @Binds abstract fun bindInitialSyncProgressService(service: DefaultInitialSyncProgressService): InitialSyncProgressService diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index ca59f9b7bb..7fa9c1526a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -62,10 +62,10 @@ import im.vector.matrix.android.internal.session.room.tags.AddTagToRoomTask import im.vector.matrix.android.internal.session.room.tags.DefaultAddTagToRoomTask import im.vector.matrix.android.internal.session.room.tags.DefaultDeleteTagFromRoomTask import im.vector.matrix.android.internal.session.room.tags.DeleteTagFromRoomTask -import im.vector.matrix.android.internal.session.room.timeline.DefaultFetchNextTokenAndPaginateTask +import im.vector.matrix.android.internal.session.room.timeline.DefaultFetchTokenAndPaginateTask import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask -import im.vector.matrix.android.internal.session.room.timeline.FetchNextTokenAndPaginateTask +import im.vector.matrix.android.internal.session.room.timeline.FetchTokenAndPaginateTask import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask import im.vector.matrix.android.internal.session.room.timeline.PaginationTask import im.vector.matrix.android.internal.session.room.typing.DefaultSendTypingTask @@ -176,7 +176,7 @@ internal abstract class RoomModule { abstract fun bindPaginationTask(task: DefaultPaginationTask): PaginationTask @Binds - abstract fun bindFetchNextTokenAndPaginateTask(task: DefaultFetchNextTokenAndPaginateTask): FetchNextTokenAndPaginateTask + abstract fun bindFetchNextTokenAndPaginateTask(task: DefaultFetchTokenAndPaginateTask): FetchTokenAndPaginateTask @Binds abstract fun bindFetchEditHistoryTask(task: DefaultFetchEditHistoryTask): FetchEditHistoryTask diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index c93d1e7e4b..1822587972 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -29,17 +29,14 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper -import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntityFields -import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields import im.vector.matrix.android.internal.database.query.TimelineEventFilter import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.database.query.whereInRoom import im.vector.matrix.android.internal.database.query.whereRoomId import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith @@ -72,7 +69,7 @@ internal class DefaultTimeline( private val realmConfiguration: RealmConfiguration, private val taskExecutor: TaskExecutor, private val contextOfEventTask: GetContextOfEventTask, - private val fetchNextTokenAndPaginateTask: FetchNextTokenAndPaginateTask, + private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, private val paginationTask: PaginationTask, private val timelineEventMapper: TimelineEventMapper, private val settings: TimelineSettings, @@ -98,9 +95,7 @@ internal class DefaultTimeline( private lateinit var nonFilteredEvents: RealmResults private lateinit var filteredEvents: RealmResults - private lateinit var eventRelations: RealmResults - - private var roomEntity: RoomEntity? = null + private lateinit var sendingEvents: RealmResults private var prevDisplayIndex: Int? = null private var nextDisplayIndex: Int? = null @@ -119,23 +114,10 @@ internal class DefaultTimeline( if (!results.isLoaded || !results.isValid) { return@OrderedRealmCollectionChangeListener } + results.createSnapshot() handleUpdates(results, changeSet) } - private val relationsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> - var hasChange = false - - (changeSet.insertions + changeSet.changes).forEach { - val eventRelations = collection[it] - if (eventRelations != null) { - hasChange = rebuildEvent(eventRelations.eventId) { te -> - te.copy(annotations = eventRelations.asDomain()) - } || hasChange - } - } - if (hasChange) postSnapshot() - } - // Public methods ****************************************************************************** override fun paginate(direction: Timeline.Direction, count: Int) { @@ -173,15 +155,23 @@ internal class DefaultTimeline( val realm = Realm.getInstance(realmConfiguration) backgroundRealm.set(realm) - roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() + val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() + ?: throw IllegalStateException("Can't open a timeline without a room") + + sendingEvents = roomEntity.sendingTimelineEvents.where().filterEventsWithSettings().findAll() + sendingEvents.addChangeListener { events -> + // Remove in memory as soon as they are known by database + events.forEach { te -> + inMemorySendingEvents.removeAll { te.eventId == it.eventId } + } + postSnapshot() + } nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() filteredEvents = nonFilteredEvents.where() .filterEventsWithSettings() .findAll() + filteredEvents.addChangeListener(eventsChangeListener) handleInitialLoad() - eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId) - .findAllAsync() - if (settings.shouldHandleHiddenReadReceipts()) { hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this) } @@ -202,9 +192,8 @@ internal class DefaultTimeline( cancelableBag.cancel() BACKGROUND_HANDLER.removeCallbacksAndMessages(null) BACKGROUND_HANDLER.post { - roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() - if (this::eventRelations.isInitialized) { - eventRelations.removeAllChangeListeners() + if (this::sendingEvents.isInitialized) { + sendingEvents.removeAllChangeListeners() } if (this::nonFilteredEvents.isInitialized) { nonFilteredEvents.removeAllChangeListeners() @@ -303,7 +292,7 @@ internal class DefaultTimeline( listeners.clear() } - // TimelineHiddenReadReceipts.Delegate +// TimelineHiddenReadReceipts.Delegate override fun rebuildEvent(eventId: String, readReceipts: List): Boolean { return rebuildEvent(eventId) { te -> @@ -336,7 +325,7 @@ internal class DefaultTimeline( } } - // Private methods ***************************************************************************** +// Private methods ***************************************************************************** private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean { return builtEventsIdMap[eventId]?.let { builtIndex -> @@ -400,20 +389,16 @@ internal class DefaultTimeline( } private fun buildSendingEvents(): List { - val sendingEvents = ArrayList() + val builtSendingEvents = ArrayList() if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) { - sendingEvents.addAll(inMemorySendingEvents.filterEventsWithSettings()) - roomEntity?.sendingTimelineEvents - ?.where() - ?.filterEventsWithSettings() - ?.findAll() - ?.forEach { timelineEventEntity -> - if (sendingEvents.find { it.eventId == timelineEventEntity.eventId } == null) { - sendingEvents.add(timelineEventMapper.map(timelineEventEntity)) - } - } + builtSendingEvents.addAll(inMemorySendingEvents.filterEventsWithSettings()) + sendingEvents.forEach { timelineEventEntity -> + if (builtSendingEvents.find { it.eventId == timelineEventEntity.eventId } == null) { + builtSendingEvents.add(timelineEventMapper.map(timelineEventEntity)) + } + } } - return sendingEvents + return builtSendingEvents } private fun canPaginate(direction: Timeline.Direction): Boolean { @@ -514,19 +499,25 @@ internal class DefaultTimeline( val currentChunk = getLiveChunk() val token = if (direction == Timeline.Direction.BACKWARDS) currentChunk?.prevToken else currentChunk?.nextToken if (token == null) { - if (direction == Timeline.Direction.FORWARDS && currentChunk?.hasBeenALastForwardChunk().orFalse()) { - // We are in the case that next event exists, but we do not know the next token. - // Fetch (again) the last event to get a nextToken - val lastKnownEventId = nonFilteredEvents.firstOrNull()?.eventId + if (direction == Timeline.Direction.BACKWARDS || + (direction == Timeline.Direction.FORWARDS && currentChunk?.hasBeenALastForwardChunk().orFalse())) { + // We are in the case where event exists, but we do not know the token. + // Fetch (again) the last event to get a token + val lastKnownEventId = if (direction == Timeline.Direction.FORWARDS) { + nonFilteredEvents.firstOrNull()?.eventId + } else { + nonFilteredEvents.lastOrNull()?.eventId + } if (lastKnownEventId == null) { updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } } else { - val params = FetchNextTokenAndPaginateTask.Params( + val params = FetchTokenAndPaginateTask.Params( roomId = roomId, limit = limit, + direction = direction.toPaginationDirection(), lastKnownEventId = lastKnownEventId ) - cancelableBag += fetchNextTokenAndPaginateTask + cancelableBag += fetchTokenAndPaginateTask .configureWith(params) { this.callback = createPaginationCallback(limit, direction) } @@ -755,7 +746,7 @@ internal class DefaultTimeline( } } -// Extension methods *************************************************************************** + // Extension methods *************************************************************************** private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index 449061c2f7..5723568197 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -43,7 +43,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv private val contextOfEventTask: GetContextOfEventTask, private val eventDecryptor: TimelineEventDecryptor, private val paginationTask: PaginationTask, - private val fetchNextTokenAndPaginateTask: FetchNextTokenAndPaginateTask, + private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, private val timelineEventMapper: TimelineEventMapper, private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper ) : TimelineService { @@ -66,7 +66,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings), eventBus = eventBus, eventDecryptor = eventDecryptor, - fetchNextTokenAndPaginateTask = fetchNextTokenAndPaginateTask + fetchTokenAndPaginateTask = fetchTokenAndPaginateTask ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/FetchNextTokenAndPaginateTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/FetchTokenAndPaginateTask.kt similarity index 67% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/FetchNextTokenAndPaginateTask.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/FetchTokenAndPaginateTask.kt index 6579e0031a..3c1f6b18bd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/FetchNextTokenAndPaginateTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/FetchTokenAndPaginateTask.kt @@ -28,38 +28,48 @@ import im.vector.matrix.android.internal.util.awaitTransaction import org.greenrobot.eventbus.EventBus import javax.inject.Inject -internal interface FetchNextTokenAndPaginateTask : Task { +internal interface FetchTokenAndPaginateTask : Task { data class Params( val roomId: String, val lastKnownEventId: String, + val direction: PaginationDirection, val limit: Int ) } -internal class DefaultFetchNextTokenAndPaginateTask @Inject constructor( +internal class DefaultFetchTokenAndPaginateTask @Inject constructor( private val roomAPI: RoomAPI, @SessionDatabase private val monarchy: Monarchy, private val filterRepository: FilterRepository, private val paginationTask: PaginationTask, private val eventBus: EventBus -) : FetchNextTokenAndPaginateTask { +) : FetchTokenAndPaginateTask { - override suspend fun execute(params: FetchNextTokenAndPaginateTask.Params): TokenChunkEventPersistor.Result { + override suspend fun execute(params: FetchTokenAndPaginateTask.Params): TokenChunkEventPersistor.Result { val filter = filterRepository.getRoomFilter() val response = executeRequest(eventBus) { apiCall = roomAPI.getContextOfEvent(params.roomId, params.lastKnownEventId, 0, filter) } - if (response.end == null) { - throw IllegalStateException("No next token found") + val fromToken = if (params.direction == PaginationDirection.FORWARDS) { + response.end + } else { + response.start } - monarchy.awaitTransaction { - ChunkEntity.findIncludingEvent(it, params.lastKnownEventId)?.nextToken = response.end + ?: throw IllegalStateException("No token found") + + monarchy.awaitTransaction { realm -> + val chunkToUpdate = ChunkEntity.findIncludingEvent(realm, params.lastKnownEventId) + if (params.direction == PaginationDirection.FORWARDS) { + chunkToUpdate?.nextToken = fromToken + } else { + chunkToUpdate?.prevToken = fromToken + } } val paginationParams = PaginationTask.Params( roomId = params.roomId, - from = response.end, - direction = PaginationDirection.FORWARDS, + from = fromToken, + direction = params.direction, limit = params.limit ) return paginationTask.execute(paginationParams) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt index ae10ccf80d..ddfa7e91fe 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -125,7 +125,7 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu .isNotEmpty(ReadReceiptsSummaryEntityFields.READ_RECEIPTS.`$`) .filterReceiptsWithSettings() .findAllAsync() - //.also { it.addChangeListener(hiddenReadReceiptsListener) } + .also { it.addChangeListener(hiddenReadReceiptsListener) } } /** From 7434aed43fe0c048fd31423f28276a109fde6ee3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 3 Jul 2020 21:12:27 +0200 Subject: [PATCH 004/122] Use writeAsync for localEcho --- .../internal/session/room/send/LocalEchoEventFactory.kt | 5 +---- .../internal/session/room/send/LocalEchoRepository.kt | 6 +++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 08dc4e80d8..cdb5f7dc7a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -62,7 +62,6 @@ import im.vector.matrix.android.internal.session.content.ThumbnailExtractor import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.StringProvider -import kotlinx.coroutines.launch import javax.inject.Inject /** @@ -474,9 +473,7 @@ internal class LocalEchoEventFactory @Inject constructor( fun createLocalEcho(event: Event) { checkNotNull(event.roomId) { "Your event should have a roomId" } - taskExecutor.executorScope.launch { - localEchoRepository.createLocalEcho(event) - } + localEchoRepository.createLocalEcho(event) } companion object { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt index fb47c16943..2d2c64ef7f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt @@ -48,7 +48,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private private val eventBus: EventBus, private val timelineEventMapper: TimelineEventMapper) { - suspend fun createLocalEcho(event: Event) { + fun createLocalEcho(event: Event) { val roomId = event.roomId ?: throw IllegalStateException("You should have set a roomId for your event") val senderId = event.senderId ?: throw IllegalStateException("You should have set a senderIf for your event") if (event.eventId == null) { @@ -70,8 +70,8 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private } val timelineEvent = timelineEventMapper.map(timelineEventEntity) eventBus.post(DefaultTimeline.OnLocalEchoCreated(roomId = roomId, timelineEvent = timelineEvent)) - monarchy.awaitTransaction { realm -> - val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@awaitTransaction + monarchy.writeAsync { realm -> + val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@writeAsync roomEntity.sendingTimelineEvents.add(0, timelineEventEntity) roomSummaryUpdater.update(realm, roomId) } From 04f0146afd193d34e43c8d0061e3ba5ad2b2ddb7 Mon Sep 17 00:00:00 2001 From: Tobias Preuss Date: Wed, 24 Jun 2020 12:16:30 +0200 Subject: [PATCH 005/122] Use Context#withStyledAttributes extension function. + This function is more concise and ensures "recycle()" is always invoked. + Sources: https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-preference-release/core/core-ktx/src/main/java/androidx/core/content/Context.kt#52 --- CHANGES.md | 2 +- .../behavior/PercentViewBehavior.kt | 29 +++++---- .../core/platform/EllipsizingTextView.kt | 8 ++- .../core/platform/MaxHeightScrollView.kt | 9 +-- .../core/ui/views/BottomSheetActionButton.kt | 18 ++--- .../timeline/item/PollResultLineView.kt | 13 ++-- .../reactions/widget/ReactionButton.kt | 65 ++++++++++--------- 7 files changed, 75 insertions(+), 69 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 20b3b34375..22220d3b0b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,7 +26,7 @@ Build 🧱: - Upgrade gradle from 5.4.1 to 5.6.4 Other changes: - - + - Use `Context#withStyledAttributes` extension function (#1546) Changes in Riot.imX 0.91.3 (2020-07-01) =================================================== diff --git a/vector/src/main/java/im/vector/riotx/core/animations/behavior/PercentViewBehavior.kt b/vector/src/main/java/im/vector/riotx/core/animations/behavior/PercentViewBehavior.kt index 967d7d638d..37c07b8293 100644 --- a/vector/src/main/java/im/vector/riotx/core/animations/behavior/PercentViewBehavior.kt +++ b/vector/src/main/java/im/vector/riotx/core/animations/behavior/PercentViewBehavior.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright 2020 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. @@ -22,6 +22,7 @@ import android.graphics.drawable.ColorDrawable import android.util.AttributeSet import android.view.View import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.content.withStyledAttributes import im.vector.riotx.R import kotlin.math.abs @@ -67,19 +68,19 @@ class PercentViewBehavior(context: Context, attrs: AttributeSet) : Coo private var isPrepared: Boolean = false init { - val a = context.obtainStyledAttributes(attrs, R.styleable.PercentViewBehavior) - dependViewId = a.getResourceId(R.styleable.PercentViewBehavior_behavior_dependsOn, 0) - dependType = a.getInt(R.styleable.PercentViewBehavior_behavior_dependType, DEPEND_TYPE_WIDTH) - dependTarget = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_dependTarget, UNSPECIFIED_INT) - targetX = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetX, UNSPECIFIED_INT) - targetY = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetY, UNSPECIFIED_INT) - targetWidth = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetWidth, UNSPECIFIED_INT) - targetHeight = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetHeight, UNSPECIFIED_INT) - targetBackgroundColor = a.getColor(R.styleable.PercentViewBehavior_behavior_targetBackgroundColor, UNSPECIFIED_INT) - targetAlpha = a.getFloat(R.styleable.PercentViewBehavior_behavior_targetAlpha, UNSPECIFIED_FLOAT) - targetRotateX = a.getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateX, UNSPECIFIED_FLOAT) - targetRotateY = a.getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateY, UNSPECIFIED_FLOAT) - a.recycle() + context.withStyledAttributes(attrs, R.styleable.PercentViewBehavior) { + dependViewId = getResourceId(R.styleable.PercentViewBehavior_behavior_dependsOn, 0) + dependType = getInt(R.styleable.PercentViewBehavior_behavior_dependType, DEPEND_TYPE_WIDTH) + dependTarget = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_dependTarget, UNSPECIFIED_INT) + targetX = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetX, UNSPECIFIED_INT) + targetY = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetY, UNSPECIFIED_INT) + targetWidth = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetWidth, UNSPECIFIED_INT) + targetHeight = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetHeight, UNSPECIFIED_INT) + targetBackgroundColor = getColor(R.styleable.PercentViewBehavior_behavior_targetBackgroundColor, UNSPECIFIED_INT) + targetAlpha = getFloat(R.styleable.PercentViewBehavior_behavior_targetAlpha, UNSPECIFIED_FLOAT) + targetRotateX = getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateX, UNSPECIFIED_FLOAT) + targetRotateY = getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateY, UNSPECIFIED_FLOAT) + } } private fun prepare(parent: CoordinatorLayout, child: View, dependency: View) { diff --git a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt index f451308c36..f54776fc40 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt @@ -38,6 +38,7 @@ import android.text.TextUtils.substring import android.text.style.ForegroundColorSpan import android.util.AttributeSet import androidx.appcompat.widget.AppCompatTextView +import androidx.core.content.withStyledAttributes import timber.log.Timber import java.util.ArrayList import java.util.regex.Pattern @@ -71,6 +72,7 @@ class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: Att private var maxLines = 0 private var lineSpacingMult = 1.0f private var lineAddVertPad = 0.0f + /** * The end punctuation which will be removed when appending [.ELLIPSIS]. */ @@ -408,9 +410,9 @@ class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: Att } init { - val a = context.obtainStyledAttributes(attrs, intArrayOf(android.R.attr.maxLines, android.R.attr.ellipsize), defStyle, 0) - maxLines = a.getInt(0, Int.MAX_VALUE) - a.recycle() + context.withStyledAttributes(attrs, intArrayOf(android.R.attr.maxLines, android.R.attr.ellipsize), defStyle) { + maxLines = getInt(0, Int.MAX_VALUE) + } setEndPunctuationPattern(DEFAULT_END_PUNCTUATION) val currentTextColor = currentTextColor val ellipsizeColor = Color.argb(ELLIPSIZE_ALPHA, Color.red(currentTextColor), Color.green(currentTextColor), Color.blue(currentTextColor)) diff --git a/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt b/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt index b8587750a3..99c158252f 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright 2020 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. @@ -18,6 +18,7 @@ package im.vector.riotx.core.platform import android.content.Context import android.util.AttributeSet +import androidx.core.content.withStyledAttributes import androidx.core.widget.NestedScrollView import im.vector.riotx.R @@ -34,9 +35,9 @@ class MaxHeightScrollView @JvmOverloads constructor(context: Context, attrs: Att init { if (attrs != null) { - val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView) - maxHeight = styledAttrs.getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT) - styledAttrs.recycle() + context.withStyledAttributes(attrs, R.styleable.MaxHeightScrollView) { + maxHeight = getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT) + } } } diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt index d29982c9e4..455e856833 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt @@ -25,6 +25,7 @@ import android.view.View import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView +import androidx.core.content.withStyledAttributes import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible @@ -117,16 +118,15 @@ class BottomSheetActionButton @JvmOverloads constructor( inflate(context, R.layout.item_verification_action, this) ButterKnife.bind(this) - val typedArray = context.obtainStyledAttributes(attrs, R.styleable.BottomSheetActionButton, 0, 0) - title = typedArray.getString(R.styleable.BottomSheetActionButton_actionTitle) ?: "" - subTitle = typedArray.getString(R.styleable.BottomSheetActionButton_actionDescription) ?: "" - forceStartPadding = typedArray.getBoolean(R.styleable.BottomSheetActionButton_forceStartPadding, false) - leftIcon = typedArray.getDrawable(R.styleable.BottomSheetActionButton_leftIcon) + context.withStyledAttributes(attrs, R.styleable.BottomSheetActionButton) { + title = getString(R.styleable.BottomSheetActionButton_actionTitle) ?: "" + subTitle = getString(R.styleable.BottomSheetActionButton_actionDescription) ?: "" + forceStartPadding = getBoolean(R.styleable.BottomSheetActionButton_forceStartPadding, false) + leftIcon = getDrawable(R.styleable.BottomSheetActionButton_leftIcon) - rightIcon = typedArray.getDrawable(R.styleable.BottomSheetActionButton_rightIcon) + rightIcon = getDrawable(R.styleable.BottomSheetActionButton_rightIcon) - tint = typedArray.getColor(R.styleable.BottomSheetActionButton_tint, ThemeUtils.getColor(context, android.R.attr.textColor)) - - typedArray.recycle() + tint = getColor(R.styleable.BottomSheetActionButton_tint, ThemeUtils.getColor(context, android.R.attr.textColor)) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/PollResultLineView.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/PollResultLineView.kt index c52b863658..bee3ca6c5b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/PollResultLineView.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/PollResultLineView.kt @@ -22,6 +22,7 @@ import android.view.View import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView +import androidx.core.content.withStyledAttributes import butterknife.BindView import butterknife.ButterKnife import im.vector.riotx.R @@ -73,11 +74,11 @@ class PollResultLineView @JvmOverloads constructor( orientation = HORIZONTAL ButterKnife.bind(this) - val typedArray = context.obtainStyledAttributes(attrs, R.styleable.PollResultLineView, 0, 0) - label = typedArray.getString(R.styleable.PollResultLineView_optionName) ?: "" - percent = typedArray.getString(R.styleable.PollResultLineView_optionCount) ?: "" - optionSelected = typedArray.getBoolean(R.styleable.PollResultLineView_optionSelected, false) - isWinner = typedArray.getBoolean(R.styleable.PollResultLineView_optionIsWinner, false) - typedArray.recycle() + context.withStyledAttributes(attrs, R.styleable.PollResultLineView) { + label = getString(R.styleable.PollResultLineView_optionName) ?: "" + percent = getString(R.styleable.PollResultLineView_optionCount) ?: "" + optionSelected = getBoolean(R.styleable.PollResultLineView_optionSelected, false) + isWinner = getBoolean(R.styleable.PollResultLineView_optionIsWinner, false) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt b/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt index 811d399b23..ec5aba8ee5 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright 2020 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. @@ -34,6 +34,7 @@ import android.widget.TextView import androidx.annotation.ColorInt import androidx.annotation.ColorRes import androidx.core.content.ContextCompat +import androidx.core.content.withStyledAttributes import im.vector.riotx.EmojiCompatWrapper import im.vector.riotx.R import im.vector.riotx.core.di.HasScreenInjector @@ -110,41 +111,41 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut // emojiView?.typeface = this.emojiTypeFace ?: Typeface.DEFAULT - val array = context.obtainStyledAttributes(attrs, R.styleable.ReactionButton, defStyleAttr, 0) + context.withStyledAttributes(attrs, R.styleable.ReactionButton, defStyleAttr) { - onDrawable = ContextCompat.getDrawable(context, R.drawable.rounded_rect_shape) - offDrawable = ContextCompat.getDrawable(context, R.drawable.rounded_rect_shape_off) + onDrawable = ContextCompat.getDrawable(context, R.drawable.rounded_rect_shape) + offDrawable = ContextCompat.getDrawable(context, R.drawable.rounded_rect_shape_off) - circleStartColor = array.getColor(R.styleable.ReactionButton_circle_start_color, 0) + circleStartColor = getColor(R.styleable.ReactionButton_circle_start_color, 0) - if (circleStartColor != 0) { - circleView.startColor = circleStartColor + if (circleStartColor != 0) { + circleView.startColor = circleStartColor + } + + circleEndColor = getColor(R.styleable.ReactionButton_circle_end_color, 0) + + if (circleEndColor != 0) { + circleView.endColor = circleEndColor + } + + dotPrimaryColor = getColor(R.styleable.ReactionButton_dots_primary_color, 0) + dotSecondaryColor = getColor(R.styleable.ReactionButton_dots_secondary_color, 0) + + if (dotPrimaryColor != 0 && dotSecondaryColor != 0) { + dotsView.setColors(dotPrimaryColor, dotSecondaryColor) + } + + getString(R.styleable.ReactionButton_emoji)?.let { + reactionString = it + } + + reactionCount = getInt(R.styleable.ReactionButton_reaction_count, 0) + + val status = getBoolean(R.styleable.ReactionButton_toggled, false) + setChecked(status) + setOnClickListener(this@ReactionButton) + setOnLongClickListener(this@ReactionButton) } - - circleEndColor = array.getColor(R.styleable.ReactionButton_circle_end_color, 0) - - if (circleEndColor != 0) { - circleView.endColor = circleEndColor - } - - dotPrimaryColor = array.getColor(R.styleable.ReactionButton_dots_primary_color, 0) - dotSecondaryColor = array.getColor(R.styleable.ReactionButton_dots_secondary_color, 0) - - if (dotPrimaryColor != 0 && dotSecondaryColor != 0) { - dotsView.setColors(dotPrimaryColor, dotSecondaryColor) - } - - array.getString(R.styleable.ReactionButton_emoji)?.let { - reactionString = it - } - - reactionCount = array.getInt(R.styleable.ReactionButton_reaction_count, 0) - - val status = array.getBoolean(R.styleable.ReactionButton_toggled, false) - setChecked(status) - setOnClickListener(this) - setOnLongClickListener(this) - array.recycle() } private fun getDrawableFromResource(array: TypedArray, styleableIndexId: Int): Drawable? { From 9d4e903c4a72bca631378ea4bc61b195df9007f8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 6 Jul 2020 15:59:49 +0200 Subject: [PATCH 006/122] Upgrade some dependencies --- CHANGES.md | 1 + build.gradle | 4 ++-- matrix-sdk-android-rx/build.gradle | 2 +- matrix-sdk-android/build.gradle | 4 +--- multipicker/build.gradle | 2 +- vector/build.gradle | 22 +++++++++++----------- 6 files changed, 17 insertions(+), 18 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 20b3b34375..6c6ef72404 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -24,6 +24,7 @@ Build 🧱: - Fix lint false-positive about WorkManger (#1012) - Upgrade build-tools from 3.5.3 to 3.6.6 - Upgrade gradle from 5.4.1 to 5.6.4 + - Upgrade some dependencies Other changes: - diff --git a/build.gradle b/build.gradle index 5f1fa78620..edd6edcd93 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.3.72' repositories { google() jcenter() @@ -15,7 +15,7 @@ buildscript { classpath "com.airbnb.okreplay:gradle-plugin:1.5.0" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1' - classpath 'com.google.android.gms:oss-licenses-plugin:0.9.5' + classpath 'com.google.android.gms:oss-licenses-plugin:0.10.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/matrix-sdk-android-rx/build.gradle b/matrix-sdk-android-rx/build.gradle index c67d10d810..70a05114c2 100644 --- a/matrix-sdk-android-rx/build.gradle +++ b/matrix-sdk-android-rx/build.gradle @@ -39,7 +39,7 @@ dependencies { implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' // Paging - implementation "androidx.paging:paging-runtime-ktx:2.1.0" + implementation "androidx.paging:paging-runtime-ktx:2.1.2" // Logging implementation 'com.jakewharton.timber:timber:4.7.1' diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 422a5dac1d..71b763545c 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -51,7 +51,6 @@ android { } buildTypes { - debug { // Set to true to log privacy or sensible data, such as token buildConfigField "boolean", "LOG_PRIVATE_DATA", project.property("vector.debugPrivateData") @@ -123,7 +122,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "androidx.appcompat:appcompat:1.1.0" - implementation "androidx.core:core-ktx:1.1.0" + implementation "androidx.core:core-ktx:1.3.0" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" @@ -205,5 +204,4 @@ dependencies { androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' androidTestUtil 'androidx.test:orchestrator:1.2.0' - } diff --git a/multipicker/build.gradle b/multipicker/build.gradle index 8b08a9d3ef..8f2226e884 100644 --- a/multipicker/build.gradle +++ b/multipicker/build.gradle @@ -44,7 +44,7 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.core:core-ktx:1.2.0' + implementation 'androidx.core:core-ktx:1.3.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' diff --git a/vector/build.gradle b/vector/build.gradle index f966f441b2..2387a3cf4f 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -262,16 +262,16 @@ android { dependencies { def epoxy_version = '3.11.0' - def fragment_version = '1.2.0' + def fragment_version = '1.2.5' def arrow_version = "0.8.2" def coroutines_version = "1.3.2" def markwon_version = '4.1.2' def big_image_viewer_version = '1.6.2' - def glide_version = '4.10.0' + def glide_version = '4.11.0' def moshi_version = '1.8.0' def daggerVersion = '2.25.4' def autofill_version = "1.0.0" - def work_version = '2.3.3' + def work_version = '2.3.4' def arch_version = '2.1.0' def lifecycle_version = '2.2.0' @@ -285,12 +285,12 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" - implementation "androidx.recyclerview:recyclerview:1.2.0-alpha01" + implementation "androidx.recyclerview:recyclerview:1.2.0-alpha04" implementation 'androidx.appcompat:appcompat:1.1.0' implementation "androidx.fragment:fragment:$fragment_version" implementation "androidx.fragment:fragment-ktx:$fragment_version" - implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4' - implementation 'androidx.core:core-ktx:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta7' + implementation 'androidx.core:core-ktx:1.3.0' implementation "org.threeten:threetenbp:1.4.0:no-tzdb" implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.7.0" @@ -326,17 +326,17 @@ dependencies { implementation "androidx.work:work-runtime-ktx:$work_version" // Paging - implementation "androidx.paging:paging-runtime-ktx:2.1.1" + implementation "androidx.paging:paging-runtime-ktx:2.1.2" // Functional Programming implementation "io.arrow-kt:arrow-core:$arrow_version" // Pref - implementation 'androidx.preference:preference:1.1.0' + implementation 'androidx.preference:preference:1.1.1' // UI implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' - implementation 'com.google.android.material:material:1.2.0-alpha03' + implementation 'com.google.android.material:material:1.3.0-alpha01' implementation 'me.gujun.android:span:1.7' implementation "io.noties.markwon:core:$markwon_version" implementation "io.noties.markwon:html:$markwon_version" @@ -385,7 +385,7 @@ dependencies { // gplay flavor only // Warning: due to the exclude, Android Studio does not propose to upgrade. Uncomment next line to be proposed to upgrade // implementation 'com.google.firebase:firebase-messaging:20.0.0' - gplayImplementation('com.google.firebase:firebase-messaging:20.0.0') { + gplayImplementation('com.google.firebase:firebase-messaging:20.2.1') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' @@ -394,7 +394,7 @@ dependencies { // OSS License, gplay flavor only gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' - implementation "androidx.emoji:emoji-appcompat:1.0.0" + implementation "androidx.emoji:emoji-appcompat:1.1.0" implementation 'com.github.BillCarsonFr:JsonViewer:0.5' From 32d2cea7f8dca84afbc25068e1325c45d46d750c Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 6 Jul 2020 18:38:30 +0200 Subject: [PATCH 007/122] EventInsert: add InsertType to avoid trying to process events we shouldn't --- ...ver.kt => VerificationMessageProcessor.kt} | 6 +++- .../internal/database/DatabaseCleaner.kt | 10 ++++--- .../database/EventInsertLiveObserver.kt | 19 ++++++------ .../database/RealmLiveEntityObserver.kt | 3 +- .../database/model/EventInsertEntity.kt | 14 ++++++++- .../database/model/EventInsertType.java | 24 +++++++++++++++ .../database/query/EventEntityQueries.kt | 7 +++-- .../session/EventInsertLiveProcessor.kt | 3 +- .../session/call/CallEventProcessor.kt | 7 ++++- .../EventRelationsAggregationProcessor.kt | 3 +- .../room/create/RoomCreateEventProcessor.kt | 3 +- .../room/membership/LoadRoomMembersTask.kt | 4 ++- .../room/prune/RedactionEventProcessor.kt | 3 +- .../session/room/send/LocalEchoRepository.kt | 6 ++++ .../session/room/timeline/DefaultTimeline.kt | 4 ++- .../room/timeline/TokenChunkEventPersistor.kt | 5 ++-- .../tombstone/RoomTombstoneEventProcessor.kt | 3 +- .../internal/session/sync/RoomSyncHandler.kt | 29 +++++++++++++------ 18 files changed, 115 insertions(+), 38 deletions(-) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/{VerificationMessageLiveObserver.kt => VerificationMessageProcessor.kt} (97%) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventInsertType.java diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageProcessor.kt similarity index 97% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageProcessor.kt index f932dd7a69..c266d965cd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageProcessor.kt @@ -29,6 +29,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageVerificati import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult +import im.vector.matrix.android.internal.database.model.EventInsertType import im.vector.matrix.android.internal.di.DeviceId import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.EventInsertLiveProcessor @@ -58,7 +59,10 @@ internal class VerificationMessageProcessor @Inject constructor( EventType.ENCRYPTED ) - override fun shouldProcess(eventId: String, eventType: String): Boolean { + override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { + if (insertType != EventInsertType.INCREMENTAL_SYNC) { + return false + } return allowedTypes.contains(eventType) && !LocalEcho.isLocalEchoId(eventId) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/DatabaseCleaner.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/DatabaseCleaner.kt index a13cd6e078..ca2126e621 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/DatabaseCleaner.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/DatabaseCleaner.kt @@ -34,12 +34,14 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -private const val MAX_NUMBER_OF_EVENTS = 35_000L +private const val MAX_NUMBER_OF_EVENTS_IN_DB = 35_000L private const val MIN_NUMBER_OF_EVENTS_BY_CHUNK = 300 /** * This class makes sure to stay under a maximum number of events as it makes Realm to be unusable when listening to events - * when the database is getting too big. + * when the database is getting too big. This will try incrementally to remove the biggest chunks until we get below the threshold. + * We make sure to still have a minimum number of events so it's not becoming unusable. + * So this won't work for users with a big number of very active rooms. */ internal class DatabaseCleaner @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration, private val taskExecutor: TaskExecutor) : SessionLifecycleObserver { @@ -49,7 +51,7 @@ internal class DatabaseCleaner @Inject constructor(@SessionDatabase private val awaitTransaction(realmConfiguration) { realm -> val allRooms = realm.where(RoomEntity::class.java).findAll() Timber.v("There are ${allRooms.size} rooms in this session") - cleanUp(realm, MAX_NUMBER_OF_EVENTS / 2L) + cleanUp(realm, MAX_NUMBER_OF_EVENTS_IN_DB / 2L) } } } @@ -58,7 +60,7 @@ internal class DatabaseCleaner @Inject constructor(@SessionDatabase private val val numberOfEvents = realm.where(EventEntity::class.java).findAll().size val numberOfTimelineEvents = realm.where(TimelineEventEntity::class.java).findAll().size Timber.v("Number of events in db: $numberOfEvents | Number of timeline events in db: $numberOfTimelineEvents") - if (threshold <= MIN_NUMBER_OF_EVENTS_BY_CHUNK || numberOfTimelineEvents < MAX_NUMBER_OF_EVENTS) { + if (threshold <= MIN_NUMBER_OF_EVENTS_BY_CHUNK || numberOfTimelineEvents < MAX_NUMBER_OF_EVENTS_IN_DB) { Timber.v("Db is low enough") } else { val thresholdChunks = realm.where(ChunkEntity::class.java) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/EventInsertLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/EventInsertLiveObserver.kt index 443ca781bc..f0884918c0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/EventInsertLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/EventInsertLiveObserver.kt @@ -27,7 +27,6 @@ import im.vector.matrix.android.internal.database.model.EventInsertEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.session.EventInsertLiveProcessor -import io.realm.OrderedCollectionChangeSet import io.realm.RealmConfiguration import io.realm.RealmResults import kotlinx.coroutines.launch @@ -48,18 +47,18 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real return } Timber.v("EventInsertEntity updated with ${results.size} results in db") - val filteredEventIds = results.mapNotNull { - val shouldProcess = shouldProcess(it) - if (shouldProcess) { - it.eventId + val filteredEvents = results.mapNotNull { + if (shouldProcess(it)) { + results.realm.copyFromRealm(it) } else { null } } - Timber.v("There are ${filteredEventIds.size} events to process") + Timber.v("There are ${filteredEvents.size} events to process") observerScope.launch { awaitTransaction(realmConfiguration) { realm -> - filteredEventIds.forEach { eventId -> + filteredEvents.forEach { eventInsert -> + val eventId = eventInsert.eventId val event = EventEntity.where(realm, eventId).findFirst() if (event == null) { Timber.v("Event $eventId not found") @@ -67,7 +66,9 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real } val domainEvent = event.asDomain() decryptIfNeeded(domainEvent) - processors.forEach { + processors.filter { + it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType) + }.forEach { it.process(realm, domainEvent) } } @@ -95,7 +96,7 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean { return processors.any { - it.shouldProcess(eventInsertEntity.eventId, eventInsertEntity.eventType) + it.shouldProcess(eventInsertEntity.eventId, eventInsertEntity.eventType, eventInsertEntity.insertType) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt index af67bae526..3f0dc4cddd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt @@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.database import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.session.SessionLifecycleObserver import im.vector.matrix.android.internal.util.createBackgroundHandler -import io.realm.OrderedRealmCollectionChangeListener import io.realm.Realm import io.realm.RealmChangeListener import io.realm.RealmConfiguration @@ -31,7 +30,7 @@ import kotlinx.coroutines.cancelChildren import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference -internal interface LiveEntityObserver: SessionLifecycleObserver +internal interface LiveEntityObserver : SessionLifecycleObserver internal abstract class RealmLiveEntityObserver(protected val realmConfiguration: RealmConfiguration) : LiveEntityObserver, RealmChangeListener> { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventInsertEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventInsertEntity.kt index c2744fc721..bb5186263c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventInsertEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventInsertEntity.kt @@ -16,13 +16,25 @@ package im.vector.matrix.android.internal.database.model +import im.vector.matrix.android.api.session.room.model.Membership import io.realm.RealmObject import io.realm.annotations.Index +/** + * This class is used to get notification on new events being inserted. It's to avoid realm getting slow when listening to insert + * in EventEntity table. + */ internal open class EventInsertEntity(var eventId: String = "", var eventType: String = "" ) : RealmObject() { - companion object + private var insertTypeStr: String = EventInsertType.INCREMENTAL_SYNC.name + var insertType: EventInsertType + get() { + return EventInsertType.valueOf(insertTypeStr) + } + set(value) { + insertTypeStr = value.name + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventInsertType.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventInsertType.java new file mode 100644 index 0000000000..179b0f791c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventInsertType.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 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.database.model; + +public enum EventInsertType { + INITIAL_SYNC, + INCREMENTAL_SYNC, + PAGINATION, + LOCAL_ECHO +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt index 1020b2bfaf..3618f3f7a8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt @@ -19,18 +19,21 @@ package im.vector.matrix.android.internal.database.query import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.EventInsertEntity +import im.vector.matrix.android.internal.database.model.EventInsertType import io.realm.Realm import io.realm.RealmList import io.realm.RealmQuery import io.realm.kotlin.where -internal fun EventEntity.copyToRealmOrIgnore(realm: Realm): EventEntity { +internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInsertType): EventEntity { val eventEntity = realm.where() .equalTo(EventEntityFields.EVENT_ID, eventId) .equalTo(EventEntityFields.ROOM_ID, roomId) .findFirst() return if (eventEntity == null) { - val insertEntity = EventInsertEntity(eventId = eventId, eventType = type) + val insertEntity = EventInsertEntity(eventId = eventId, eventType = type).apply { + this.insertType = insertType + } realm.insert(insertEntity) // copy this event entity and return it realm.copyToRealm(this) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/EventInsertLiveProcessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/EventInsertLiveProcessor.kt index c781d91c04..70d0a0d99f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/EventInsertLiveProcessor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/EventInsertLiveProcessor.kt @@ -17,11 +17,12 @@ package im.vector.matrix.android.internal.session import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.internal.database.model.EventInsertType import io.realm.Realm internal interface EventInsertLiveProcessor { - fun shouldProcess(eventId: String, eventType: String): Boolean + fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean suspend fun process(realm: Realm, event: Event) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallEventProcessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallEventProcessor.kt index 8a4a5407d5..142e64969f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallEventProcessor.kt @@ -18,6 +18,8 @@ package im.vector.matrix.android.internal.session.call import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.internal.database.model.EventInsertEntity +import im.vector.matrix.android.internal.database.model.EventInsertType import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.EventInsertLiveProcessor import io.realm.Realm @@ -37,7 +39,10 @@ internal class CallEventProcessor @Inject constructor( EventType.ENCRYPTED ) - override fun shouldProcess(eventId: String, eventType: String): Boolean { + override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { + if (insertType != EventInsertType.INCREMENTAL_SYNC) { + return false + } return allowedTypes.contains(eventType) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationProcessor.kt index 9cb9b7254b..bde0cc512d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationProcessor.kt @@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.database.mapper.EventMapper import im.vector.matrix.android.internal.database.model.EditAggregatedSummaryEntity import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.EventInsertType import im.vector.matrix.android.internal.database.model.PollResponseAggregatedSummaryEntity import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntity import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields @@ -96,7 +97,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr EventType.ENCRYPTED ) - override fun shouldProcess(eventId: String, eventType: String): Boolean { + override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { return allowedTypes.contains(eventType) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventProcessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventProcessor.kt index f2a3da3a30..fe2bf846ff 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventProcessor.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.VersioningState import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent +import im.vector.matrix.android.internal.database.model.EventInsertType import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.EventInsertLiveProcessor @@ -39,7 +40,7 @@ internal class RoomCreateEventProcessor @Inject constructor() : EventInsertLiveP realm.insertOrUpdate(predecessorRoomSummary) } - override fun shouldProcess(eventId: String, eventType: String): Boolean { + override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { return eventType == EventType.STATE_ROOM_CREATE } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt index ce4b31b89a..3ae3f812a9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt @@ -21,6 +21,8 @@ import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.database.mapper.toEntity import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity +import im.vector.matrix.android.internal.database.model.EventInsertEntity +import im.vector.matrix.android.internal.database.model.EventInsertType import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.copyToRealmOrIgnore import im.vector.matrix.android.internal.database.query.getOrCreate @@ -76,7 +78,7 @@ internal class DefaultLoadRoomMembersTask @Inject constructor( continue } val ageLocalTs = roomMemberEvent.unsignedData?.age?.let { now - it } - val eventEntity = roomMemberEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm) + val eventEntity = roomMemberEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) CurrentStateEventEntity.getOrCreate(realm, roomId, roomMemberEvent.stateKey, roomMemberEvent.type).apply { eventId = roomMemberEvent.eventId root = eventEntity diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/RedactionEventProcessor.kt index 2f61a98df3..f0e080cb37 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/RedactionEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/RedactionEventProcessor.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.events.model.UnsignedData import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.mapper.EventMapper import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.EventInsertType import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.findWithSenderMembershipEvent import im.vector.matrix.android.internal.database.query.where @@ -38,7 +39,7 @@ import javax.inject.Inject */ internal class RedactionEventProcessor @Inject constructor() : EventInsertLiveProcessor { - override fun shouldProcess(eventId: String, eventType: String): Boolean { + override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { return eventType == EventType.REDACTION } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt index 2d2c64ef7f..9ebced26e0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt @@ -29,6 +29,8 @@ import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.toEntity import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.EventInsertEntity +import im.vector.matrix.android.internal.database.model.EventInsertType import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates @@ -71,6 +73,10 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private val timelineEvent = timelineEventMapper.map(timelineEventEntity) eventBus.post(DefaultTimeline.OnLocalEchoCreated(roomId = roomId, timelineEvent = timelineEvent)) monarchy.writeAsync { realm -> + val eventInsertEntity = EventInsertEntity(event.eventId, event.type).apply { + this.insertType = EventInsertType.LOCAL_ECHO + } + realm.insert(eventInsertEntity) val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@writeAsync roomEntity.sendingTimelineEvents.add(0, timelineEventEntity) roomSummaryUpdater.update(realm, roomId) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 1822587972..6ecd8bbe7d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -29,14 +29,17 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper +import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntityFields +import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields import im.vector.matrix.android.internal.database.query.TimelineEventFilter import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.database.query.whereInRoom import im.vector.matrix.android.internal.database.query.whereRoomId import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith @@ -114,7 +117,6 @@ internal class DefaultTimeline( if (!results.isLoaded || !results.isValid) { return@OrderedRealmCollectionChangeListener } - results.createSnapshot() handleUpdates(results, changeSet) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt index 609ebe16fc..8e0e4759e9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -28,6 +28,7 @@ import im.vector.matrix.android.internal.database.helper.deleteOnCascade import im.vector.matrix.android.internal.database.helper.merge import im.vector.matrix.android.internal.database.mapper.toEntity import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.EventInsertType import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity @@ -204,7 +205,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri for (stateEvent in stateEvents) { val ageLocalTs = stateEvent.unsignedData?.age?.let { now - it } - val stateEventEntity = stateEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm) + val stateEventEntity = stateEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) currentChunk.addStateEvent(roomId, stateEventEntity, direction) if (stateEvent.type == EventType.STATE_ROOM_MEMBER && stateEvent.stateKey != null) { roomMemberContentsByUser[stateEvent.stateKey] = stateEvent.content.toModel() @@ -217,7 +218,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri } val ageLocalTs = event.unsignedData?.age?.let { now - it } eventIds.add(event.eventId) - val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm) + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) { val contentToUse = if (direction == PaginationDirection.BACKWARDS) { event.prevContent diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt index 68ef2981d8..cfdf43c737 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.VersioningState import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent +import im.vector.matrix.android.internal.database.model.EventInsertType import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.EventInsertLiveProcessor @@ -42,7 +43,7 @@ internal class RoomTombstoneEventProcessor @Inject constructor() : EventInsertLi realm.insertOrUpdate(predecessorRoomSummary) } - override fun shouldProcess(eventId: String, eventType: String): Boolean { + override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { return eventType == EventType.STATE_ROOM_TOMBSTONE } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index b1d8d7b0b5..f3af24001d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -35,6 +35,7 @@ import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.mapper.toEntity import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity +import im.vector.matrix.android.internal.database.model.EventInsertType import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity import im.vector.matrix.android.internal.database.query.copyToRealmOrIgnore @@ -47,9 +48,9 @@ import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService import im.vector.matrix.android.internal.session.mapWithProgress -import im.vector.matrix.android.internal.session.room.summary.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.membership.RoomMemberEventHandler import im.vector.matrix.android.internal.session.room.read.FullyReadContent +import im.vector.matrix.android.internal.session.room.summary.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecryptor @@ -97,20 +98,25 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle // PRIVATE METHODS ***************************************************************************** private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy, isInitialSync: Boolean, reporter: DefaultInitialSyncProgressService?) { + val insertType = if (isInitialSync) { + EventInsertType.INITIAL_SYNC + } else { + EventInsertType.INCREMENTAL_SYNC + } val syncLocalTimeStampMillis = System.currentTimeMillis() val rooms = when (handlingStrategy) { is HandlingStrategy.JOINED -> handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_joined_rooms, 0.6f) { - handleJoinedRoom(realm, it.key, it.value, isInitialSync, syncLocalTimeStampMillis) + handleJoinedRoom(realm, it.key, it.value, isInitialSync, insertType, syncLocalTimeStampMillis) } is HandlingStrategy.INVITED -> handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_invited_rooms, 0.1f) { - handleInvitedRoom(realm, it.key, it.value, syncLocalTimeStampMillis) + handleInvitedRoom(realm, it.key, it.value, insertType, syncLocalTimeStampMillis) } is HandlingStrategy.LEFT -> { handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_left_rooms, 0.3f) { - handleLeftRoom(realm, it.key, it.value, syncLocalTimeStampMillis) + handleLeftRoom(realm, it.key, it.value, insertType, syncLocalTimeStampMillis) } } } @@ -121,6 +127,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle roomId: String, roomSync: RoomSync, isInitialSync: Boolean, + insertType: EventInsertType, syncLocalTimestampMillis: Long): RoomEntity { Timber.v("Handle join sync for room $roomId") @@ -147,7 +154,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle continue } val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } - val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm) + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { eventId = event.eventId root = eventEntity @@ -165,6 +172,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle roomSync.timeline.events, roomSync.timeline.prevToken, roomSync.timeline.limited, + insertType, syncLocalTimestampMillis, isInitialSync ) @@ -191,6 +199,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle private fun handleInvitedRoom(realm: Realm, roomId: String, roomSync: InvitedRoomSync, + insertType: EventInsertType, syncLocalTimestampMillis: Long): RoomEntity { Timber.v("Handle invited sync for room $roomId") val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) @@ -201,7 +210,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle return@forEach } val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } - val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm) + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { eventId = eventEntity.eventId root = eventEntity @@ -219,6 +228,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle private fun handleLeftRoom(realm: Realm, roomId: String, roomSync: RoomSync, + insertType: EventInsertType, syncLocalTimestampMillis: Long): RoomEntity { val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) for (event in roomSync.state?.events.orEmpty()) { @@ -226,7 +236,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle continue } val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } - val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm) + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { eventId = event.eventId root = eventEntity @@ -238,7 +248,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle continue } val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } - val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm) + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) if (event.stateKey != null) { CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { eventId = event.eventId @@ -263,6 +273,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle eventList: List, prevToken: String? = null, isLimited: Boolean = true, + insertType: EventInsertType, syncLocalTimestampMillis: Long, isInitialSync: Boolean): ChunkEntity { val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId) @@ -289,7 +300,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } - val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm) + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) if (event.stateKey != null) { CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { eventId = event.eventId From 9ebf87df62bd9c3f5ead2455b01aa79b60957ffa Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 6 Jul 2020 18:47:59 +0200 Subject: [PATCH 008/122] Group: rework a bit how and when we fetch data about groups --- .../matrix/android/api/session/group/Group.kt | 11 +++ .../internal/database/mapper/GroupMapper.kt | 34 ------- .../internal/database/model/GroupEntity.kt | 3 +- .../database/query/GroupEntityQueries.kt | 9 +- .../query/GroupSummaryEntityQueries.kt | 6 ++ .../internal/di/WorkManagerProvider.kt | 12 +++ .../android/internal/session/SessionModule.kt | 5 - .../session/group/DefaultGetGroupDataTask.kt | 83 +++++++++++------ .../internal/session/group/DefaultGroup.kt | 16 +++- .../session/group/DefaultGroupService.kt | 10 +- .../session/group/GetGroupDataWorker.kt | 16 ++-- .../internal/session/group/GroupFactory.kt | 40 ++++++++ .../internal/session/group/GroupModule.kt | 5 + .../session/group/GroupSummaryUpdater.kt | 93 ------------------- .../internal/session/sync/GroupSyncHandler.kt | 10 +- .../session/sync/SyncResponseHandler.kt | 40 ++++++++ .../features/grouplist/GroupListViewModel.kt | 3 + 17 files changed, 212 insertions(+), 184 deletions(-) delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/GroupMapper.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupFactory.kt delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/Group.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/Group.kt index 3967c15704..cdc8bc1621 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/Group.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/Group.kt @@ -16,9 +16,20 @@ package im.vector.matrix.android.api.session.group +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable + /** * This interface defines methods to interact within a group. */ interface Group { val groupId: String + + /** + * This methods allows you to refresh data about this group. It will be reflected on the GroupSummary. + * The SDK also takes care of refreshing group data every hour. + * @param callback : the matrix callback to be notified of success or failure + * @return a Cancelable to be able to cancel requests. + */ + fun fetchGroupData(callback: MatrixCallback): Cancelable } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/GroupMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/GroupMapper.kt deleted file mode 100644 index 89ed5844c2..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/GroupMapper.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2019 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.database.mapper - -import im.vector.matrix.android.api.session.group.Group -import im.vector.matrix.android.internal.database.model.GroupEntity -import im.vector.matrix.android.internal.session.group.DefaultGroup - -internal object GroupMapper { - - fun map(groupEntity: GroupEntity): Group { - return DefaultGroup( - groupEntity.groupId - ) - } -} - -internal fun GroupEntity.asDomain(): Group { - return GroupMapper.map(this) -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/GroupEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/GroupEntity.kt index eb346a74ca..a0054ae8d6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/GroupEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/GroupEntity.kt @@ -22,8 +22,7 @@ import io.realm.annotations.PrimaryKey /** * This class is used to store group info (groupId and membership) from the sync response. - * Then [im.vector.matrix.android.internal.session.group.GroupSummaryUpdater] observes change and - * makes requests to fetch group information from the homeserver + * Then GetGroupDataTask is called regularly to fetch group information from the homeserver. */ internal open class GroupEntity(@PrimaryKey var groupId: String = "") : RealmObject() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupEntityQueries.kt index 802bfbeae6..1e4f5639c4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupEntityQueries.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.database.query import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.internal.database.model.GroupEntity import im.vector.matrix.android.internal.database.model.GroupEntityFields +import im.vector.matrix.android.internal.query.process import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.where @@ -28,10 +29,6 @@ internal fun GroupEntity.Companion.where(realm: Realm, groupId: String): RealmQu .equalTo(GroupEntityFields.GROUP_ID, groupId) } -internal fun GroupEntity.Companion.where(realm: Realm, membership: Membership? = null): RealmQuery { - val query = realm.where() - if (membership != null) { - query.equalTo(GroupEntityFields.MEMBERSHIP_STR, membership.name) - } - return query +internal fun GroupEntity.Companion.where(realm: Realm, memberships: List): RealmQuery { + return realm.where().process(GroupEntityFields.MEMBERSHIP_STR, memberships) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupSummaryEntityQueries.kt index 601da098ca..b062793f00 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupSummaryEntityQueries.kt @@ -18,8 +18,10 @@ package im.vector.matrix.android.internal.database.query import im.vector.matrix.android.internal.database.model.GroupSummaryEntity import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import io.realm.Realm import io.realm.RealmQuery +import io.realm.kotlin.createObject import io.realm.kotlin.where internal fun GroupSummaryEntity.Companion.where(realm: Realm, groupId: String? = null): RealmQuery { @@ -34,3 +36,7 @@ internal fun GroupSummaryEntity.Companion.where(realm: Realm, groupIds: List() .`in`(GroupSummaryEntityFields.GROUP_ID, groupIds.toTypedArray()) } + +internal fun GroupSummaryEntity.Companion.getOrCreate(realm: Realm, groupId: String): GroupSummaryEntity { + return where(realm, groupId).findFirst() ?: realm.createObject(groupId) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/WorkManagerProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/WorkManagerProvider.kt index 5a0202719b..71a3c4bfba 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/WorkManagerProvider.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/WorkManagerProvider.kt @@ -21,7 +21,10 @@ import androidx.work.Constraints import androidx.work.ListenableWorker import androidx.work.NetworkType import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager +import java.time.Duration +import java.util.concurrent.TimeUnit import javax.inject.Inject internal class WorkManagerProvider @Inject constructor( @@ -39,6 +42,15 @@ internal class WorkManagerProvider @Inject constructor( OneTimeWorkRequestBuilder() .addTag(tag) + /** + * Create a PeriodicWorkRequestBuilder, with the Matrix SDK tag + */ + inline fun matrixPeriodicWorkRequestBuilder(repeatInterval: Long, + repeatIntervalTimeUnit: TimeUnit) = + PeriodicWorkRequestBuilder(repeatInterval, repeatIntervalTimeUnit) + .addTag(tag) + + /** * Cancel all works instantiated by the Matrix SDK for the current session, and not those from the SDK client, or for other sessions */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 8aabd709ab..0feb944b38 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -68,7 +68,6 @@ import im.vector.matrix.android.internal.network.token.AccessTokenProvider import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider import im.vector.matrix.android.internal.session.call.CallEventProcessor import im.vector.matrix.android.internal.session.download.DownloadProgressInterceptor -import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater import im.vector.matrix.android.internal.session.homeserver.DefaultHomeServerCapabilitiesService import im.vector.matrix.android.internal.session.identity.DefaultIdentityService import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManager @@ -293,10 +292,6 @@ internal abstract class SessionModule { @Binds abstract fun bindNetworkConnectivityChecker(checker: DefaultNetworkConnectivityChecker): NetworkConnectivityChecker - @Binds - @IntoSet - abstract fun bindGroupSummaryUpdater(updater: GroupSummaryUpdater): SessionLifecycleObserver - @Binds @IntoSet abstract fun bindEventRedactionProcessor(processor: RedactionEventProcessor): EventInsertLiveProcessor diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt index 7c5de5b137..ee43441453 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt @@ -18,7 +18,9 @@ package im.vector.matrix.android.internal.session.group import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.internal.database.model.GroupEntity import im.vector.matrix.android.internal.database.model.GroupSummaryEntity +import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.network.executeRequest @@ -28,11 +30,14 @@ import im.vector.matrix.android.internal.session.group.model.GroupUsers import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.awaitTransaction import org.greenrobot.eventbus.EventBus +import timber.log.Timber import javax.inject.Inject internal interface GetGroupDataTask : Task { - - data class Params(val groupId: String) + sealed class Params { + object FetchAllActive : Params() + data class FetchWithIds(val groupIds: List) : Params() + } } internal class DefaultGetGroupDataTask @Inject constructor( @@ -41,44 +46,64 @@ internal class DefaultGetGroupDataTask @Inject constructor( private val eventBus: EventBus ) : GetGroupDataTask { + private data class GroupData( + val groupId: String, + val groupSummary: GroupSummaryResponse, + val groupRooms: GroupRooms, + val groupUsers: GroupUsers + ) + override suspend fun execute(params: GetGroupDataTask.Params) { - val groupId = params.groupId - val groupSummary = executeRequest(eventBus) { - apiCall = groupAPI.getSummary(groupId) + val groupIds = when (params) { + is GetGroupDataTask.Params.FetchAllActive -> { + getActiveGroupIds() + } + is GetGroupDataTask.Params.FetchWithIds -> { + params.groupIds + } } - val groupRooms = executeRequest(eventBus) { - apiCall = groupAPI.getRooms(groupId) + Timber.v("Fetch data for group with ids: ${groupIds.joinToString(";")}") + val data = groupIds.map { groupId -> + val groupSummary = executeRequest(eventBus) { + apiCall = groupAPI.getSummary(groupId) + } + val groupRooms = executeRequest(eventBus) { + apiCall = groupAPI.getRooms(groupId) + } + val groupUsers = executeRequest(eventBus) { + apiCall = groupAPI.getUsers(groupId) + } + GroupData(groupId, groupSummary, groupRooms, groupUsers) } - val groupUsers = executeRequest(eventBus) { - apiCall = groupAPI.getUsers(groupId) - } - insertInDb(groupSummary, groupRooms, groupUsers, groupId) + insertInDb(data) } - private suspend fun insertInDb(groupSummary: GroupSummaryResponse, - groupRooms: GroupRooms, - groupUsers: GroupUsers, - groupId: String) { + private fun getActiveGroupIds(): List { + return monarchy.fetchAllMappedSync( + { realm -> + GroupEntity.where(realm, Membership.activeMemberships()) + }, + { it.groupId } + ) + } + + private suspend fun insertInDb(groupDataList: List) { monarchy .awaitTransaction { realm -> - val groupSummaryEntity = GroupSummaryEntity.where(realm, groupId).findFirst() - ?: realm.createObject(GroupSummaryEntity::class.java, groupId) + groupDataList.forEach { groupData -> - groupSummaryEntity.avatarUrl = groupSummary.profile?.avatarUrl ?: "" - val name = groupSummary.profile?.name - groupSummaryEntity.displayName = if (name.isNullOrEmpty()) groupId else name - groupSummaryEntity.shortDescription = groupSummary.profile?.shortDescription ?: "" + val groupSummaryEntity = GroupSummaryEntity.getOrCreate(realm, groupData.groupId) - groupSummaryEntity.roomIds.clear() - groupRooms.rooms.mapTo(groupSummaryEntity.roomIds) { it.roomId } + groupSummaryEntity.avatarUrl = groupData.groupSummary.profile?.avatarUrl ?: "" + val name = groupData.groupSummary.profile?.name + groupSummaryEntity.displayName = if (name.isNullOrEmpty()) groupData.groupId else name + groupSummaryEntity.shortDescription = groupData.groupSummary.profile?.shortDescription ?: "" - groupSummaryEntity.userIds.clear() - groupUsers.users.mapTo(groupSummaryEntity.userIds) { it.userId } + groupSummaryEntity.roomIds.clear() + groupData.groupRooms.rooms.mapTo(groupSummaryEntity.roomIds) { it.roomId } - groupSummaryEntity.membership = when (groupSummary.user?.membership) { - Membership.JOIN.value -> Membership.JOIN - Membership.INVITE.value -> Membership.INVITE - else -> Membership.LEAVE + groupSummaryEntity.userIds.clear() + groupData.groupUsers.users.mapTo(groupSummaryEntity.userIds) { it.userId } } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroup.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroup.kt index 6c7b5b2a8b..a9e77a73d0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroup.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroup.kt @@ -16,6 +16,20 @@ package im.vector.matrix.android.internal.session.group +import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.group.Group +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith -internal class DefaultGroup(override val groupId: String) : Group +internal class DefaultGroup(override val groupId: String, + private val taskExecutor: TaskExecutor, + private val getGroupDataTask: GetGroupDataTask) : Group { + + override fun fetchGroupData(callback: MatrixCallback): Cancelable { + val params = GetGroupDataTask.Params.FetchWithIds(listOf(groupId)) + return getGroupDataTask.configureWith(params) { + this.callback = callback + }.executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt index af73b896f4..4dd162276f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.GroupEntity import im.vector.matrix.android.internal.database.model.GroupSummaryEntity import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields import im.vector.matrix.android.internal.database.query.where @@ -33,10 +34,15 @@ import io.realm.Realm import io.realm.RealmQuery import javax.inject.Inject -internal class DefaultGroupService @Inject constructor(@SessionDatabase private val monarchy: Monarchy) : GroupService { +internal class DefaultGroupService @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + private val groupFactory: GroupFactory) : GroupService { override fun getGroup(groupId: String): Group? { - return null + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + GroupEntity.where(realm, groupId).findFirst()?.let { + groupFactory.create(groupId) + } + } } override fun getGroupSummary(groupId: String): GroupSummary? { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt index bb33212f9c..f025040c39 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt @@ -35,7 +35,6 @@ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) : @JsonClass(generateAdapter = true) internal data class Params( override val sessionId: String, - val groupIds: List, override val lastFailureMessage: String? = null ) : SessionWorkerParams @@ -48,14 +47,11 @@ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) : val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) - val results = params.groupIds.map { groupId -> - runCatching { fetchGroupData(groupId) } - } - val isSuccessful = results.none { it.isFailure } - return if (isSuccessful) Result.success() else Result.retry() - } - - private suspend fun fetchGroupData(groupId: String) { - getGroupDataTask.execute(GetGroupDataTask.Params(groupId)) + return runCatching { + getGroupDataTask.execute(GetGroupDataTask.Params.FetchAllActive) + }.fold( + { Result.success() }, + { Result.retry() } + ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupFactory.kt new file mode 100644 index 0000000000..a5046d45d4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupFactory.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 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.session.group + +import im.vector.matrix.android.api.session.group.Group +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.task.TaskExecutor +import javax.inject.Inject + +internal interface GroupFactory { + fun create(groupId: String): Group +} + +@SessionScope +internal class DefaultGroupFactory @Inject constructor(private val getGroupDataTask: GetGroupDataTask, + private val taskExecutor: TaskExecutor) : + GroupFactory { + + override fun create(groupId: String): Group { + return DefaultGroup( + groupId = groupId, + taskExecutor = taskExecutor, + getGroupDataTask = getGroupDataTask + ) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupModule.kt index b48c6a96e8..bf960f061f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupModule.kt @@ -21,6 +21,8 @@ import dagger.Module import dagger.Provides import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.room.DefaultRoomFactory +import im.vector.matrix.android.internal.session.room.RoomFactory import retrofit2.Retrofit @Module @@ -36,6 +38,9 @@ internal abstract class GroupModule { } } + @Binds + abstract fun bindGroupFactory(factory: DefaultGroupFactory): GroupFactory + @Binds abstract fun bindGetGroupDataTask(task: DefaultGetGroupDataTask): GetGroupDataTask diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt deleted file mode 100644 index 52268ab7de..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2019 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.session.group - -import androidx.work.ExistingWorkPolicy -import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.internal.database.RealmLiveEntityObserver -import im.vector.matrix.android.internal.database.awaitTransaction -import im.vector.matrix.android.internal.database.model.GroupEntity -import im.vector.matrix.android.internal.database.model.GroupSummaryEntity -import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.di.SessionDatabase -import im.vector.matrix.android.internal.di.SessionId -import im.vector.matrix.android.internal.di.WorkManagerProvider -import im.vector.matrix.android.internal.worker.WorkerParamsFactory -import io.realm.OrderedCollectionChangeSet -import io.realm.RealmResults -import kotlinx.coroutines.launch -import javax.inject.Inject - -private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER" - -internal class GroupSummaryUpdater @Inject constructor( - private val workManagerProvider: WorkManagerProvider, - @SessionId private val sessionId: String, - @SessionDatabase private val monarchy: Monarchy) - : RealmLiveEntityObserver(monarchy.realmConfiguration) { - - override val query = Monarchy.Query { GroupEntity.where(it) } - - override fun onChange(results: RealmResults) { - /* - // `insertions` for new groups and `changes` to handle left groups - val modifiedGroupEntity = (changeSet.insertions + changeSet.changes) - .asSequence() - .mapNotNull { results[it] } - - fetchGroupsData(modifiedGroupEntity - .filter { it.membership == Membership.JOIN || it.membership == Membership.INVITE } - .map { it.groupId } - .toList()) - - modifiedGroupEntity - .filter { it.membership == Membership.LEAVE } - .map { it.groupId } - .toList() - .also { - observerScope.launch { - deleteGroups(it) - } - } - */ - } - - private fun fetchGroupsData(groupIds: List) { - val getGroupDataWorkerParams = GetGroupDataWorker.Params(sessionId, groupIds) - - val workData = WorkerParamsFactory.toData(getGroupDataWorkerParams) - - val getGroupWork = workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setInputData(workData) - .setConstraints(WorkManagerProvider.workConstraints) - .build() - - workManagerProvider.workManager - .beginUniqueWork(GET_GROUP_DATA_WORKER, ExistingWorkPolicy.APPEND, getGroupWork) - .enqueue() - } - - /** - * Delete the GroupSummaryEntity of left groups - */ - private suspend fun deleteGroups(groupIds: List) = awaitTransaction(monarchy.realmConfiguration) { realm -> - GroupSummaryEntity.where(realm, groupIds) - .findAll() - .deleteAllFromRealm() - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/GroupSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/GroupSyncHandler.kt index 392db0a73c..e3d4eae575 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/GroupSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/GroupSyncHandler.kt @@ -19,6 +19,8 @@ package im.vector.matrix.android.internal.session.sync import im.vector.matrix.android.R import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.internal.database.model.GroupEntity +import im.vector.matrix.android.internal.database.model.GroupSummaryEntity +import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService import im.vector.matrix.android.internal.session.mapWithProgress @@ -64,29 +66,33 @@ internal class GroupSyncHandler @Inject constructor() { handleLeftGroup(realm, it.key) } } - - /** Note: [im.vector.matrix.android.internal.session.group.GroupSummaryUpdater] is observing changes */ realm.insertOrUpdate(groups) } private fun handleJoinedGroup(realm: Realm, groupId: String): GroupEntity { val groupEntity = GroupEntity.where(realm, groupId).findFirst() ?: GroupEntity(groupId) + val groupSummaryEntity = GroupSummaryEntity.getOrCreate(realm, groupId) groupEntity.membership = Membership.JOIN + groupSummaryEntity.membership = Membership.JOIN return groupEntity } private fun handleInvitedGroup(realm: Realm, groupId: String): GroupEntity { val groupEntity = GroupEntity.where(realm, groupId).findFirst() ?: GroupEntity(groupId) + val groupSummaryEntity = GroupSummaryEntity.getOrCreate(realm, groupId) groupEntity.membership = Membership.INVITE + groupSummaryEntity.membership = Membership.INVITE return groupEntity } private fun handleLeftGroup(realm: Realm, groupId: String): GroupEntity { val groupEntity = GroupEntity.where(realm, groupId).findFirst() ?: GroupEntity(groupId) + val groupSummaryEntity = GroupSummaryEntity.getOrCreate(realm, groupId) groupEntity.membership = Membership.LEAVE + groupSummaryEntity.membership = Membership.LEAVE return groupEntity } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt index 5e8ef5a608..0769895d38 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt @@ -16,23 +16,34 @@ package im.vector.matrix.android.internal.session.sync +import androidx.work.ExistingPeriodicWorkPolicy import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R import im.vector.matrix.android.api.pushrules.PushRuleService import im.vector.matrix.android.api.pushrules.RuleScope import im.vector.matrix.android.internal.crypto.DefaultCryptoService import im.vector.matrix.android.internal.di.SessionDatabase +import im.vector.matrix.android.internal.di.SessionId +import im.vector.matrix.android.internal.di.WorkManagerProvider import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService +import im.vector.matrix.android.internal.session.group.GetGroupDataWorker import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask import im.vector.matrix.android.internal.session.reportSubtask +import im.vector.matrix.android.internal.session.sync.model.GroupsSyncResponse import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse import im.vector.matrix.android.internal.session.sync.model.SyncResponse import im.vector.matrix.android.internal.util.awaitTransaction +import im.vector.matrix.android.internal.worker.WorkerParamsFactory import timber.log.Timber +import java.util.concurrent.TimeUnit import javax.inject.Inject import kotlin.system.measureTimeMillis +private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER" + internal class SyncResponseHandler @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + @SessionId private val sessionId: String, + private val workManagerProvider: WorkManagerProvider, private val roomSyncHandler: RoomSyncHandler, private val userAccountDataSyncHandler: UserAccountDataSyncHandler, private val groupSyncHandler: GroupSyncHandler, @@ -109,10 +120,39 @@ internal class SyncResponseHandler @Inject constructor(@SessionDatabase private checkPushRules(it, isInitialSync) userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite) } + syncResponse.groups?.also { + scheduleGroupDataFetchingIfNeeded(it) + } + Timber.v("On sync completed") cryptoSyncHandler.onSyncCompleted(syncResponse) } + /** + * At the moment we don't get any group data through the sync, so we poll where every hour. + You can also force to refetch group data using [Group] API. + */ + private fun scheduleGroupDataFetchingIfNeeded(groupsSyncResponse: GroupsSyncResponse) { + val groupIds = ArrayList() + groupIds.addAll(groupsSyncResponse.join.keys) + groupIds.addAll(groupsSyncResponse.invite.keys) + if (groupIds.isEmpty()) { + Timber.v("No new groups to fetch data for.") + return + } + Timber.v("There are ${groupIds.size} new groups to fetch data for.") + val getGroupDataWorkerParams = GetGroupDataWorker.Params(sessionId) + val workData = WorkerParamsFactory.toData(getGroupDataWorkerParams) + + val getGroupWork = workManagerProvider.matrixPeriodicWorkRequestBuilder(1, TimeUnit.HOURS) + .setInputData(workData) + .setConstraints(WorkManagerProvider.workConstraints) + .build() + + workManagerProvider.workManager + .enqueueUniquePeriodicWork(GET_GROUP_DATA_WORKER, ExistingPeriodicWorkPolicy.REPLACE, getGroupWork) + } + private suspend fun checkPushRules(roomsSyncResponse: RoomsSyncResponse, isInitialSync: Boolean) { Timber.v("[PushRules] --> checkPushRules") if (isInitialSync) { diff --git a/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListViewModel.kt index f14583c5d5..b4a65d67c8 100644 --- a/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListViewModel.kt @@ -23,6 +23,7 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.NoOpMatrixCallback import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.group.groupSummaryQueryParams @@ -88,6 +89,8 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro private fun handleSelectGroup(action: GroupListAction.SelectGroup) = withState { state -> if (state.selectedGroup?.groupId != action.groupSummary.groupId) { + // We take care of refreshing group data when selecting to be sure we get all the rooms and users + session.getGroup(action.groupSummary.groupId)?.fetchGroupData(NoOpMatrixCallback()) setState { copy(selectedGroup = action.groupSummary) } } } From c1da4aecd77eb5eba9f3f596bd7d6e27eb030115 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 6 Jul 2020 19:09:08 +0200 Subject: [PATCH 009/122] Fix leaving selected group --- .../im/vector/riotx/features/grouplist/GroupListViewModel.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListViewModel.kt index b4a65d67c8..4c8e5c2333 100644 --- a/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListViewModel.kt @@ -75,6 +75,11 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro } val optionGroup = Option.just(groupSummary) selectedGroupStore.post(optionGroup) + } else { + // If selected group is null we force to default. It can happens when leaving the selected group. + setState { + copy(selectedGroup = this.asyncGroups()?.find { it.groupId == ALL_COMMUNITIES_GROUP_ID }) + } } } } From bf03b367f129b2b8fb7c97d3a4eadd684b3686a6 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 6 Jul 2020 19:12:24 +0200 Subject: [PATCH 010/122] Clean code --- .../android/internal/database/model/ChunkEntity.kt | 2 +- .../internal/database/model/EventInsertEntity.kt | 3 --- .../database/query/GroupSummaryEntityQueries.kt | 1 - .../matrix/android/internal/di/WorkManagerProvider.kt | 2 -- .../matrix/android/internal/session/DefaultSession.kt | 11 ----------- .../internal/session/call/CallEventProcessor.kt | 1 - .../android/internal/session/call/CallModule.kt | 1 - .../android/internal/session/filter/FilterUtil.kt | 3 +-- .../android/internal/session/group/GroupModule.kt | 2 -- .../session/room/membership/LoadRoomMembersTask.kt | 1 - .../internal/session/room/timeline/DefaultTimeline.kt | 7 ++----- 11 files changed, 4 insertions(+), 30 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt index b1e08fdeea..7014146539 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt @@ -27,7 +27,7 @@ internal open class ChunkEntity(@Index var prevToken: String? = null, @Index var nextToken: String? = null, var stateEvents: RealmList = RealmList(), var timelineEvents: RealmList = RealmList(), - var numberOfTimelineEvents: Long= 0, + var numberOfTimelineEvents: Long = 0, // Only one chunk will have isLastForward == true @Index var isLastForward: Boolean = false, @Index var isLastBackward: Boolean = false diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventInsertEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventInsertEntity.kt index bb5186263c..e23ea5c3d0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventInsertEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventInsertEntity.kt @@ -16,9 +16,7 @@ package im.vector.matrix.android.internal.database.model -import im.vector.matrix.android.api.session.room.model.Membership import io.realm.RealmObject -import io.realm.annotations.Index /** * This class is used to get notification on new events being inserted. It's to avoid realm getting slow when listening to insert @@ -36,5 +34,4 @@ internal open class EventInsertEntity(var eventId: String = "", set(value) { insertTypeStr = value.name } - } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupSummaryEntityQueries.kt index b062793f00..18d40d0e68 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupSummaryEntityQueries.kt @@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.database.query import im.vector.matrix.android.internal.database.model.GroupSummaryEntity import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields -import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.createObject diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/WorkManagerProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/WorkManagerProvider.kt index 71a3c4bfba..a7d1b68c92 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/WorkManagerProvider.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/WorkManagerProvider.kt @@ -23,7 +23,6 @@ import androidx.work.NetworkType import androidx.work.OneTimeWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager -import java.time.Duration import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -50,7 +49,6 @@ internal class WorkManagerProvider @Inject constructor( PeriodicWorkRequestBuilder(repeatInterval, repeatIntervalTimeUnit) .addTag(tag) - /** * Cancel all works instantiated by the Matrix SDK for the current session, and not those from the SDK client, or for other sessions */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index e7ab2a7926..83ba76d5b8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -50,21 +50,10 @@ import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.widgets.WidgetService import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.crypto.DefaultCryptoService -import im.vector.matrix.android.internal.database.awaitTransaction -import im.vector.matrix.android.internal.database.helper.nextDisplayIndex -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.ChunkEntityFields -import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity -import im.vector.matrix.android.internal.database.model.CurrentStateEventEntityFields -import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.WorkManagerProvider import im.vector.matrix.android.internal.session.identity.DefaultIdentityService -import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecryptor import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.session.sync.job.SyncThread diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallEventProcessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallEventProcessor.kt index 142e64969f..34ee7b2c54 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallEventProcessor.kt @@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.session.call import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.internal.database.model.EventInsertEntity import im.vector.matrix.android.internal.database.model.EventInsertType import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.EventInsertLiveProcessor diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallModule.kt index 5e75738dec..bc4cef8772 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/call/CallModule.kt @@ -41,5 +41,4 @@ internal abstract class CallModule { @Binds abstract fun bindGetTurnServerTask(task: DefaultGetTurnServerTask): GetTurnServerTask - } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterUtil.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterUtil.kt index d95a53cb07..53ede5ad45 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterUtil.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterUtil.kt @@ -98,8 +98,7 @@ internal object FilterUtil { state = filter.room.state?.copy(lazyLoadMembers = true) ?: RoomEventFilter(lazyLoadMembers = true) ) - ?: RoomFilter(state = RoomEventFilter(lazyLoadMembers = true), - timeline = RoomEventFilter(limit = 1500)) + ?: RoomFilter(state = RoomEventFilter(lazyLoadMembers = true)) ) } else { val newRoomEventFilter = filter.room?.state?.copy(lazyLoadMembers = null)?.takeIf { it.hasData() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupModule.kt index bf960f061f..6799ffd3e5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupModule.kt @@ -21,8 +21,6 @@ import dagger.Module import dagger.Provides import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.internal.session.SessionScope -import im.vector.matrix.android.internal.session.room.DefaultRoomFactory -import im.vector.matrix.android.internal.session.room.RoomFactory import retrofit2.Retrofit @Module diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt index 3ae3f812a9..d860ccc7e4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt @@ -21,7 +21,6 @@ import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.database.mapper.toEntity import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity -import im.vector.matrix.android.internal.database.model.EventInsertEntity import im.vector.matrix.android.internal.database.model.EventInsertType import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.copyToRealmOrIgnore diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 6ecd8bbe7d..16c98770e2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -29,17 +29,14 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper -import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntityFields -import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields import im.vector.matrix.android.internal.database.query.TimelineEventFilter import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.database.query.whereInRoom import im.vector.matrix.android.internal.database.query.whereRoomId import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith @@ -501,8 +498,8 @@ internal class DefaultTimeline( val currentChunk = getLiveChunk() val token = if (direction == Timeline.Direction.BACKWARDS) currentChunk?.prevToken else currentChunk?.nextToken if (token == null) { - if (direction == Timeline.Direction.BACKWARDS || - (direction == Timeline.Direction.FORWARDS && currentChunk?.hasBeenALastForwardChunk().orFalse())) { + if (direction == Timeline.Direction.BACKWARDS + || (direction == Timeline.Direction.FORWARDS && currentChunk?.hasBeenALastForwardChunk().orFalse())) { // We are in the case where event exists, but we do not know the token. // Fetch (again) the last event to get a token val lastKnownEventId = if (direction == Timeline.Direction.FORWARDS) { From a0998e4aff8d0b88f5d1cd9ecb900bd1cf00427e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 6 Jul 2020 21:53:10 +0200 Subject: [PATCH 011/122] Revert to gradle build 3.5.3 --- build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index edd6edcd93..af3952b2d3 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,8 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:3.6.3' + // Warning: 3.6.3 leads to infinite gradle builds. Stick to 3.5.3 for the moment + classpath 'com.android.tools.build:gradle:3.5.3' classpath 'com.google.gms:google-services:4.3.2' classpath "com.airbnb.okreplay:gradle-plugin:1.5.0" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" From 92e809fa6d421e4cc780bbdf85926ad8d5fe9e3d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 6 Jul 2020 21:57:28 +0200 Subject: [PATCH 012/122] Fix lint error, following the upgrade of the libs --- .../settings/troubleshoot/TestBatteryOptimization.kt | 3 ++- .../im/vector/riotx/core/preference/VectorListPreference.kt | 4 +--- .../src/main/java/im/vector/riotx/core/utils/SystemUtils.kt | 2 +- .../crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt | 4 ++-- .../crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt | 6 +++--- .../crypto/recover/BootstrapSaveRecoveryKeyFragment.kt | 2 +- .../riotx/features/home/room/detail/RoomDetailFragment.kt | 2 +- .../riotx/features/roomprofile/RoomProfileFragment.kt | 2 +- .../riotx/features/settings/VectorSettingsBaseFragment.kt | 4 ++-- .../features/settings/VectorSettingsGeneralFragment.kt | 4 ++-- .../VectorSettingsNotificationsTroubleshootFragment.kt | 2 +- .../features/settings/VectorSettingsPreferencesFragment.kt | 2 +- .../account/deactivation/DeactivateAccountFragment.kt | 2 +- 13 files changed, 19 insertions(+), 20 deletions(-) diff --git a/vector/src/fdroid/java/im/vector/riotx/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt b/vector/src/fdroid/java/im/vector/riotx/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt index 4d18beac8f..2abbf7a419 100644 --- a/vector/src/fdroid/java/im/vector/riotx/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt +++ b/vector/src/fdroid/java/im/vector/riotx/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt @@ -26,7 +26,8 @@ import im.vector.riotx.features.settings.troubleshoot.TroubleshootTest class TestBatteryOptimization(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_battery_title) { override fun perform() { - if (fragment.context != null && isIgnoringBatteryOptimizations(fragment.context!!)) { + val context = fragment.context + if (context != null && isIgnoringBatteryOptimizations(context)) { description = fragment.getString(R.string.settings_troubleshoot_test_battery_success) status = TestStatus.SUCCESS quickFix = null diff --git a/vector/src/main/java/im/vector/riotx/core/preference/VectorListPreference.kt b/vector/src/main/java/im/vector/riotx/core/preference/VectorListPreference.kt index d85d343155..174c52d831 100644 --- a/vector/src/main/java/im/vector/riotx/core/preference/VectorListPreference.kt +++ b/vector/src/main/java/im/vector/riotx/core/preference/VectorListPreference.kt @@ -90,8 +90,6 @@ class VectorListPreference : ListPreference { fun setWarningIconVisible(isVisible: Boolean) { mIsWarningIconVisible = isVisible - if (null != mWarningIconView) { - mWarningIconView!!.visibility = if (mIsWarningIconVisible) View.VISIBLE else View.GONE - } + mWarningIconView?.visibility = if (mIsWarningIconVisible) View.VISIBLE else View.GONE } } diff --git a/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt index 9e5af038ef..900d5565dc 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt @@ -162,7 +162,7 @@ fun startImportTextFromFileIntent(fragment: Fragment, requestCode: Int) { val intent = Intent(Intent.ACTION_GET_CONTENT).apply { type = "text/plain" } - if (intent.resolveActivity(fragment.activity!!.packageManager) != null) { + if (intent.resolveActivity(fragment.requireActivity().packageManager) != null) { fragment.startActivityForResult(intent, requestCode) } else { fragment.activity?.toast(R.string.error_no_external_application_found) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt index 93d6f43763..a3306677fe 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt @@ -176,7 +176,7 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment() else -> { viewModel.megolmBackupCreationInfo = null - viewModel.prepareRecoveryKey(activity!!, viewModel.passphrase.value) + viewModel.prepareRecoveryKey(requireActivity(), viewModel.passphrase.value) } } } @@ -188,7 +188,7 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment() // Generate a recovery key for the user viewModel.megolmBackupCreationInfo = null - viewModel.prepareRecoveryKey(activity!!, null) + viewModel.prepareRecoveryKey(requireActivity(), null) } else -> { // User has entered a passphrase but want to skip this step. diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt index 1478b99d3b..21a25f1684 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt @@ -105,7 +105,7 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment() @OnClick(R.id.keys_backup_setup_step3_copy_button) fun onCopyButtonClicked() { - val dialog = BottomSheetDialog(activity!!) + val dialog = BottomSheetDialog(requireActivity()) dialog.setContentView(R.layout.bottom_sheet_save_recovery_key) dialog.setCanceledOnTouchOutside(true) val recoveryKey = viewModel.recoveryKey.value!! @@ -124,7 +124,7 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment() } it.debouncedClicks { - copyToClipboard(activity!!, recoveryKey) + copyToClipboard(requireActivity(), recoveryKey) } } } @@ -159,7 +159,7 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment() viewModel.recoveryKey.value?.let { viewModel.copyHasBeenMade = true - copyToClipboard(activity!!, it) + copyToClipboard(requireActivity(), it) } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt index 3ab48e44ff..2c31474122 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt @@ -78,7 +78,7 @@ class BootstrapSaveRecoveryKeyFragment @Inject constructor( if (resultCode == RESULT_OK && uri != null) { GlobalScope.launch(Dispatchers.IO) { try { - sharedViewModel.handle(BootstrapActions.SaveKeyToUri(context!!.contentResolver!!.openOutputStream(uri)!!)) + sharedViewModel.handle(BootstrapActions.SaveKeyToUri(requireContext().contentResolver!!.openOutputStream(uri)!!)) } catch (failure: Throwable) { sharedViewModel.handle(BootstrapActions.SaveReqFailed) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 9d7ea58bb5..8c075004a9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -1541,7 +1541,7 @@ class RoomDetailFragment @Inject constructor( } private fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) { - Snackbar.make(view!!, message, duration).show() + Snackbar.make(requireView(), message, duration).show() } // VectorInviteView.Callback diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt index f0cb29ea6b..aa414ec2a1 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt @@ -243,7 +243,7 @@ class RoomProfileFragment @Inject constructor( private fun onAvatarClicked(view: View, matrixItem: MatrixItem.RoomItem) = withState(roomProfileViewModel) { if (matrixItem.avatarUrl?.isNotEmpty() == true) { val intent = BigImageViewerActivity.newIntent(requireContext(), matrixItem.getBestName(), matrixItem.avatarUrl!!, it.canChangeAvatar) - val options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity!!, view, ViewCompat.getTransitionName(view) ?: "") + val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), view, ViewCompat.getTransitionName(view) ?: "") startActivityForResult(intent, BigImageViewerActivity.REQUEST_CODE, options.toBundle()) } else if (it.canChangeAvatar) { showAvatarSelector() diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsBaseFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsBaseFragment.kt index 85d32251b6..c43a6ab40d 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsBaseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsBaseFragment.kt @@ -83,8 +83,8 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), HasScree * ========================================================================================== */ protected fun notImplemented() { - // Snackbar cannot be display on PreferenceFragment - // Snackbar.make(view!!, R.string.not_implemented, Snackbar.LENGTH_SHORT) + // Snackbar cannot be display on PreferenceFragment. TODO It's maybe because the show() method is not used... + // Snackbar.make(requireView(), R.string.not_implemented, Snackbar.LENGTH_SHORT) activity?.toast(R.string.not_implemented) } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt index 18fa9d95ed..6bfb88a480 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt @@ -221,7 +221,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { it.onPreferenceClickListener = Preference.OnPreferenceClickListener { displayLoadingView() - MainActivity.restartApp(activity!!, MainActivityArgs(clearCache = true)) + MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCache = true)) false } } @@ -622,7 +622,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { var order = addEmailBtn.order for ((index, email3PID) in currentEmail3PID.withIndex()) { - val preference = VectorPreference(activity!!) + val preference = VectorPreference(requireActivity()) preference.title = getString(R.string.settings_email_address) preference.summary = "TODO" // email3PID.address diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt index 04908e166f..3ac097abfe 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt @@ -72,7 +72,7 @@ class VectorSettingsNotificationsTroubleshootFragment @Inject constructor( mRecyclerView.addItemDecoration(dividerItemDecoration) mSummaryButton.debouncedClicks { - bugReporter.openBugReportScreen(activity!!) + bugReporter.openBugReportScreen(requireActivity()) } mRunButton.debouncedClicks { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsPreferencesFragment.kt index ed8f15db98..5848caacdb 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsPreferencesFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsPreferencesFragment.kt @@ -134,7 +134,7 @@ class VectorSettingsPreferencesFragment @Inject constructor( selectedLanguagePreference.summary = VectorLocale.localeToLocalisedString(VectorLocale.applicationLocale) // Text size - textSizePreference.summary = getString(FontScale.getFontScaleValue(activity!!).nameResId) + textSizePreference.summary = getString(FontScale.getFontScaleValue(requireActivity()).nameResId) textSizePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { activity?.let { displayTextSizeSelection(it) } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountFragment.kt index 8a8a5fa4e4..447f1086be 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountFragment.kt @@ -107,7 +107,7 @@ class DeactivateAccountFragment @Inject constructor( displayErrorDialog(it.throwable) } DeactivateAccountViewEvents.Done -> - MainActivity.restartApp(activity!!, MainActivityArgs(clearCredentials = true, isAccountDeactivated = true)) + MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCredentials = true, isAccountDeactivated = true)) }.exhaustive } } From 89506b9e81f87330ee5ca2abd0bff78364db1f7c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 6 Jul 2020 23:35:05 +0200 Subject: [PATCH 013/122] Version++ --- CHANGES.md | 26 +++++++++++++++++++++++++- vector/build.gradle | 2 +- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e264facd8e..6e404f9b6c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,28 @@ -Changes in Riot.imX 0.91.4 (2020-XX-XX) +Changes in Riot.imX 0.91.5 (2020-XX-XX) +=================================================== + +Features ✨: + - + +Improvements 🙌: + - + +Bugfix 🐛: + - + +Translations 🗣: + - + +SDK API changes ⚠️: + - + +Build 🧱: + - + +Other changes: + - + +Changes in Riot.imX 0.91.4 (2020-07-06) =================================================== Features ✨: diff --git a/vector/build.gradle b/vector/build.gradle index f966f441b2..c83b0671a7 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -17,7 +17,7 @@ androidExtensions { // Note: 2 digits max for each value ext.versionMajor = 0 ext.versionMinor = 91 -ext.versionPatch = 4 +ext.versionPatch = 5 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' From bcfd322b8535dcfe1e10ef624e54a5b9fa555f44 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 7 Jul 2020 12:00:41 +0200 Subject: [PATCH 014/122] Update changelog after PR merged --- CHANGES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 76fd186648..04ac45c3cc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,9 @@ Features ✨: - Improvements 🙌: - - + - Cleaning chunks with lots of events as long as a threshold has been exceeded (35_000 events in DB) (#1634) + - Creating and listening to EventInsertEntity. (#1634) + - Handling (almost) properly the groups fetching (#1634) Bugfix 🐛: - From 6f996f1f09e6ada22fffce6ec44ec37984896d83 Mon Sep 17 00:00:00 2001 From: Mathieu Velten Date: Tue, 7 Jul 2020 14:23:59 +0200 Subject: [PATCH 015/122] Put xmx to 2048m --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d9d9e57cbc..99fd9d64fd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ # The setting is particularly useful for tweaking memory settings. android.enableJetifier=true android.useAndroidX=true -org.gradle.jvmargs=-Xmx8192m +org.gradle.jvmargs=-Xmx2048m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects From 0855806ae200b5f71c407e8f67b5eeacdc0d09ed Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 7 Jul 2020 22:14:11 +0200 Subject: [PATCH 016/122] Fix edit being stuck --- vector/build.gradle | 3 ++- .../home/room/detail/RoomDetailFragment.kt | 2 +- .../home/room/detail/RoomDetailViewModel.kt | 17 +++++++---------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/vector/build.gradle b/vector/build.gradle index 59ae3d35de..86ffb8f9da 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -289,7 +289,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.1.0' implementation "androidx.fragment:fragment:$fragment_version" implementation "androidx.fragment:fragment-ktx:$fragment_version" - implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta7' + // Keep at 2.0.0-beta4 at the moment, as updating is breaking some UI + implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4' implementation 'androidx.core:core-ktx:1.3.0' implementation "org.threeten:threetenbp:1.4.0:no-tzdb" diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 8c075004a9..e0a1b10f9e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -636,7 +636,7 @@ class RoomDetailFragment @Inject constructor( val document = parser.parse(messageContent.formattedBody ?: messageContent.body) formattedBody = eventHtmlRenderer.render(document) } - composerLayout.composerRelatedMessageContent.text = formattedBody ?: nonFormattedBody + composerLayout.composerRelatedMessageContent.text = (formattedBody ?: nonFormattedBody) updateComposerText(defaultContent) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 62830a1c63..6276089145 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -846,17 +846,14 @@ class RoomDetailViewModel @AssistedInject constructor( } } - private fun handleExitSpecialMode(action: RoomDetailAction.ExitSpecialMode) { - setState { copy(sendMode = SendMode.REGULAR(action.text)) } - withState { state -> - // For edit, just delete the current draft - if (state.sendMode is SendMode.EDIT) { - room.deleteDraft(NoOpMatrixCallback()) - } else { - // Save a new draft and keep the previously entered text - room.saveDraft(UserDraft.REGULAR(action.text), NoOpMatrixCallback()) - } + private fun handleExitSpecialMode(action: RoomDetailAction.ExitSpecialMode) = withState { + if (it.sendMode is SendMode.EDIT) { + room.deleteDraft(NoOpMatrixCallback()) + } else { + // Save a new draft and keep the previously entered text + room.saveDraft(UserDraft.REGULAR(action.text), NoOpMatrixCallback()) } + setState { copy(sendMode = SendMode.REGULAR(action.text)) } } private fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) { From c5ba74690471fa8763513592ae1744678ef25763 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 8 Jul 2020 11:00:13 +0200 Subject: [PATCH 017/122] Fixes #1647 share not working --- CHANGES.md | 2 +- .../home/room/detail/RoomDetailFragment.kt | 14 +++++++------- .../home/room/detail/RoomDetailViewModel.kt | 14 +++++++------- .../features/media/ImageMediaViewerActivity.kt | 14 +++++++------- .../riotx/features/media/VideoContentRenderer.kt | 2 +- .../features/media/VideoMediaViewerActivity.kt | 14 +++++++------- .../roomprofile/uploads/RoomUploadsViewModel.kt | 14 +++++++------- 7 files changed, 37 insertions(+), 37 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 04ac45c3cc..1200be4a12 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,7 +10,7 @@ Improvements 🙌: - Handling (almost) properly the groups fetching (#1634) Bugfix 🐛: - - + - Regression | Share action menu do not work (#1647) Translations 🗣: - diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 8c075004a9..d38a26c099 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -1337,13 +1337,13 @@ class RoomDetailFragment @Inject constructor( private fun onShareActionClicked(action: EventSharedAction.Share) { session.fileService().downloadFile( - FileService.DownloadMode.FOR_EXTERNAL_SHARE, - action.eventId, - action.messageContent.body, - action.messageContent.getFileUrl(), - action.messageContent.mimeType, - action.messageContent.encryptedFileInfo?.toElementToDecrypt(), - object : MatrixCallback { + downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, + id = action.eventId, + fileName = action.messageContent.body, + mimeType = action.messageContent.mimeType, + url = action.messageContent.getFileUrl(), + elementToDecrypt = action.messageContent.encryptedFileInfo?.toElementToDecrypt(), + callback = object : MatrixCallback { override fun onSuccess(data: File) { if (isAdded) { shareMedia(requireContext(), data, getMimeTypeFromUri(requireContext(), data.toUri())) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 62830a1c63..e2e7700d1f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -873,13 +873,13 @@ class RoomDetailViewModel @AssistedInject constructor( } } else { session.fileService().downloadFile( - FileService.DownloadMode.FOR_INTERNAL_USE, - action.eventId, - action.messageFileContent.getFileName(), - action.messageFileContent.mimeType, - mxcUrl, - action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(), - object : MatrixCallback { + downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, + id = action.eventId, + fileName = action.messageFileContent.getFileName(), + mimeType = action.messageFileContent.mimeType, + url = mxcUrl, + elementToDecrypt = action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(), + callback = object : MatrixCallback { override fun onSuccess(data: File) { _viewEvents.post(RoomDetailViewEvents.DownloadFileState( action.messageFileContent.mimeType, diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt index 2be940d0c1..092199759f 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt @@ -134,13 +134,13 @@ class ImageMediaViewerActivity : VectorBaseActivity() { private fun onShareActionClicked() { session.fileService().downloadFile( - FileService.DownloadMode.FOR_EXTERNAL_SHARE, - mediaData.eventId, - mediaData.filename, - mediaData.mimeType, - mediaData.url, - mediaData.elementToDecrypt, - object : MatrixCallback { + downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, + id = mediaData.eventId, + fileName = mediaData.filename, + mimeType = mediaData.mimeType, + url = mediaData.url, + elementToDecrypt = mediaData.elementToDecrypt, + callback = object : MatrixCallback { override fun onSuccess(data: File) { shareMedia(this@ImageMediaViewerActivity, data, getMimeTypeFromUri(this@ImageMediaViewerActivity, data.toUri())) } diff --git a/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt index eb9105f792..760d3b12a0 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt @@ -70,7 +70,7 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder: downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, id = data.eventId, fileName = data.filename, - mimeType = null, + mimeType = data.mimeType, url = data.url, elementToDecrypt = data.elementToDecrypt, callback = object : MatrixCallback { diff --git a/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt index 6ef8927f00..d9df861a25 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt @@ -79,13 +79,13 @@ class VideoMediaViewerActivity : VectorBaseActivity() { private fun onShareActionClicked() { session.fileService().downloadFile( - FileService.DownloadMode.FOR_EXTERNAL_SHARE, - mediaData.eventId, - mediaData.filename, - mediaData.mimeType, - mediaData.url, - mediaData.elementToDecrypt, - object : MatrixCallback { + downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, + id = mediaData.eventId, + fileName = mediaData.filename, + mimeType = mediaData.mimeType, + url = mediaData.url, + elementToDecrypt = mediaData.elementToDecrypt, + callback = object : MatrixCallback { override fun onSuccess(data: File) { shareMedia(this@VideoMediaViewerActivity, data, getMimeTypeFromUri(this@VideoMediaViewerActivity, data.toUri())) } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt index 10f0a5051e..7cc8b9b31d 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt @@ -158,13 +158,13 @@ class RoomUploadsViewModel @AssistedInject constructor( try { val file = awaitCallback { session.fileService().downloadFile( - FileService.DownloadMode.FOR_EXTERNAL_SHARE, - action.uploadEvent.eventId, - action.uploadEvent.contentWithAttachmentContent.body, - action.uploadEvent.contentWithAttachmentContent.getFileUrl(), - action.uploadEvent.contentWithAttachmentContent.mimeType, - action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(), - it) + downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, + id = action.uploadEvent.eventId, + fileName = action.uploadEvent.contentWithAttachmentContent.body, + mimeType = action.uploadEvent.contentWithAttachmentContent.mimeType, + url = action.uploadEvent.contentWithAttachmentContent.getFileUrl(), + elementToDecrypt = action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(), + callback = it) } _viewEvents.post(RoomUploadsViewEvents.FileReadyForSaving(file, action.uploadEvent.contentWithAttachmentContent.body)) } catch (failure: Throwable) { From e8dbed1642197037cd1e470870b5c78c0f841391 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 8 Jul 2020 14:51:15 +0200 Subject: [PATCH 018/122] Fix relations on encrypted room --- .../session/room/EventRelationsAggregationProcessor.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationProcessor.kt index bde0cc512d..5214317f3b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationProcessor.kt @@ -109,7 +109,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr return } val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "") - when (event.getClearType()) { + when (event.type) { EventType.REACTION -> { // we got a reaction!! Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}") @@ -161,7 +161,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE || encryptedEventContent?.relatesTo?.type == RelationType.RESPONSE ) { - // we need to decrypt if needed event.getClearContent().toModel()?.let { if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) { Timber.v("###REPLACE in room $roomId for event ${event.eventId}") From 3aabb17ea5ea9e9c76d03558fc2ac5a1a429d733 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 8 Jul 2020 15:51:00 +0200 Subject: [PATCH 019/122] Fix timeline pagination when no displayable events --- .../internal/session/room/timeline/DefaultTimeline.kt | 5 ++++- .../session/room/timeline/TokenChunkEventPersistor.kt | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 16c98770e2..567698668b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -349,7 +349,7 @@ internal class DefaultTimeline( updateState(Timeline.Direction.FORWARDS) { it.copy( - hasMoreInCache = firstBuiltEvent == null || firstBuiltEvent.displayIndex < firstCacheEvent?.displayIndex ?: Int.MIN_VALUE, + hasMoreInCache = firstBuiltEvent != null && firstBuiltEvent.displayIndex < firstCacheEvent?.displayIndex ?: Int.MIN_VALUE, hasReachedEnd = chunkEntity?.isLastForward ?: false ) } @@ -369,6 +369,9 @@ internal class DefaultTimeline( private fun paginateInternal(startDisplayIndex: Int?, direction: Timeline.Direction, count: Int): Boolean { + if (count == 0) { + return false + } updateState(direction) { it.copy(requestedPaginationCount = count, isPaginating = true) } val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong()) val shouldFetchMore = builtCount < count && !hasReachedEnd(direction) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt index 8e0e4759e9..87bff38587 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -241,12 +241,12 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri chunksToDelete.add(it) } } - val shouldUpdateSummary = chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS chunksToDelete.forEach { it.deleteOnCascade() } + val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) + val shouldUpdateSummary = roomSummaryEntity.latestPreviewableEvent == null || (chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS) if (shouldUpdateSummary) { - val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) val latestPreviewableEvent = TimelineEventEntity.latestEvent( realm, roomId, From 85e8e652f1ca2ee161c9af0dc2ade7eb5a2425bc Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 8 Jul 2020 17:32:54 +0200 Subject: [PATCH 020/122] Fix IM terms of review path --- .../java/im/vector/riotx/features/widgets/WidgetViewModel.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewModel.kt index d516137bc5..89d597c4dc 100644 --- a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewModel.kt @@ -16,6 +16,7 @@ package im.vector.riotx.features.widgets +import android.net.Uri import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail @@ -236,7 +237,9 @@ class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: Wi _viewEvents.post(WidgetViewEvents.OnURLFormatted(formattedUrl)) } catch (failure: Throwable) { if (failure is WidgetManagementFailure.TermsNotSignedException) { - _viewEvents.post(WidgetViewEvents.DisplayTerms(initialState.baseUrl, failure.token)) + // Terms for IM shouldn't have path appended + val displayTermsBaseUrl = Uri.parse(initialState.baseUrl).buildUpon().path("").toString() + _viewEvents.post(WidgetViewEvents.DisplayTerms(displayTermsBaseUrl, failure.token)) } setState { copy(formattedURL = Fail(failure)) } } From 6ebedaf540e63150a60649ca33fce7df72b1ea7d Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 8 Jul 2020 17:40:37 +0200 Subject: [PATCH 021/122] Update CHANGES --- CHANGES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 04ac45c3cc..da6f0abf44 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,7 +10,9 @@ Improvements 🙌: - Handling (almost) properly the groups fetching (#1634) Bugfix 🐛: - - + - Integration Manager: Wrong URL to review terms if URL in config contains path (#1606) + - Regression Composer does not grow, crops out text (#1650) + - Bug / Unwanted draft (#698) Translations 🗣: - From 75c2dfcd48e8fe4844ef358605b6c5340b45f30f Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 8 Jul 2020 19:16:22 +0200 Subject: [PATCH 022/122] Fix user data being affected by local room member event changes --- .../android/api/session/events/model/UnsignedData.kt | 7 ++++++- .../session/room/membership/RoomMemberEventHandler.kt | 2 +- .../room/detail/timeline/format/NoticeEventFormatter.kt | 8 ++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt index b179cb7a31..16ff36ea07 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt @@ -39,5 +39,10 @@ data class UnsignedData( * Optional. The previous content for this event. If there is no previous content, this key will be missing. */ @Json(name = "prev_content") val prevContent: Map? = null, - @Json(name = "m.relations") val relations: AggregatedRelations? = null + @Json(name = "m.relations") val relations: AggregatedRelations? = null, + /** + * Optional. The eventId of the previous state event being replaced. + */ + @Json(name = "replaces_state") val replacesState: String? = null + ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt index d7d578b635..b225895532 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt @@ -34,7 +34,7 @@ internal class RoomMemberEventHandler @Inject constructor() { val userId = event.stateKey ?: return false val roomMemberEntity = RoomMemberEntityFactory.create(roomId, userId, roomMember) realm.insertOrUpdate(roomMemberEntity) - if (roomMember.membership.isActive()) { + if (roomMember.membership.isActive() && event.unsignedData?.replacesState.isNullOrEmpty()) { val userEntity = UserEntityFactory.create(userId, roomMember) realm.insertOrUpdate(userEntity) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index c1f4187e0b..fff74e0328 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -92,7 +92,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active private fun formatRoomPowerLevels(event: Event, disambiguatedDisplayName: String): CharSequence? { val powerLevelsContent: PowerLevelsContent = event.getClearContent().toModel() ?: return null - val previousPowerLevelsContent: PowerLevelsContent = event.prevContent.toModel() ?: return null + val previousPowerLevelsContent: PowerLevelsContent = event.resolvedPrevContent().toModel() ?: return null val userIds = HashSet() userIds.addAll(powerLevelsContent.users.keys) userIds.addAll(previousPowerLevelsContent.users.keys) @@ -120,7 +120,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active private fun formatWidgetEvent(event: Event, disambiguatedDisplayName: String): CharSequence? { val widgetContent: WidgetContent = event.getClearContent().toModel() ?: return null - val previousWidgetContent: WidgetContent? = event.prevContent.toModel() + val previousWidgetContent: WidgetContent? = event.resolvedPrevContent().toModel() return if (widgetContent.isActive()) { val widgetName = widgetContent.getHumanName() if (previousWidgetContent?.isActive().orFalse()) { @@ -294,7 +294,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active private fun formatRoomMemberEvent(event: Event, senderName: String?): String? { val eventContent: RoomMemberContent? = event.getClearContent().toModel() - val prevEventContent: RoomMemberContent? = event.prevContent.toModel() + val prevEventContent: RoomMemberContent? = event.resolvedPrevContent().toModel() val isMembershipEvent = prevEventContent?.membership != eventContent?.membership return if (isMembershipEvent) { buildMembershipNotice(event, senderName, eventContent, prevEventContent) @@ -305,7 +305,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active private fun formatRoomAliasesEvent(event: Event, senderName: String?): String? { val eventContent: RoomAliasesContent? = event.getClearContent().toModel() - val prevEventContent: RoomAliasesContent? = event.unsignedData?.prevContent?.toModel() + val prevEventContent: RoomAliasesContent? = event.resolvedPrevContent()?.toModel() val addedAliases = eventContent?.aliases.orEmpty() - prevEventContent?.aliases.orEmpty() val removedAliases = prevEventContent?.aliases.orEmpty() - eventContent?.aliases.orEmpty() From 4a2a6d34aebbb2383a71eaa0ad99c24dac81cabd Mon Sep 17 00:00:00 2001 From: Valere Date: Sun, 5 Jul 2020 21:47:38 +0200 Subject: [PATCH 023/122] Initial commit --- attachment-viewer/.gitignore | 1 + attachment-viewer/build.gradle | 81 +++++++ attachment-viewer/consumer-rules.pro | 0 attachment-viewer/proguard-rules.pro | 21 ++ .../src/main/AndroidManifest.xml | 11 + .../AttachmentSourceProvider.kt | 36 +++ .../AttachmentViewerActivity.kt | 210 ++++++++++++++++++ .../attachment_viewer/AttachmentsAdapter.kt | 133 +++++++++++ .../attachment_viewer/ImageViewHolder.kt | 75 +++++++ .../riotx/attachment_viewer/SwipeDirection.kt | 38 ++++ .../SwipeDirectionDetector.kt | 90 ++++++++ .../SwipeToDismissHandler.kt | 126 +++++++++++ .../res/layout/activity_attachment_viewer.xml | 46 ++++ .../main/res/layout/item_image_attachment.xml | 22 ++ .../main/res/layout/item_video_attachment.xml | 26 +++ .../main/res/layout/view_image_attachment.xml | 17 ++ .../src/main/res/values/dimens.xml | 3 + .../src/main/res/values/strings.xml | 11 + .../src/main/res/values/styles.xml | 12 + build.gradle | 2 + .../session/room/timeline/TimelineService.kt | 2 + .../room/timeline/DefaultTimelineService.kt | 25 ++- settings.gradle | 4 +- vector/build.gradle | 5 + vector/src/main/AndroidManifest.xml | 5 + .../vector/riotx/core/di/ScreenComponent.kt | 2 + .../home/room/detail/RoomDetailFragment.kt | 2 +- .../features/media/ImageContentRenderer.kt | 63 ++++++ .../media/ImageMediaViewerActivity.kt | 2 + .../features/media/RoomAttachmentProvider.kt | 82 +++++++ .../media/VectorAttachmentViewerActivity.kt | 207 +++++++++++++++++ .../features/navigation/DefaultNavigator.kt | 56 +++-- .../riotx/features/navigation/Navigator.kt | 2 +- .../riotx/features/popup/PopupAlertManager.kt | 3 +- .../uploads/media/RoomUploadsMediaFragment.kt | 2 +- .../features/themes/ActivityOtherThemes.kt | 6 + vector/src/main/res/values/theme_common.xml | 11 + 37 files changed, 1409 insertions(+), 31 deletions(-) create mode 100644 attachment-viewer/.gitignore create mode 100644 attachment-viewer/build.gradle create mode 100644 attachment-viewer/consumer-rules.pro create mode 100644 attachment-viewer/proguard-rules.pro create mode 100644 attachment-viewer/src/main/AndroidManifest.xml create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentViewerActivity.kt create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentsAdapter.kt create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ImageViewHolder.kt create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirection.kt create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirectionDetector.kt create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeToDismissHandler.kt create mode 100644 attachment-viewer/src/main/res/layout/activity_attachment_viewer.xml create mode 100644 attachment-viewer/src/main/res/layout/item_image_attachment.xml create mode 100644 attachment-viewer/src/main/res/layout/item_video_attachment.xml create mode 100644 attachment-viewer/src/main/res/layout/view_image_attachment.xml create mode 100644 attachment-viewer/src/main/res/values/dimens.xml create mode 100644 attachment-viewer/src/main/res/values/strings.xml create mode 100644 attachment-viewer/src/main/res/values/styles.xml create mode 100644 vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt diff --git a/attachment-viewer/.gitignore b/attachment-viewer/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/attachment-viewer/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle new file mode 100644 index 0000000000..7fcda7a742 --- /dev/null +++ b/attachment-viewer/build.gradle @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2020 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. + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +buildscript { + repositories { + maven { + url 'https://jitpack.io' + content { + // PhotoView + includeGroupByRegex 'com\\.github\\.chrisbanes' + } + } + jcenter() + } + +} + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.3" + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { +// implementation 'com.github.MikeOrtiz:TouchImageView:3.0.2' + implementation 'com.github.chrisbanes:PhotoView:2.0.0' + implementation "com.github.bumptech.glide:glide:4.10.0" + + implementation fileTree(dir: "libs", include: ["*.jar"]) + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.core:core-ktx:1.3.0' + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'com.google.android.material:material:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.navigation:navigation-fragment-ktx:2.1.0' + implementation 'androidx.navigation:navigation-ui-ktx:2.1.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + +} \ No newline at end of file diff --git a/attachment-viewer/consumer-rules.pro b/attachment-viewer/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/attachment-viewer/proguard-rules.pro b/attachment-viewer/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/attachment-viewer/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/attachment-viewer/src/main/AndroidManifest.xml b/attachment-viewer/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..4a632774f7 --- /dev/null +++ b/attachment-viewer/src/main/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt new file mode 100644 index 0000000000..9fd2902970 --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 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.riotx.attachment_viewer + +sealed class AttachmentInfo { + data class Image(val url: String, val data: Any?) : AttachmentInfo() + data class Video(val url: String, val data: Any) : AttachmentInfo() + data class Audio(val url: String, val data: Any) : AttachmentInfo() + data class File(val url: String, val data: Any) : AttachmentInfo() + + fun bind() { + } +} + +interface AttachmentSourceProvider { + + fun getItemCount(): Int + + fun getAttachmentInfoAt(position: Int): AttachmentInfo + + fun loadImage(holder: ImageViewHolder, info: AttachmentInfo.Image) +} diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentViewerActivity.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentViewerActivity.kt new file mode 100644 index 0000000000..2d4cbff00d --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentViewerActivity.kt @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2020 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.riotx.attachment_viewer + +import android.graphics.Color +import android.os.Bundle +import android.util.Log +import android.view.MotionEvent +import android.view.ScaleGestureDetector +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import androidx.viewpager2.widget.ViewPager2 +import kotlinx.android.synthetic.main.activity_attachment_viewer.* +import kotlin.math.abs + +abstract class AttachmentViewerActivity : AppCompatActivity() { + + lateinit var pager2: ViewPager2 + lateinit var imageTransitionView: ImageView + lateinit var transitionImageContainer: ViewGroup + + // TODO + private var overlayView: View? = null + + private lateinit var swipeDismissHandler: SwipeToDismissHandler + private lateinit var directionDetector: SwipeDirectionDetector + private lateinit var scaleDetector: ScaleGestureDetector + + + var currentPosition = 0 + + private var swipeDirection: SwipeDirection? = null + + private fun isScaled() = attachmentsAdapter.isScaled(currentPosition) + + private var wasScaled: Boolean = false + private var isSwipeToDismissAllowed: Boolean = true + private lateinit var attachmentsAdapter: AttachmentsAdapter + +// private val shouldDismissToBottom: Boolean +// get() = e == null +// || !externalTransitionImageView.isRectVisible +// || !isAtStartPosition + + private var isImagePagerIdle = true + + fun setSourceProvider(sourceProvider: AttachmentSourceProvider) { + attachmentsAdapter.attachmentSourceProvider = sourceProvider + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_attachment_viewer) + attachmentPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL + attachmentsAdapter = AttachmentsAdapter() + attachmentPager.adapter = attachmentsAdapter + imageTransitionView = transitionImageView + transitionImageContainer = findViewById(R.id.transitionImageContainer) + pager2 = attachmentPager + directionDetector = createSwipeDirectionDetector() + + attachmentPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageScrollStateChanged(state: Int) { + isImagePagerIdle = state == ViewPager2.SCROLL_STATE_IDLE + } + + override fun onPageSelected(position: Int) { + currentPosition = position + } + }) + + swipeDismissHandler = createSwipeToDismissHandler() + rootContainer.setOnTouchListener(swipeDismissHandler) + rootContainer.viewTreeObserver.addOnGlobalLayoutListener { swipeDismissHandler.translationLimit = dismissContainer.height / 4 } + + scaleDetector = createScaleGestureDetector() + + } + + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + + // The zoomable view is configured to disallow interception when image is zoomed + + // Check if the overlay is visible, and wants to handle the click +// if (overlayView.isVisible && overlayView?.dispatchTouchEvent(event) == true) { +// return true +// } + + + Log.v("ATTACHEMENTS", "================\ndispatchTouchEvent $ev") + handleUpDownEvent(ev) + + Log.v("ATTACHEMENTS", "scaleDetector is in progress ${scaleDetector.isInProgress}") + Log.v("ATTACHEMENTS", "pointerCount ${ev.pointerCount}") + Log.v("ATTACHEMENTS", "wasScaled ${wasScaled}") + if (swipeDirection == null && (scaleDetector.isInProgress || ev.pointerCount > 1 || wasScaled)) { + wasScaled = true + Log.v("ATTACHEMENTS", "dispatch to pager") + return attachmentPager.dispatchTouchEvent(ev) + } + + + Log.v("ATTACHEMENTS", "is current item scaled ${isScaled()}") + return (if (isScaled()) super.dispatchTouchEvent(ev) else handleTouchIfNotScaled(ev)).also { + Log.v("ATTACHEMENTS", "\n================") + } + } + + private fun handleUpDownEvent(event: MotionEvent) { + Log.v("ATTACHEMENTS", "handleUpDownEvent $event") + if (event.action == MotionEvent.ACTION_UP) { + handleEventActionUp(event) + } + + if (event.action == MotionEvent.ACTION_DOWN) { + handleEventActionDown(event) + } + + scaleDetector.onTouchEvent(event) +// gestureDetector.onTouchEvent(event) + } + + private fun handleEventActionDown(event: MotionEvent) { + swipeDirection = null + wasScaled = false + attachmentPager.dispatchTouchEvent(event) + + swipeDismissHandler.onTouch(rootContainer, event) +// isOverlayWasClicked = dispatchOverlayTouch(event) + } + + private fun handleEventActionUp(event: MotionEvent) { +// wasDoubleTapped = false + swipeDismissHandler.onTouch(rootContainer, event) + attachmentPager.dispatchTouchEvent(event) +// isOverlayWasClicked = dispatchOverlayTouch(event) + } + + private fun handleTouchIfNotScaled(event: MotionEvent): Boolean { + + Log.v("ATTACHEMENTS", "handleTouchIfNotScaled ${event}") + directionDetector.handleTouchEvent(event) + + return when (swipeDirection) { + SwipeDirection.Up, SwipeDirection.Down -> { + if (isSwipeToDismissAllowed && !wasScaled && isImagePagerIdle) { + swipeDismissHandler.onTouch(rootContainer, event) + } else true + } + SwipeDirection.Left, SwipeDirection.Right -> { + attachmentPager.dispatchTouchEvent(event) + } + else -> true + } + } + + + private fun handleSwipeViewMove(translationY: Float, translationLimit: Int) { + val alpha = calculateTranslationAlpha(translationY, translationLimit) + backgroundView.alpha = alpha + dismissContainer.alpha = alpha + overlayView?.alpha = alpha + } + + private fun dispatchOverlayTouch(event: MotionEvent): Boolean = + overlayView + ?.let { it.isVisible && it.dispatchTouchEvent(event) } + ?: false + + private fun calculateTranslationAlpha(translationY: Float, translationLimit: Int): Float = + 1.0f - 1.0f / translationLimit.toFloat() / 4f * abs(translationY) + + private fun createSwipeToDismissHandler() + : SwipeToDismissHandler = SwipeToDismissHandler( + swipeView = dismissContainer, + shouldAnimateDismiss = { shouldAnimateDismiss() }, + onDismiss = { animateClose() }, + onSwipeViewMove = ::handleSwipeViewMove) + + private fun createSwipeDirectionDetector() = + SwipeDirectionDetector(this) { swipeDirection = it } + + private fun createScaleGestureDetector() = + ScaleGestureDetector(this, ScaleGestureDetector.SimpleOnScaleGestureListener()) + + + protected open fun shouldAnimateDismiss(): Boolean = true + + protected open fun animateClose() { + window.statusBarColor = Color.TRANSPARENT + finish() + } +} diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentsAdapter.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentsAdapter.kt new file mode 100644 index 0000000000..b9914e4dda --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentsAdapter.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2020 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.riotx.attachment_viewer + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView + + +abstract class BaseViewHolder constructor(itemView: View) : + RecyclerView.ViewHolder(itemView) { + + abstract fun bind(attachmentInfo: AttachmentInfo) +} + + +class AttachmentViewHolder constructor(itemView: View) : + BaseViewHolder(itemView) { + + override fun bind(attachmentInfo: AttachmentInfo) { + + } +} + +//class AttachmentsAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : FragmentStateAdapter(fragmentManager, lifecycle) { +class AttachmentsAdapter() : RecyclerView.Adapter() { + + var attachmentSourceProvider: AttachmentSourceProvider? = null + set(value) { + field = value + notifyDataSetChanged() + } + + var recyclerView: RecyclerView? = null + + override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { + this.recyclerView = recyclerView + } + + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + this.recyclerView = null + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { + val inflater = LayoutInflater.from(parent.context) + val itemView = inflater.inflate(viewType, parent, false) + return when (viewType) { + R.layout.item_image_attachment -> ImageViewHolder(itemView) + else -> AttachmentViewHolder(itemView) + } + } + + override fun getItemViewType(position: Int): Int { + val info = attachmentSourceProvider!!.getAttachmentInfoAt(position) + return when (info) { + is AttachmentInfo.Image -> R.layout.item_image_attachment + is AttachmentInfo.Video -> R.layout.item_video_attachment + is AttachmentInfo.Audio -> TODO() + is AttachmentInfo.File -> TODO() + } + + } + + override fun getItemCount(): Int { + return attachmentSourceProvider?.getItemCount() ?: 0 + } + + override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { + attachmentSourceProvider?.getAttachmentInfoAt(position)?.let { + holder.bind(it) + if (it is AttachmentInfo.Image) { + attachmentSourceProvider?.loadImage(holder as ImageViewHolder, it) + } + } + } + + fun isScaled(position: Int): Boolean { + val holder = recyclerView?.findViewHolderForAdapterPosition(position) + if (holder is ImageViewHolder) { + return holder.touchImageView.attacher.scale > 1f + } + return false + } + +// override fun getItemCount(): Int { +// return 8 +// } +// +// override fun createFragment(position: Int): Fragment { +// // Return a NEW fragment instance in createFragment(int) +// val fragment = DemoObjectFragment() +// fragment.arguments = Bundle().apply { +// // Our object is just an integer :-P +// putInt(ARG_OBJECT, position + 1) +// } +// return fragment +// } + +} + + +//private const val ARG_OBJECT = "object" +// +//// Instances of this class are fragments representing a single +//// object in our collection. +//class DemoObjectFragment : Fragment() { +// +// override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { +// return inflater.inflate(R.layout.view_image_attachment, container, false) +// } +// +// override fun onViewCreated(view: View, savedInstanceState: Bundle?) { +// arguments?.takeIf { it.containsKey(ARG_OBJECT) }?.apply { +// val textView: TextView = view.findViewById(R.id.testPage) +// textView.text = getInt(ARG_OBJECT).toString() +// } +// } +//} diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ImageViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ImageViewHolder.kt new file mode 100644 index 0000000000..cac6a4fd9e --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ImageViewHolder.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2020 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.riotx.attachment_viewer + +import android.graphics.drawable.Drawable +import android.util.Log +import android.view.View +import android.widget.LinearLayout +import android.widget.ProgressBar +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import com.bumptech.glide.request.target.CustomViewTarget +import com.bumptech.glide.request.transition.Transition +import com.github.chrisbanes.photoview.PhotoView + +class ImageViewHolder constructor(itemView: View) : + BaseViewHolder(itemView) { + + val touchImageView: PhotoView = itemView.findViewById(R.id.touchImageView) + val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress) + + init { + touchImageView.setAllowParentInterceptOnEdge(false) + touchImageView.setOnScaleChangeListener { scaleFactor, _, _ -> + Log.v("ATTACHEMENTS", "scaleFactor $scaleFactor") + // It's a bit annoying but when you pitch down the scaling + // is not exactly one :/ + touchImageView.setAllowParentInterceptOnEdge(scaleFactor <= 1.0008f) + } + touchImageView.setScale(1.0f, true) + touchImageView.setAllowParentInterceptOnEdge(true) + } + + val customTargetView = object : CustomViewTarget(touchImageView) { + + override fun onResourceLoading(placeholder: Drawable?) { + imageLoaderProgress.isVisible = true + } + + override fun onLoadFailed(errorDrawable: Drawable?) { + imageLoaderProgress.isVisible = false + } + + override fun onResourceCleared(placeholder: Drawable?) { + touchImageView.setImageDrawable(placeholder) + } + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + imageLoaderProgress.isVisible = false + // Glide mess up the view size :/ + touchImageView.updateLayoutParams { + width = LinearLayout.LayoutParams.MATCH_PARENT + height = LinearLayout.LayoutParams.MATCH_PARENT + } + touchImageView.setImageDrawable(resource) + } + } + + override fun bind(attachmentInfo: AttachmentInfo) { + } +} diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirection.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirection.kt new file mode 100644 index 0000000000..fc54d292c2 --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirection.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 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.riotx.attachment_viewer + +sealed class SwipeDirection { + object NotDetected : SwipeDirection() + object Up : SwipeDirection() + object Down : SwipeDirection() + object Left : SwipeDirection() + object Right : SwipeDirection() + + companion object { + fun fromAngle(angle: Double): SwipeDirection { + return when (angle) { + in 0.0..45.0 -> Right + in 45.0..135.0 -> Up + in 135.0..225.0 -> Left + in 225.0..315.0 -> Down + in 315.0..360.0 -> Right + else -> NotDetected + } + } + } +} diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirectionDetector.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirectionDetector.kt new file mode 100644 index 0000000000..cce37a6d05 --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirectionDetector.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2020 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.riotx.attachment_viewer + +import android.content.Context +import android.view.MotionEvent +import kotlin.math.sqrt + +class SwipeDirectionDetector( + context: Context, + private val onDirectionDetected: (SwipeDirection) -> Unit +) { + + private val touchSlop: Int = android.view.ViewConfiguration.get(context).scaledTouchSlop + private var startX: Float = 0f + private var startY: Float = 0f + private var isDetected: Boolean = false + + fun handleTouchEvent(event: MotionEvent) { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + startX = event.x + startY = event.y + } + MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { + if (!isDetected) { + onDirectionDetected(SwipeDirection.NotDetected) + } + startY = 0.0f + startX = startY + isDetected = false + } + MotionEvent.ACTION_MOVE -> if (!isDetected && getEventDistance(event) > touchSlop) { + isDetected = true + onDirectionDetected(getDirection(startX, startY, event.x, event.y)) + } + } + } + + /** + * Given two points in the plane p1=(x1, x2) and p2=(y1, y1), this method + * returns the direction that an arrow pointing from p1 to p2 would have. + * + * @param x1 the x position of the first point + * @param y1 the y position of the first point + * @param x2 the x position of the second point + * @param y2 the y position of the second point + * @return the direction + */ + private fun getDirection(x1: Float, y1: Float, x2: Float, y2: Float): SwipeDirection { + val angle = getAngle(x1, y1, x2, y2) + return SwipeDirection.fromAngle(angle) + } + + /** + * Finds the angle between two points in the plane (x1,y1) and (x2, y2) + * The angle is measured with 0/360 being the X-axis to the right, angles + * increase counter clockwise. + * + * @param x1 the x position of the first point + * @param y1 the y position of the first point + * @param x2 the x position of the second point + * @param y2 the y position of the second point + * @return the angle between two points + */ + private fun getAngle(x1: Float, y1: Float, x2: Float, y2: Float): Double { + val rad = Math.atan2((y1 - y2).toDouble(), (x2 - x1).toDouble()) + Math.PI + return (rad * 180 / Math.PI + 180) % 360 + } + + private fun getEventDistance(ev: MotionEvent): Float { + val dx = ev.getX(0) - startX + val dy = ev.getY(0) - startY + return sqrt((dx * dx + dy * dy).toDouble()).toFloat() + } +} diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeToDismissHandler.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeToDismissHandler.kt new file mode 100644 index 0000000000..3a317d94e2 --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeToDismissHandler.kt @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2020 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.riotx.attachment_viewer + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.annotation.SuppressLint +import android.graphics.Rect +import android.view.MotionEvent +import android.view.View +import android.view.ViewPropertyAnimator +import android.view.animation.AccelerateInterpolator + +class SwipeToDismissHandler( + private val swipeView: View, + private val onDismiss: () -> Unit, + private val onSwipeViewMove: (translationY: Float, translationLimit: Int) -> Unit, + private val shouldAnimateDismiss: () -> Boolean +) : View.OnTouchListener { + + companion object { + private const val ANIMATION_DURATION = 200L + } + + var translationLimit: Int = swipeView.height / 4 + private var isTracking = false + private var startY: Float = 0f + + @SuppressLint("ClickableViewAccessibility") + override fun onTouch(v: View, event: MotionEvent): Boolean { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + if (swipeView.hitRect.contains(event.x.toInt(), event.y.toInt())) { + isTracking = true + } + startY = event.y + return true + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + if (isTracking) { + isTracking = false + onTrackingEnd(v.height) + } + return true + } + MotionEvent.ACTION_MOVE -> { + if (isTracking) { + val translationY = event.y - startY + swipeView.translationY = translationY + onSwipeViewMove(translationY, translationLimit) + } + return true + } + else -> { + return false + } + } + } + + internal fun initiateDismissToBottom() { + animateTranslation(swipeView.height.toFloat()) + } + + private fun onTrackingEnd(parentHeight: Int) { + val animateTo = when { + swipeView.translationY < -translationLimit -> -parentHeight.toFloat() + swipeView.translationY > translationLimit -> parentHeight.toFloat() + else -> 0f + } + + if (animateTo != 0f && !shouldAnimateDismiss()) { + onDismiss() + } else { + animateTranslation(animateTo) + } + } + + private fun animateTranslation(translationTo: Float) { + swipeView.animate() + .translationY(translationTo) + .setDuration(ANIMATION_DURATION) + .setInterpolator(AccelerateInterpolator()) + .setUpdateListener { onSwipeViewMove(swipeView.translationY, translationLimit) } + .setAnimatorListener(onAnimationEnd = { + if (translationTo != 0f) { + onDismiss() + } + + //remove the update listener, otherwise it will be saved on the next animation execution: + swipeView.animate().setUpdateListener(null) + }) + .start() + } +} + +internal fun ViewPropertyAnimator.setAnimatorListener( + onAnimationEnd: ((Animator?) -> Unit)? = null, + onAnimationStart: ((Animator?) -> Unit)? = null +) = this.setListener( + object : AnimatorListenerAdapter() { + + override fun onAnimationEnd(animation: Animator?) { + onAnimationEnd?.invoke(animation) + } + + override fun onAnimationStart(animation: Animator?) { + onAnimationStart?.invoke(animation) + } + }) + +internal val View?.hitRect: Rect + get() = Rect().also { this?.getHitRect(it) } diff --git a/attachment-viewer/src/main/res/layout/activity_attachment_viewer.xml b/attachment-viewer/src/main/res/layout/activity_attachment_viewer.xml new file mode 100644 index 0000000000..a8a68db1a5 --- /dev/null +++ b/attachment-viewer/src/main/res/layout/activity_attachment_viewer.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/attachment-viewer/src/main/res/layout/item_image_attachment.xml b/attachment-viewer/src/main/res/layout/item_image_attachment.xml new file mode 100644 index 0000000000..91a009df2a --- /dev/null +++ b/attachment-viewer/src/main/res/layout/item_image_attachment.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/attachment-viewer/src/main/res/layout/item_video_attachment.xml b/attachment-viewer/src/main/res/layout/item_video_attachment.xml new file mode 100644 index 0000000000..9449ec2e9f --- /dev/null +++ b/attachment-viewer/src/main/res/layout/item_video_attachment.xml @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/attachment-viewer/src/main/res/layout/view_image_attachment.xml b/attachment-viewer/src/main/res/layout/view_image_attachment.xml new file mode 100644 index 0000000000..3518a4472d --- /dev/null +++ b/attachment-viewer/src/main/res/layout/view_image_attachment.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/attachment-viewer/src/main/res/values/dimens.xml b/attachment-viewer/src/main/res/values/dimens.xml new file mode 100644 index 0000000000..125df87119 --- /dev/null +++ b/attachment-viewer/src/main/res/values/dimens.xml @@ -0,0 +1,3 @@ + + 16dp + \ No newline at end of file diff --git a/attachment-viewer/src/main/res/values/strings.xml b/attachment-viewer/src/main/res/values/strings.xml new file mode 100644 index 0000000000..6dcb56555a --- /dev/null +++ b/attachment-viewer/src/main/res/values/strings.xml @@ -0,0 +1,11 @@ + + AttachementViewerActivity + + First Fragment + Second Fragment + Next + Previous + + Hello first fragment + Hello second fragment. Arg: %1$s + \ No newline at end of file diff --git a/attachment-viewer/src/main/res/values/styles.xml b/attachment-viewer/src/main/res/values/styles.xml new file mode 100644 index 0000000000..a81174782e --- /dev/null +++ b/attachment-viewer/src/main/res/values/styles.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index af3952b2d3..47b3ab240d 100644 --- a/build.gradle +++ b/build.gradle @@ -39,6 +39,8 @@ allprojects { includeGroupByRegex "com\\.github\\.yalantis" // JsonViewer includeGroupByRegex 'com\\.github\\.BillCarsonFr' + // PhotoView + includeGroupByRegex 'com\\.github\\.chrisbanes' } } maven { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt index a69127532e..bdbbbf11bd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt @@ -39,4 +39,6 @@ interface TimelineService { fun getTimeLineEvent(eventId: String): TimelineEvent? fun getTimeLineEventLive(eventId: String): LiveData> + + fun getAttachementMessages() : List } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index 5723568197..ebdb8dd24d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -21,19 +21,24 @@ import androidx.lifecycle.Transformations import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.events.model.isImageMessage import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineService import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.toOptional +import im.vector.matrix.android.internal.crypto.store.db.doWithRealm import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.fetchCopyMap +import io.realm.Sort +import io.realm.kotlin.where import org.greenrobot.eventbus.EventBus internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String, @@ -73,10 +78,10 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv override fun getTimeLineEvent(eventId: String): TimelineEvent? { return monarchy .fetchCopyMap({ - TimelineEventEntity.where(it, roomId = roomId, eventId = eventId).findFirst() - }, { entity, _ -> - timelineEventMapper.map(entity) - }) + TimelineEventEntity.where(it, roomId = roomId, eventId = eventId).findFirst() + }, { entity, _ -> + timelineEventMapper.map(entity) + }) } override fun getTimeLineEventLive(eventId: String): LiveData> { @@ -88,4 +93,16 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv events.firstOrNull().toOptional() } } + + override fun getAttachementMessages(): List { + // TODO pretty bad query.. maybe we should denormalize clear type in base? + return doWithRealm(monarchy.realmConfiguration) { realm -> + realm.where() + .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) + .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) + .findAll() + ?.mapNotNull { timelineEventMapper.map(it).takeIf { it.root.isImageMessage() } } + ?: emptyList() + } + } } diff --git a/settings.gradle b/settings.gradle index 04307e89d9..3a7aa9ac1c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ -include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch' -include ':multipicker' +include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch', ':attachment-viewer' +include ':multipicker' \ No newline at end of file diff --git a/vector/build.gradle b/vector/build.gradle index 59ae3d35de..b409a7d8b8 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -279,6 +279,7 @@ dependencies { implementation project(":matrix-sdk-android-rx") implementation project(":diff-match-patch") implementation project(":multipicker") + implementation project(":attachment-viewer") implementation 'com.android.support:multidex:1.0.3' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" @@ -368,6 +369,10 @@ dependencies { implementation "com.github.piasy:GlideImageLoader:$big_image_viewer_version" implementation "com.github.piasy:ProgressPieIndicator:$big_image_viewer_version" implementation "com.github.piasy:GlideImageViewFactory:$big_image_viewer_version" + + // implementation 'com.github.MikeOrtiz:TouchImageView:3.0.2' + implementation 'com.github.chrisbanes:PhotoView:2.0.0' + implementation "com.github.bumptech.glide:glide:$glide_version" kapt "com.github.bumptech.glide:compiler:$glide_version" implementation 'com.danikula:videocache:2.7.1' diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index f9b78db17c..155c3bcd64 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -85,6 +85,11 @@ + + + + navigator.openImageViewer(requireActivity(), roomDetailArgs.roomId, mediaData, view) { pairs -> pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: "")) pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: "")) } diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt index eeeb55ed15..7cd7ba56e5 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt @@ -19,11 +19,13 @@ package im.vector.riotx.features.media import android.graphics.drawable.Drawable import android.net.Uri import android.os.Parcelable +import android.view.View import android.widget.ImageView import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.CustomViewTarget import com.bumptech.glide.request.target.Target import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.ORIENTATION_USE_EXIF import com.github.piasy.biv.view.BigImageView @@ -93,6 +95,25 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: .into(imageView) } + fun render(data: Data, contextView: View, target: CustomViewTarget<*, Drawable>) { + val req = if (data.elementToDecrypt != null) { + // Encrypted image + GlideApp + .with(contextView) + .load(data) + } else { + // Clear image + val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver().resolveFullSize(data.url) + GlideApp + .with(contextView) + .load(resolvedUrl) + } + + req.override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) + .fitCenter() + .into(target) + } + fun renderFitTarget(data: Data, mode: Mode, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) { val size = processSize(data, mode) @@ -122,6 +143,48 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: .into(imageView) } + fun renderThumbnailDontTransform(data: Data, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) { + + // a11y + imageView.contentDescription = data.filename + + val req = if (data.elementToDecrypt != null) { + // Encrypted image + GlideApp + .with(imageView) + .load(data) + } else { + // Clear image + val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver().resolveFullSize(data.url) + GlideApp + .with(imageView) + .load(resolvedUrl) + } + + req.listener(object : RequestListener { + override fun onLoadFailed(e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean): Boolean { + callback?.invoke(false) + return false + } + + override fun onResourceReady(resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean): Boolean { + callback?.invoke(true) + return false + } + }) + .dontTransform() + .into(imageView) + + + } + private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest { return if (data.elementToDecrypt != null) { // Encrypted image diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt index 092199759f..8a6c2f7545 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt @@ -91,6 +91,8 @@ class ImageMediaViewerActivity : VectorBaseActivity() { encryptedImageView.isVisible = false // Postpone transaction a bit until thumbnail is loaded supportPostponeEnterTransition() + + // We are not passing the exact same image that in the imageContentRenderer.renderFitTarget(mediaData, ImageContentRenderer.Mode.THUMBNAIL, imageTransitionView) { // Proceed with transaction scheduleStartPostponedTransition(imageTransitionView) diff --git a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt new file mode 100644 index 0000000000..991ecaafde --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2020 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.riotx.features.media + +import android.graphics.drawable.Drawable +import com.bumptech.glide.request.target.CustomViewTarget +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent +import im.vector.matrix.android.api.session.room.model.message.getFileUrl +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt +import im.vector.riotx.attachment_viewer.AttachmentInfo +import im.vector.riotx.attachment_viewer.AttachmentSourceProvider +import im.vector.riotx.attachment_viewer.ImageViewHolder +import javax.inject.Inject + +class RoomAttachmentProvider( + private val attachments: List, + private val initialIndex: Int, + private val imageContentRenderer: ImageContentRenderer +) : AttachmentSourceProvider { + + override fun getItemCount(): Int { + return attachments.size + } + + override fun getAttachmentInfoAt(position: Int): AttachmentInfo { + return attachments[position].let { + val content = it.root.getClearContent().toModel() as? MessageWithAttachmentContent + val data = ImageContentRenderer.Data( + eventId = it.eventId, + filename = content?.body ?: "", + mimeType = content?.mimeType, + url = content?.getFileUrl(), + elementToDecrypt = content?.encryptedFileInfo?.toElementToDecrypt(), + maxHeight = -1, + maxWidth = -1, + width = null, + height = null + ) + AttachmentInfo.Image( + content?.url ?: "", + data + ) + } + } + + override fun loadImage(holder: ImageViewHolder, info: AttachmentInfo.Image) { + (info.data as? ImageContentRenderer.Data)?.let { + imageContentRenderer.render(it, holder.touchImageView, holder.customTargetView as CustomViewTarget<*, Drawable>) + } + } +// override fun loadImage(holder: ImageViewHolder, info: AttachmentInfo.Image) { +// (info.data as? ImageContentRenderer.Data)?.let { +// imageContentRenderer.render(it, ImageContentRenderer.Mode.FULL_SIZE, holder.touchImageView) +// } +// } +} + +class RoomAttachmentProviderFactory @Inject constructor( + private val imageContentRenderer: ImageContentRenderer +) { + + fun createProvider(attachments: List, initialIndex: Int): RoomAttachmentProvider { + return RoomAttachmentProvider(attachments, initialIndex, imageContentRenderer) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt new file mode 100644 index 0000000000..2df8bfd0f6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2020 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.riotx.features.media + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +import android.view.View +import android.view.ViewTreeObserver +import androidx.core.app.ActivityCompat +import androidx.core.transition.addListener +import androidx.core.view.ViewCompat +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import androidx.transition.Transition +import im.vector.riotx.attachment_viewer.AttachmentViewerActivity +import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.di.DaggerScreenComponent +import im.vector.riotx.core.di.HasVectorInjector +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.di.VectorComponent +import im.vector.riotx.features.themes.ActivityOtherThemes +import im.vector.riotx.features.themes.ThemeUtils +import kotlinx.android.parcel.Parcelize +import timber.log.Timber +import javax.inject.Inject +import kotlin.system.measureTimeMillis + +class VectorAttachmentViewerActivity : AttachmentViewerActivity() { + + @Parcelize + data class Args( + val roomId: String?, + val eventId: String, + val sharedTransitionName: String? + ) : Parcelable + + @Inject + lateinit var sessionHolder: ActiveSessionHolder + + @Inject + lateinit var dataSourceFactory: RoomAttachmentProviderFactory + + @Inject + lateinit var imageContentRenderer: ImageContentRenderer + + private lateinit var screenComponent: ScreenComponent + + private var initialIndex = 0 + private var isAnimatingOut = false + + override fun onCreate(savedInstanceState: Bundle?) { + + super.onCreate(savedInstanceState) + Timber.i("onCreate Activity ${this.javaClass.simpleName}") + val vectorComponent = getVectorComponent() + screenComponent = DaggerScreenComponent.factory().create(vectorComponent, this) + val timeForInjection = measureTimeMillis { + screenComponent.inject(this) + } + Timber.v("Injecting dependencies into ${javaClass.simpleName} took $timeForInjection ms") + ThemeUtils.setActivityTheme(this, getOtherThemes()) + + val args = args() ?: throw IllegalArgumentException("Missing arguments") + val session = sessionHolder.getSafeActiveSession() ?: return Unit.also { finish() } + + val room = args.roomId?.let { session.getRoom(it) } + val events = room?.getAttachementMessages() ?: emptyList() + val index = events.indexOfFirst { it.eventId == args.eventId } + initialIndex = index + + + if (savedInstanceState == null && addTransitionListener()) { + args.sharedTransitionName?.let { + ViewCompat.setTransitionName(imageTransitionView, it) + transitionImageContainer.isVisible = true + + // Postpone transaction a bit until thumbnail is loaded + val mediaData: ImageContentRenderer.Data? = intent.getParcelableExtra(EXTRA_IMAGE_DATA) + if (mediaData != null) { + // will be shown at end of transition + pager2.isInvisible = true + supportPostponeEnterTransition() + imageContentRenderer.renderThumbnailDontTransform(mediaData, imageTransitionView) { + // Proceed with transaction + scheduleStartPostponedTransition(imageTransitionView) + } + } + } + } + + setSourceProvider(dataSourceFactory.createProvider(events, index)) + if (savedInstanceState == null) { + pager2.setCurrentItem(index, false) + } + + } + + private fun getOtherThemes() = ActivityOtherThemes.VectorAttachmentsPreview + + + override fun shouldAnimateDismiss(): Boolean { + return currentPosition != initialIndex + } + + override fun onBackPressed() { + if (currentPosition == initialIndex) { + // show back the transition view + // TODO, we should track and update the mapping + transitionImageContainer.isVisible = true + } + isAnimatingOut = true + super.onBackPressed() + } + + override fun animateClose() { + if (currentPosition == initialIndex) { + // show back the transition view + // TODO, we should track and update the mapping + transitionImageContainer.isVisible = true + } + isAnimatingOut = true + ActivityCompat.finishAfterTransition(this); + } + + /* ========================================================================================== + * PRIVATE METHODS + * ========================================================================================== */ + + /** + * Try and add a [Transition.TransitionListener] to the entering shared element + * [Transition]. We do this so that we can load the full-size image after the transition + * has completed. + * + * @return true if we were successful in adding a listener to the enter transition + */ + private fun addTransitionListener(): Boolean { + val transition = window.sharedElementEnterTransition + + if (transition != null) { + // There is an entering shared element transition so add a listener to it + transition.addListener( + onEnd = { + if (!isAnimatingOut) { + // The listener is also called when we are exiting + transitionImageContainer.isVisible = false + pager2.isInvisible = false + } + }, + onCancel = { + if (!isAnimatingOut) { + transitionImageContainer.isVisible = false + pager2.isInvisible = false + } + } + ) + return true + } + + // If we reach here then we have not added a listener + return false + } + + private fun args() = intent.getParcelableExtra(EXTRA_ARGS) + + + private fun getVectorComponent(): VectorComponent { + return (application as HasVectorInjector).injector() + } + + private fun scheduleStartPostponedTransition(sharedElement: View) { + sharedElement.viewTreeObserver.addOnPreDrawListener( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + sharedElement.viewTreeObserver.removeOnPreDrawListener(this) + supportStartPostponedEnterTransition() + return true + } + }) + } + + companion object { + + const val EXTRA_ARGS = "EXTRA_ARGS" + const val EXTRA_IMAGE_DATA = "EXTRA_IMAGE_DATA" + + fun newIntent(context: Context, mediaData: ImageContentRenderer.Data, roomId: String?, eventId: String, sharedTransitionName: String?) = Intent(context, VectorAttachmentViewerActivity::class.java).also { + it.putExtra(EXTRA_ARGS, Args(roomId, eventId, sharedTransitionName)) + it.putExtra(EXTRA_IMAGE_DATA, mediaData) + } + + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 0b89ab8ec4..debd58e6d2 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -49,11 +49,7 @@ import im.vector.riotx.features.home.room.detail.RoomDetailArgs import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.invite.InviteUsersToRoomActivity -import im.vector.riotx.features.media.BigImageViewerActivity -import im.vector.riotx.features.media.ImageContentRenderer -import im.vector.riotx.features.media.ImageMediaViewerActivity -import im.vector.riotx.features.media.VideoContentRenderer -import im.vector.riotx.features.media.VideoMediaViewerActivity +import im.vector.riotx.features.media.* import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity @@ -89,7 +85,8 @@ class DefaultNavigator @Inject constructor( override fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) { val session = sessionHolder.getSafeActiveSession() ?: return - val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId) ?: return + val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId) + ?: return (tx as? IncomingSasVerificationTransaction)?.performAccept() if (context is VectorBaseActivity) { VerificationBottomSheet.withArgs( @@ -216,7 +213,8 @@ class DefaultNavigator @Inject constructor( ?.let { avatarUrl -> val intent = BigImageViewerActivity.newIntent(activity, matrixItem.getBestName(), avatarUrl) val options = sharedElement?.let { - ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it) ?: "") + ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it) + ?: "") } activity.startActivity(intent, options?.toBundle()) } @@ -244,22 +242,38 @@ class DefaultNavigator @Inject constructor( context.startActivity(WidgetActivity.newIntent(context, widgetArgs)) } - override fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?) { - val intent = ImageMediaViewerActivity.newIntent(activity, mediaData, ViewCompat.getTransitionName(view)) - val pairs = ArrayList>() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let { - pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) + override fun openImageViewer(activity: Activity, roomId: String?, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?) { + VectorAttachmentViewerActivity.newIntent(activity, mediaData, roomId, mediaData.eventId, ViewCompat.getTransitionName(view)).let { intent -> + val pairs = ArrayList>() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let { + pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) + } + activity.window.decorView.findViewById(android.R.id.navigationBarBackground)?.let { + pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) + } } - activity.window.decorView.findViewById(android.R.id.navigationBarBackground)?.let { - pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) - } - } - pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: "")) - options?.invoke(pairs) + pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: "")) + options?.invoke(pairs) - val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle() - activity.startActivity(intent, bundle) + val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle() + activity.startActivity(intent, bundle) + } +// val intent = ImageMediaViewerActivity.newIntent(activity, mediaData, ViewCompat.getTransitionName(view)) +// val pairs = ArrayList>() +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { +// activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let { +// pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) +// } +// activity.window.decorView.findViewById(android.R.id.navigationBarBackground)?.let { +// pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) +// } +// } +// pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: "")) +// options?.invoke(pairs) +// +// val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle() +// activity.startActivity(intent, bundle) } override fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data) { diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index ce4d5ef3ea..54c0f55a7b 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -91,7 +91,7 @@ interface Navigator { fun openRoomWidget(context: Context, roomId: String, widget: Widget) - fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?) + fun openImageViewer(activity: Activity, roomId: String?, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?) fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data) } diff --git a/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt index 78a0cece41..e5b2f34f61 100644 --- a/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt +++ b/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt @@ -26,6 +26,7 @@ import com.tapadoo.alerter.Alerter import com.tapadoo.alerter.OnHideAlertListener import dagger.Lazy import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.themes.ThemeUtils import timber.log.Timber @@ -83,7 +84,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy + + \ No newline at end of file From 2d4a728af47898f77579b26530b5519ae5caac42 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 6 Jul 2020 09:45:42 +0200 Subject: [PATCH 024/122] Gif support --- .../AnimatedImageViewHolder.kt | 68 +++++++++++++++++++ .../AttachmentSourceProvider.kt | 4 +- .../attachment_viewer/AttachmentsAdapter.kt | 17 +++-- ...ewHolder.kt => ZoomableImageViewHolder.kt} | 2 +- .../layout/item_animated_image_attachment.xml | 22 ++++++ .../features/media/RoomAttachmentProvider.kt | 26 +++++-- 6 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AnimatedImageViewHolder.kt rename attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/{ImageViewHolder.kt => ZoomableImageViewHolder.kt} (97%) create mode 100644 attachment-viewer/src/main/res/layout/item_animated_image_attachment.xml diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AnimatedImageViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AnimatedImageViewHolder.kt new file mode 100644 index 0000000000..10b3cf8ffc --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AnimatedImageViewHolder.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020 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.riotx.attachment_viewer + +import android.graphics.drawable.Animatable +import android.graphics.drawable.Drawable +import android.util.Log +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.ProgressBar +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import com.bumptech.glide.request.target.CustomViewTarget +import com.bumptech.glide.request.transition.Transition +import com.github.chrisbanes.photoview.PhotoView + +class AnimatedImageViewHolder constructor(itemView: View) : + BaseViewHolder(itemView) { + + val touchImageView: ImageView = itemView.findViewById(R.id.imageView) + val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress) + + val customTargetView = object : CustomViewTarget(touchImageView) { + + override fun onResourceLoading(placeholder: Drawable?) { + imageLoaderProgress.isVisible = true + } + + override fun onLoadFailed(errorDrawable: Drawable?) { + imageLoaderProgress.isVisible = false + } + + override fun onResourceCleared(placeholder: Drawable?) { + touchImageView.setImageDrawable(placeholder) + } + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + imageLoaderProgress.isVisible = false + // Glide mess up the view size :/ + touchImageView.updateLayoutParams { + width = LinearLayout.LayoutParams.MATCH_PARENT + height = LinearLayout.LayoutParams.MATCH_PARENT + } + touchImageView.setImageDrawable(resource) + if (resource is Animatable) { + resource.start(); + } + } + } + + override fun bind(attachmentInfo: AttachmentInfo) { + } +} diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt index 9fd2902970..f88083f818 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt @@ -18,6 +18,7 @@ package im.vector.riotx.attachment_viewer sealed class AttachmentInfo { data class Image(val url: String, val data: Any?) : AttachmentInfo() + data class AnimatedImage(val url: String, val data: Any?) : AttachmentInfo() data class Video(val url: String, val data: Any) : AttachmentInfo() data class Audio(val url: String, val data: Any) : AttachmentInfo() data class File(val url: String, val data: Any) : AttachmentInfo() @@ -32,5 +33,6 @@ interface AttachmentSourceProvider { fun getAttachmentInfoAt(position: Int): AttachmentInfo - fun loadImage(holder: ImageViewHolder, info: AttachmentInfo.Image) + fun loadImage(holder: ZoomableImageViewHolder, info: AttachmentInfo.Image) + fun loadImage(holder: AnimatedImageViewHolder, info: AttachmentInfo.AnimatedImage) } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentsAdapter.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentsAdapter.kt index b9914e4dda..f762a6ea3e 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentsAdapter.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentsAdapter.kt @@ -60,7 +60,8 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { val inflater = LayoutInflater.from(parent.context) val itemView = inflater.inflate(viewType, parent, false) return when (viewType) { - R.layout.item_image_attachment -> ImageViewHolder(itemView) + R.layout.item_image_attachment -> ZoomableImageViewHolder(itemView) + R.layout.item_animated_image_attachment -> AnimatedImageViewHolder(itemView) else -> AttachmentViewHolder(itemView) } } @@ -70,6 +71,7 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { return when (info) { is AttachmentInfo.Image -> R.layout.item_image_attachment is AttachmentInfo.Video -> R.layout.item_video_attachment + is AttachmentInfo.AnimatedImage -> R.layout.item_animated_image_attachment is AttachmentInfo.Audio -> TODO() is AttachmentInfo.File -> TODO() } @@ -83,15 +85,22 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { attachmentSourceProvider?.getAttachmentInfoAt(position)?.let { holder.bind(it) - if (it is AttachmentInfo.Image) { - attachmentSourceProvider?.loadImage(holder as ImageViewHolder, it) + when(it) { + is AttachmentInfo.Image -> { + attachmentSourceProvider?.loadImage(holder as ZoomableImageViewHolder, it) + } + is AttachmentInfo.AnimatedImage -> { + attachmentSourceProvider?.loadImage(holder as AnimatedImageViewHolder, it) + } + else -> {} } + } } fun isScaled(position: Int): Boolean { val holder = recyclerView?.findViewHolderForAdapterPosition(position) - if (holder is ImageViewHolder) { + if (holder is ZoomableImageViewHolder) { return holder.touchImageView.attacher.scale > 1f } return false diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ImageViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ZoomableImageViewHolder.kt similarity index 97% rename from attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ImageViewHolder.kt rename to attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ZoomableImageViewHolder.kt index cac6a4fd9e..6dd387b870 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ImageViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ZoomableImageViewHolder.kt @@ -27,7 +27,7 @@ import com.bumptech.glide.request.target.CustomViewTarget import com.bumptech.glide.request.transition.Transition import com.github.chrisbanes.photoview.PhotoView -class ImageViewHolder constructor(itemView: View) : +class ZoomableImageViewHolder constructor(itemView: View) : BaseViewHolder(itemView) { val touchImageView: PhotoView = itemView.findViewById(R.id.touchImageView) diff --git a/attachment-viewer/src/main/res/layout/item_animated_image_attachment.xml b/attachment-viewer/src/main/res/layout/item_animated_image_attachment.xml new file mode 100644 index 0000000000..1096267124 --- /dev/null +++ b/attachment-viewer/src/main/res/layout/item_animated_image_attachment.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt index 991ecaafde..079c435001 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt @@ -24,9 +24,10 @@ import im.vector.matrix.android.api.session.room.model.message.MessageWithAttach import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt +import im.vector.riotx.attachment_viewer.AnimatedImageViewHolder import im.vector.riotx.attachment_viewer.AttachmentInfo import im.vector.riotx.attachment_viewer.AttachmentSourceProvider -import im.vector.riotx.attachment_viewer.ImageViewHolder +import im.vector.riotx.attachment_viewer.ZoomableImageViewHolder import javax.inject.Inject class RoomAttachmentProvider( @@ -53,14 +54,27 @@ class RoomAttachmentProvider( width = null, height = null ) - AttachmentInfo.Image( - content?.url ?: "", - data - ) + if (content?.mimeType == "image/gif") { + AttachmentInfo.AnimatedImage( + content.url ?: "", + data + ) + } else { + AttachmentInfo.Image( + content?.url ?: "", + data + ) + } } } - override fun loadImage(holder: ImageViewHolder, info: AttachmentInfo.Image) { + override fun loadImage(holder: ZoomableImageViewHolder, info: AttachmentInfo.Image) { + (info.data as? ImageContentRenderer.Data)?.let { + imageContentRenderer.render(it, holder.touchImageView, holder.customTargetView as CustomViewTarget<*, Drawable>) + } + } + + override fun loadImage(holder: AnimatedImageViewHolder, info: AttachmentInfo.AnimatedImage) { (info.data as? ImageContentRenderer.Data)?.let { imageContentRenderer.render(it, holder.touchImageView, holder.customTargetView as CustomViewTarget<*, Drawable>) } From 76133ab55e08838d59eec730d9f0d9aac8d28f55 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 6 Jul 2020 15:47:15 +0200 Subject: [PATCH 025/122] Simple overlay --- attachment-viewer/build.gradle | 4 - .../AttachmentSourceProvider.kt | 6 ++ .../AttachmentViewerActivity.kt | 49 +++++++++-- .../features/media/AttachmentOverlayView.kt | 58 +++++++++++++ .../features/media/RoomAttachmentProvider.kt | 41 ++++++++- .../media/VectorAttachmentViewerActivity.kt | 24 ++++-- .../layout/merge_image_attachment_overlay.xml | 83 +++++++++++++++++++ vector/src/main/res/values/colors_riotx.xml | 1 + 8 files changed, 244 insertions(+), 22 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt create mode 100644 vector/src/main/res/layout/merge_image_attachment_overlay.xml diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle index 7fcda7a742..ac41c3ed75 100644 --- a/attachment-viewer/build.gradle +++ b/attachment-viewer/build.gradle @@ -34,16 +34,12 @@ buildscript { android { compileSdkVersion 29 - buildToolsVersion "29.0.3" defaultConfig { minSdkVersion 21 targetSdkVersion 29 versionCode 1 versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles "consumer-rules.pro" } buildTypes { diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt index f88083f818..7b24f4bb46 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt @@ -16,6 +16,9 @@ package im.vector.riotx.attachment_viewer +import android.content.Context +import android.view.View + sealed class AttachmentInfo { data class Image(val url: String, val data: Any?) : AttachmentInfo() data class AnimatedImage(val url: String, val data: Any?) : AttachmentInfo() @@ -34,5 +37,8 @@ interface AttachmentSourceProvider { fun getAttachmentInfoAt(position: Int): AttachmentInfo fun loadImage(holder: ZoomableImageViewHolder, info: AttachmentInfo.Image) + fun loadImage(holder: AnimatedImageViewHolder, info: AttachmentInfo.AnimatedImage) + + fun overlayViewAtPosition(context: Context, position: Int) : View? } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentViewerActivity.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentViewerActivity.kt index 2d4cbff00d..4d2b4e3459 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentViewerActivity.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentViewerActivity.kt @@ -25,7 +25,9 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat import androidx.core.view.isVisible +import androidx.core.view.updatePadding import androidx.viewpager2.widget.ViewPager2 import kotlinx.android.synthetic.main.activity_attachment_viewer.* import kotlin.math.abs @@ -36,8 +38,16 @@ abstract class AttachmentViewerActivity : AppCompatActivity() { lateinit var imageTransitionView: ImageView lateinit var transitionImageContainer: ViewGroup - // TODO + var topInset = 0 + private var overlayView: View? = null + set(value) { + if (value == overlayView) return + overlayView?.let { rootContainer.removeView(it) } + rootContainer.addView(value) + value?.updatePadding(top = topInset) + field = value + } private lateinit var swipeDismissHandler: SwipeToDismissHandler private lateinit var directionDetector: SwipeDirectionDetector @@ -53,6 +63,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity() { private var wasScaled: Boolean = false private var isSwipeToDismissAllowed: Boolean = true private lateinit var attachmentsAdapter: AttachmentsAdapter + private var isOverlayWasClicked = false // private val shouldDismissToBottom: Boolean // get() = e == null @@ -67,6 +78,20 @@ abstract class AttachmentViewerActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + // This is important for the dispatchTouchEvent, if not we must correct + // the touch coordinates + window.decorView.systemUiVisibility = ( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) + +// // clear FLAG_TRANSLUCENT_STATUS flag: +// window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); +// +//// add FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS flag to the window +// window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + + setContentView(R.layout.activity_attachment_viewer) attachmentPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL attachmentsAdapter = AttachmentsAdapter() @@ -83,6 +108,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity() { override fun onPageSelected(position: Int) { currentPosition = position + overlayView = attachmentsAdapter.attachmentSourceProvider?.overlayViewAtPosition(this@AttachmentViewerActivity, position) } }) @@ -92,6 +118,13 @@ abstract class AttachmentViewerActivity : AppCompatActivity() { scaleDetector = createScaleGestureDetector() + + ViewCompat.setOnApplyWindowInsetsListener(rootContainer) { _, insets -> + overlayView?.updatePadding(top = insets.systemWindowInsetTop) + topInset = insets.systemWindowInsetTop + insets + } + } override fun dispatchTouchEvent(ev: MotionEvent): Boolean { @@ -99,9 +132,9 @@ abstract class AttachmentViewerActivity : AppCompatActivity() { // The zoomable view is configured to disallow interception when image is zoomed // Check if the overlay is visible, and wants to handle the click -// if (overlayView.isVisible && overlayView?.dispatchTouchEvent(event) == true) { -// return true -// } + if (overlayView?.isVisible == true && overlayView?.dispatchTouchEvent(ev) == true) { + return true + } Log.v("ATTACHEMENTS", "================\ndispatchTouchEvent $ev") @@ -143,14 +176,14 @@ abstract class AttachmentViewerActivity : AppCompatActivity() { attachmentPager.dispatchTouchEvent(event) swipeDismissHandler.onTouch(rootContainer, event) -// isOverlayWasClicked = dispatchOverlayTouch(event) + isOverlayWasClicked = dispatchOverlayTouch(event) } private fun handleEventActionUp(event: MotionEvent) { // wasDoubleTapped = false swipeDismissHandler.onTouch(rootContainer, event) attachmentPager.dispatchTouchEvent(event) -// isOverlayWasClicked = dispatchOverlayTouch(event) + isOverlayWasClicked = dispatchOverlayTouch(event) } private fun handleTouchIfNotScaled(event: MotionEvent): Boolean { @@ -159,7 +192,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity() { directionDetector.handleTouchEvent(event) return when (swipeDirection) { - SwipeDirection.Up, SwipeDirection.Down -> { + SwipeDirection.Up, SwipeDirection.Down -> { if (isSwipeToDismissAllowed && !wasScaled && isImagePagerIdle) { swipeDismissHandler.onTouch(rootContainer, event) } else true @@ -167,7 +200,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity() { SwipeDirection.Left, SwipeDirection.Right -> { attachmentPager.dispatchTouchEvent(event) } - else -> true + else -> true } } diff --git a/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt new file mode 100644 index 0000000000..49930fde76 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020 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.riotx.features.media + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.updateLayoutParams +import im.vector.riotx.R +import im.vector.riotx.attachment_viewer.AttachmentInfo + +class AttachmentOverlayView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + + var onShareCallback: (() -> Unit) ? = null + var onBack: (() -> Unit) ? = null + + private val counterTextView: TextView + private val infoTextView: TextView + private val shareImage: ImageView + + init { + View.inflate(context, R.layout.merge_image_attachment_overlay, this) + setBackgroundColor(Color.TRANSPARENT) + counterTextView = findViewById(R.id.overlayCounterText) + infoTextView = findViewById(R.id.overlayInfoText) + shareImage = findViewById(R.id.overlayShareButton) + + findViewById(R.id.overlayBackButton).setOnClickListener { + onBack?.invoke() + } + } + + fun updateWith(counter: String, senderInfo : String) { + counterTextView.text = counter + infoTextView.text = senderInfo + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt index 079c435001..099d4fed5d 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt @@ -16,7 +16,9 @@ package im.vector.riotx.features.media +import android.content.Context import android.graphics.drawable.Drawable +import android.view.View import com.bumptech.glide.request.target.CustomViewTarget import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.MessageContent @@ -28,14 +30,26 @@ import im.vector.riotx.attachment_viewer.AnimatedImageViewHolder import im.vector.riotx.attachment_viewer.AttachmentInfo import im.vector.riotx.attachment_viewer.AttachmentSourceProvider import im.vector.riotx.attachment_viewer.ZoomableImageViewHolder +import im.vector.riotx.core.date.VectorDateFormatter +import im.vector.riotx.core.extensions.localDateTime import javax.inject.Inject class RoomAttachmentProvider( private val attachments: List, private val initialIndex: Int, - private val imageContentRenderer: ImageContentRenderer + private val imageContentRenderer: ImageContentRenderer, + private val dateFormatter: VectorDateFormatter ) : AttachmentSourceProvider { + interface InteractionListener { + fun onDismissTapped() + fun onShareTapped() + } + + var interactionListener: InteractionListener? = null + + private var overlayView: AttachmentOverlayView? = null + override fun getItemCount(): Int { return attachments.size } @@ -79,6 +93,26 @@ class RoomAttachmentProvider( imageContentRenderer.render(it, holder.touchImageView, holder.customTargetView as CustomViewTarget<*, Drawable>) } } + + override fun overlayViewAtPosition(context: Context, position: Int): View? { + if (overlayView == null) { + overlayView = AttachmentOverlayView(context) + overlayView?.onBack = { + interactionListener?.onDismissTapped() + } + overlayView?.onShareCallback = { + interactionListener?.onShareTapped() + } + } + val item = attachments[position] + val dateString = item.root.localDateTime().let { + "${dateFormatter.formatMessageDay(it)} at ${dateFormatter.formatMessageHour(it)} " + } + overlayView?.updateWith("${position + 1} of ${attachments.size}","${item.senderInfo.displayName} $dateString" ) + return overlayView + } + + // override fun loadImage(holder: ImageViewHolder, info: AttachmentInfo.Image) { // (info.data as? ImageContentRenderer.Data)?.let { // imageContentRenderer.render(it, ImageContentRenderer.Mode.FULL_SIZE, holder.touchImageView) @@ -87,10 +121,11 @@ class RoomAttachmentProvider( } class RoomAttachmentProviderFactory @Inject constructor( - private val imageContentRenderer: ImageContentRenderer + private val imageContentRenderer: ImageContentRenderer, + private val vectorDateFormatter: VectorDateFormatter ) { fun createProvider(attachments: List, initialIndex: Int): RoomAttachmentProvider { - return RoomAttachmentProvider(attachments, initialIndex, imageContentRenderer) + return RoomAttachmentProvider(attachments, initialIndex, imageContentRenderer, vectorDateFormatter) } } diff --git a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt index 2df8bfd0f6..efc9eca517 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt @@ -22,17 +22,15 @@ import android.os.Parcelable import android.view.View import android.view.ViewTreeObserver import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat import androidx.core.transition.addListener import androidx.core.view.ViewCompat import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.transition.Transition +import im.vector.riotx.R import im.vector.riotx.attachment_viewer.AttachmentViewerActivity -import im.vector.riotx.core.di.ActiveSessionHolder -import im.vector.riotx.core.di.DaggerScreenComponent -import im.vector.riotx.core.di.HasVectorInjector -import im.vector.riotx.core.di.ScreenComponent -import im.vector.riotx.core.di.VectorComponent +import im.vector.riotx.core.di.* import im.vector.riotx.features.themes.ActivityOtherThemes import im.vector.riotx.features.themes.ThemeUtils import kotlinx.android.parcel.Parcelize @@ -40,7 +38,7 @@ import timber.log.Timber import javax.inject.Inject import kotlin.system.measureTimeMillis -class VectorAttachmentViewerActivity : AttachmentViewerActivity() { +class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmentProvider.InteractionListener { @Parcelize data class Args( @@ -103,11 +101,15 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity() { } } - setSourceProvider(dataSourceFactory.createProvider(events, index)) + val sourceProvider = dataSourceFactory.createProvider(events, index) + sourceProvider.interactionListener = this + setSourceProvider(sourceProvider) if (savedInstanceState == null) { pager2.setCurrentItem(index, false) } + window.statusBarColor = ContextCompat.getColor(this, R.color.black_alpha) + } private fun getOtherThemes() = ActivityOtherThemes.VectorAttachmentsPreview @@ -204,4 +206,12 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity() { } } + + override fun onDismissTapped() { + animateClose() + } + + override fun onShareTapped() { + TODO("Not yet implemented") + } } diff --git a/vector/src/main/res/layout/merge_image_attachment_overlay.xml b/vector/src/main/res/layout/merge_image_attachment_overlay.xml new file mode 100644 index 0000000000..07d4baedc1 --- /dev/null +++ b/vector/src/main/res/layout/merge_image_attachment_overlay.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/colors_riotx.xml b/vector/src/main/res/values/colors_riotx.xml index a9cb32c3fd..c9d1c2a223 100644 --- a/vector/src/main/res/values/colors_riotx.xml +++ b/vector/src/main/res/values/colors_riotx.xml @@ -40,6 +40,7 @@ #FF000000 #FFFFFFFF + #55000000 Ongoing conference call.\nJoin as %1$s or %2$s Voice From 8c4c909f44c14ff9fd354259f3d7dcd4fb33d48e Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 8 Jul 2020 22:27:00 +0200 Subject: [PATCH 031/122] share action --- .../features/media/AttachmentOverlayView.kt | 3 ++ .../media/VectorAttachmentViewerActivity.kt | 38 ++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt index ebd54bcd0b..05ebe17dea 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt @@ -58,6 +58,9 @@ class AttachmentOverlayView @JvmOverloads constructor( findViewById(R.id.overlayBackButton).setOnClickListener { onBack?.invoke() } + findViewById(R.id.overlayShareButton).setOnClickListener { + onShareCallback?.invoke() + } } fun updateWith(counter: String, senderInfo: String) { diff --git a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt index 4c310b9c47..44b536b2ae 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt @@ -23,11 +23,21 @@ import android.view.View import android.view.ViewTreeObserver import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import androidx.core.net.toUri import androidx.core.transition.addListener import androidx.core.view.ViewCompat import androidx.core.view.isInvisible import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle import androidx.transition.Transition +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.file.FileService +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent +import im.vector.matrix.android.api.session.room.model.message.getFileUrl +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.riotx.R import im.vector.riotx.attachmentviewer.AttachmentViewerActivity import im.vector.riotx.core.di.ActiveSessionHolder @@ -35,10 +45,13 @@ import im.vector.riotx.core.di.DaggerScreenComponent import im.vector.riotx.core.di.HasVectorInjector import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.VectorComponent +import im.vector.riotx.core.intent.getMimeTypeFromUri +import im.vector.riotx.core.utils.shareMedia import im.vector.riotx.features.themes.ActivityOtherThemes import im.vector.riotx.features.themes.ThemeUtils import kotlinx.android.parcel.Parcelize import timber.log.Timber +import java.io.File import javax.inject.Inject import kotlin.system.measureTimeMillis @@ -64,6 +77,7 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen private var initialIndex = 0 private var isAnimatingOut = false + private var eventList: List? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -81,6 +95,7 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen val room = args.roomId?.let { session.getRoom(it) } val events = room?.getAttachmentMessages() ?: emptyList() + eventList = events val index = events.indexOfFirst { it.eventId == args.eventId } initialIndex = index @@ -228,6 +243,27 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen } override fun onShareTapped() { - TODO("Not yet implemented") + // Share + eventList?.get(currentPosition)?.let { timelineEvent -> + + val messageContent = timelineEvent.root.getClearContent().toModel() + as? MessageWithAttachmentContent + ?: return@let + sessionHolder.getSafeActiveSession()?.fileService()?.downloadFile( + downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, + id = timelineEvent.eventId, + fileName = messageContent.body, + mimeType = messageContent.mimeType, + url = messageContent.getFileUrl(), + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), + callback = object : MatrixCallback { + override fun onSuccess(data: File) { + if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { + shareMedia(this@VectorAttachmentViewerActivity, data, getMimeTypeFromUri(this@VectorAttachmentViewerActivity, data.toUri())) + } + } + } + ) + } } } From e9778d6febdc57a516fd58e2b1e35bf2a00a227c Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 8 Jul 2020 22:41:17 +0200 Subject: [PATCH 032/122] Video stop/resume when paging or bg/fg --- .../AttachmentViewerActivity.kt | 9 +++++++ .../attachmentviewer/AttachmentsAdapter.kt | 13 ++++++++++ .../riotx/attachmentviewer/VideoViewHolder.kt | 25 ++++++++++++++++++- 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt index 99a90eb033..2a83ab21c7 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt @@ -143,6 +143,15 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi overlayView = attachmentsAdapter.attachmentSourceProvider?.overlayViewAtPosition(this@AttachmentViewerActivity, position) } + override fun onPause() { + attachmentsAdapter.onPause(currentPosition) + super.onPause() + } + + override fun onResume() { + super.onResume() + attachmentsAdapter.onResume(currentPosition) + } override fun dispatchTouchEvent(ev: MotionEvent): Boolean { // The zoomable view is configured to disallow interception when image is zoomed diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt index 333a1b3625..d1929f271e 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt @@ -28,6 +28,8 @@ abstract class BaseViewHolder constructor(itemView: View) : open fun onRecycled() {} open fun onAttached() {} open fun onDetached() {} + open fun entersBackground() {} + open fun entersForeground() {} open fun onSelected(selected: Boolean) {} } @@ -121,6 +123,17 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { return false } + + fun onPause(position: Int) { + val holder = recyclerView?.findViewHolderForAdapterPosition(position) as? BaseViewHolder + holder?.entersBackground() + } + + fun onResume(position: Int) { + val holder = recyclerView?.findViewHolderForAdapterPosition(position) as? BaseViewHolder + holder?.entersForeground() + + } // override fun getItemCount(): Int { // return 8 // } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt index 38b656559e..ea5fed1acb 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt @@ -26,6 +26,7 @@ import androidx.core.view.isVisible import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable +import kotlinx.coroutines.selects.select import java.io.File import java.lang.ref.WeakReference import java.util.concurrent.TimeUnit @@ -39,6 +40,7 @@ class VideoViewHolder constructor(itemView: View) : private var isSelected = false private var mVideoPath: String? = null private var progressDisposable: Disposable? = null + private var progress: Int = 0 var eventListener: WeakReference? = null @@ -89,12 +91,30 @@ class VideoViewHolder constructor(itemView: View) : } } + override fun entersBackground() { + if (videoView.isPlaying) { + progress = videoView.currentPosition + progressDisposable?.dispose() + progressDisposable = null + videoView.stopPlayback() + videoView.pause() + } + + } + + override fun entersForeground() { + onSelected(isSelected) + } + override fun onSelected(selected: Boolean) { if (!selected) { if (videoView.isPlaying) { + progress = videoView.currentPosition videoView.stopPlayback() progressDisposable?.dispose() progressDisposable = null + } else { + progress = 0 } } else { if (mVideoPath != null) { @@ -125,9 +145,12 @@ class VideoViewHolder constructor(itemView: View) : videoView.setVideoPath(mVideoPath) videoView.start() + if (progress > 0) { + videoView.seekTo(progress) + } } override fun bind(attachmentInfo: AttachmentInfo) { - Log.v("FOO", "") + progress = 0 } } From e24d5b3ca403ea42f72992c15c5dda89ff135dbe Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 8 Jul 2020 22:58:27 +0200 Subject: [PATCH 033/122] Simple play/pause overlay --- .../attachmentviewer/AttachmentEvents.kt | 6 +++- .../AttachmentViewerActivity.kt | 7 +++++ .../attachmentviewer/AttachmentsAdapter.kt | 4 +-- .../riotx/attachmentviewer/VideoViewHolder.kt | 30 ++++++++++++++----- .../features/media/AttachmentOverlayView.kt | 7 +++++ .../features/media/RoomAttachmentProvider.kt | 4 +++ .../media/VectorAttachmentViewerActivity.kt | 5 ++++ 7 files changed, 53 insertions(+), 10 deletions(-) diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentEvents.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentEvents.kt index 997790a938..5b1f2ab90d 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentEvents.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentEvents.kt @@ -21,6 +21,10 @@ sealed class AttachmentEvents { } interface AttachmentEventListener { - fun onEvent(event: AttachmentEvents) } + +sealed class AttachmentCommands { + object PauseVideo : AttachmentCommands() + object StartVideo : AttachmentCommands() +} diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt index 2a83ab21c7..6f2436f261 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt @@ -152,6 +152,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi super.onResume() attachmentsAdapter.onResume(currentPosition) } + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { // The zoomable view is configured to disallow interception when image is zoomed @@ -302,6 +303,12 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi finish() } + public fun handle(commands: AttachmentCommands) { + (attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition) as? BaseViewHolder)?.let { + it.handleCommand(commands) + } + } + private fun hideSystemUI() { systemUiVisibility = false // Enables regular immersive mode. diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt index d1929f271e..b0cb5193e8 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt @@ -31,6 +31,8 @@ abstract class BaseViewHolder constructor(itemView: View) : open fun entersBackground() {} open fun entersForeground() {} open fun onSelected(selected: Boolean) {} + + open fun handleCommand(commands: AttachmentCommands) {} } class AttachmentViewHolder constructor(itemView: View) : @@ -123,7 +125,6 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { return false } - fun onPause(position: Int) { val holder = recyclerView?.findViewHolderForAdapterPosition(position) as? BaseViewHolder holder?.entersBackground() @@ -132,7 +133,6 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { fun onResume(position: Int) { val holder = recyclerView?.findViewHolderForAdapterPosition(position) as? BaseViewHolder holder?.entersForeground() - } // override fun getItemCount(): Int { // return 8 diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt index ea5fed1acb..a2424dda57 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt @@ -26,7 +26,6 @@ import androidx.core.view.isVisible import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable -import kotlinx.coroutines.selects.select import java.io.File import java.lang.ref.WeakReference import java.util.concurrent.TimeUnit @@ -41,6 +40,7 @@ class VideoViewHolder constructor(itemView: View) : private var mVideoPath: String? = null private var progressDisposable: Disposable? = null private var progress: Int = 0 + private var wasPaused = false var eventListener: WeakReference? = null @@ -99,7 +99,6 @@ class VideoViewHolder constructor(itemView: View) : videoView.stopPlayback() videoView.pause() } - } override fun entersForeground() { @@ -111,11 +110,11 @@ class VideoViewHolder constructor(itemView: View) : if (videoView.isPlaying) { progress = videoView.currentPosition videoView.stopPlayback() - progressDisposable?.dispose() - progressDisposable = null } else { progress = 0 } + progressDisposable?.dispose() + progressDisposable = null } else { if (mVideoPath != null) { startPlaying() @@ -144,13 +143,30 @@ class VideoViewHolder constructor(itemView: View) : } videoView.setVideoPath(mVideoPath) - videoView.start() - if (progress > 0) { - videoView.seekTo(progress) + if (!wasPaused) { + videoView.start() + if (progress > 0) { + videoView.seekTo(progress) + } + } + } + + override fun handleCommand(commands: AttachmentCommands) { + if (!isSelected) return + when (commands) { + AttachmentCommands.StartVideo -> { + wasPaused = false + videoView.start() + } + AttachmentCommands.PauseVideo -> { + wasPaused = true + videoView.pause() + } } } override fun bind(attachmentInfo: AttachmentInfo) { progress = 0 + wasPaused = false } } diff --git a/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt index 05ebe17dea..a2657f7daf 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt @@ -35,6 +35,7 @@ class AttachmentOverlayView @JvmOverloads constructor( var onShareCallback: (() -> Unit)? = null var onBack: (() -> Unit)? = null + var onPlayPause: ((play: Boolean) -> Unit)? = null private val counterTextView: TextView private val infoTextView: TextView @@ -42,6 +43,8 @@ class AttachmentOverlayView @JvmOverloads constructor( private val overlayPlayPauseButton: ImageView private val overlaySeekBar: SeekBar + var isPlaying = false + val videoControlsGroup: Group init { @@ -61,6 +64,9 @@ class AttachmentOverlayView @JvmOverloads constructor( findViewById(R.id.overlayShareButton).setOnClickListener { onShareCallback?.invoke() } + findViewById(R.id.overlayPlayPauseButton).setOnClickListener { + onPlayPause?.invoke(!isPlaying) + } } fun updateWith(counter: String, senderInfo: String) { @@ -74,6 +80,7 @@ class AttachmentOverlayView @JvmOverloads constructor( overlayPlayPauseButton.setImageResource(if (!event.isPlaying) R.drawable.ic_play_arrow else R.drawable.ic_pause) val safeDuration = (if (event.duration == 0) 100 else event.duration).toFloat() val percent = ((event.progress / safeDuration) * 100f).toInt().coerceAtMost(100) + isPlaying = event.isPlaying overlaySeekBar.progress = percent } } diff --git a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt index 09459b20d1..9f6080d95f 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt @@ -57,6 +57,7 @@ class RoomAttachmentProvider( interface InteractionListener { fun onDismissTapped() fun onShareTapped() + fun onPlayPause(play: Boolean) } var interactionListener: InteractionListener? = null @@ -197,6 +198,9 @@ class RoomAttachmentProvider( overlayView?.onShareCallback = { interactionListener?.onShareTapped() } + overlayView?.onPlayPause = { play -> + interactionListener?.onPlayPause(play) + } } val item = attachments[position] val dateString = item.root.localDateTime().let { diff --git a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt index 44b536b2ae..2606f0bb76 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt @@ -39,6 +39,7 @@ import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.riotx.R +import im.vector.riotx.attachmentviewer.AttachmentCommands import im.vector.riotx.attachmentviewer.AttachmentViewerActivity import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.DaggerScreenComponent @@ -242,6 +243,10 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen animateClose() } + override fun onPlayPause(play: Boolean) { + handle(if (play) AttachmentCommands.StartVideo else AttachmentCommands.PauseVideo) + } + override fun onShareTapped() { // Share eventList?.get(currentPosition)?.let { timelineEvent -> From bf2d937ad62556232b8e8ab6bf5fdd6e6e52de02 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 9 Jul 2020 08:59:06 +0200 Subject: [PATCH 034/122] Basic video seekTo support --- .../attachmentviewer/AttachmentEvents.kt | 1 + .../AttachmentViewerActivity.kt | 7 ++--- .../riotx/attachmentviewer/VideoViewHolder.kt | 7 +++++ .../features/media/AttachmentOverlayView.kt | 31 +++++++++++++++---- .../features/media/RoomAttachmentProvider.kt | 4 +++ .../media/VectorAttachmentViewerActivity.kt | 4 +++ 6 files changed, 44 insertions(+), 10 deletions(-) diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentEvents.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentEvents.kt index 5b1f2ab90d..b2b6c9fe16 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentEvents.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentEvents.kt @@ -27,4 +27,5 @@ interface AttachmentEventListener { sealed class AttachmentCommands { object PauseVideo : AttachmentCommands() object StartVideo : AttachmentCommands() + data class SeekTo(val percentProgress: Int) : AttachmentCommands() } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt index 6f2436f261..029064e058 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt @@ -303,10 +303,9 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi finish() } - public fun handle(commands: AttachmentCommands) { - (attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition) as? BaseViewHolder)?.let { - it.handleCommand(commands) - } + fun handle(commands: AttachmentCommands) { + (attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition) as? BaseViewHolder) + ?.handleCommand(commands) } private fun hideSystemUI() { diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt index a2424dda57..5718147bab 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt @@ -162,6 +162,13 @@ class VideoViewHolder constructor(itemView: View) : wasPaused = true videoView.pause() } + is AttachmentCommands.SeekTo -> { + val duration = videoView.duration + if (duration > 0) { + val seekDuration = duration * (commands.percentProgress / 100f) + videoView.seekTo(seekDuration.toInt()) + } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt index a2657f7daf..2812b011f9 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt @@ -36,6 +36,7 @@ class AttachmentOverlayView @JvmOverloads constructor( var onShareCallback: (() -> Unit)? = null var onBack: (() -> Unit)? = null var onPlayPause: ((play: Boolean) -> Unit)? = null + var videoSeekTo: ((progress: Int) -> Unit)? = null private val counterTextView: TextView private val infoTextView: TextView @@ -47,6 +48,8 @@ class AttachmentOverlayView @JvmOverloads constructor( val videoControlsGroup: Group + var suspendSeekBarUpdate = false + init { View.inflate(context, R.layout.merge_image_attachment_overlay, this) setBackgroundColor(Color.TRANSPARENT) @@ -56,8 +59,6 @@ class AttachmentOverlayView @JvmOverloads constructor( videoControlsGroup = findViewById(R.id.overlayVideoControlsGroup) overlayPlayPauseButton = findViewById(R.id.overlayPlayPauseButton) overlaySeekBar = findViewById(R.id.overlaySeekBar) - - overlaySeekBar.isEnabled = false findViewById(R.id.overlayBackButton).setOnClickListener { onBack?.invoke() } @@ -67,6 +68,22 @@ class AttachmentOverlayView @JvmOverloads constructor( findViewById(R.id.overlayPlayPauseButton).setOnClickListener { onPlayPause?.invoke(!isPlaying) } + + overlaySeekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { + if (fromUser) { + videoSeekTo?.invoke(progress) + } + } + + override fun onStartTrackingTouch(seekBar: SeekBar?) { + suspendSeekBarUpdate = true + } + + override fun onStopTrackingTouch(seekBar: SeekBar?) { + suspendSeekBarUpdate = false + } + }) } fun updateWith(counter: String, senderInfo: String) { @@ -78,10 +95,12 @@ class AttachmentOverlayView @JvmOverloads constructor( when (event) { is AttachmentEvents.VideoEvent -> { overlayPlayPauseButton.setImageResource(if (!event.isPlaying) R.drawable.ic_play_arrow else R.drawable.ic_pause) - val safeDuration = (if (event.duration == 0) 100 else event.duration).toFloat() - val percent = ((event.progress / safeDuration) * 100f).toInt().coerceAtMost(100) - isPlaying = event.isPlaying - overlaySeekBar.progress = percent + if (!suspendSeekBarUpdate) { + val safeDuration = (if (event.duration == 0) 100 else event.duration).toFloat() + val percent = ((event.progress / safeDuration) * 100f).toInt().coerceAtMost(100) + isPlaying = event.isPlaying + overlaySeekBar.progress = percent + } } } } diff --git a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt index 9f6080d95f..4e30e0179a 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt @@ -58,6 +58,7 @@ class RoomAttachmentProvider( fun onDismissTapped() fun onShareTapped() fun onPlayPause(play: Boolean) + fun videoSeekTo(percent: Int) } var interactionListener: InteractionListener? = null @@ -201,6 +202,9 @@ class RoomAttachmentProvider( overlayView?.onPlayPause = { play -> interactionListener?.onPlayPause(play) } + overlayView?.videoSeekTo = { percent -> + interactionListener?.videoSeekTo(percent) + } } val item = attachments[position] val dateString = item.root.localDateTime().let { diff --git a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt index 2606f0bb76..10483f3fa9 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt @@ -247,6 +247,10 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen handle(if (play) AttachmentCommands.StartVideo else AttachmentCommands.PauseVideo) } + override fun videoSeekTo(percent: Int) { + handle(AttachmentCommands.SeekTo(percent)) + } + override fun onShareTapped() { // Share eventList?.get(currentPosition)?.let { timelineEvent -> From aa3e68f3fd433099e10c1ef8f4818e3ac48ba9b6 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 9 Jul 2020 10:08:55 +0200 Subject: [PATCH 035/122] Refactoring Remove glide dependency + protect against cell reuse bugs --- attachment-viewer/build.gradle | 2 - .../AnimatedImageViewHolder.kt | 38 +------ .../AttachmentSourceProvider.kt | 25 ++--- .../attachmentviewer/AttachmentsAdapter.kt | 75 ++++--------- .../attachmentviewer/ImageLoaderTarget.kt | 103 ++++++++++++++++++ .../attachmentviewer/VideoLoaderTarget.kt | 76 +++++++++++++ .../riotx/attachmentviewer/VideoViewHolder.kt | 33 +----- .../ZoomableImageViewHolder.kt | 34 +----- .../features/media/RoomAttachmentProvider.kt | 82 +++++++++----- 9 files changed, 279 insertions(+), 189 deletions(-) create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ImageLoaderTarget.kt create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoLoaderTarget.kt diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle index 6b64e661fa..3a5c3298d4 100644 --- a/attachment-viewer/build.gradle +++ b/attachment-viewer/build.gradle @@ -58,9 +58,7 @@ android { } dependencies { -// implementation 'com.github.MikeOrtiz:TouchImageView:3.0.2' implementation 'com.github.chrisbanes:PhotoView:2.0.0' - implementation "com.github.bumptech.glide:glide:4.10.0" implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AnimatedImageViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AnimatedImageViewHolder.kt index 9f512e78be..f00a4eff30 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AnimatedImageViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AnimatedImageViewHolder.kt @@ -16,16 +16,9 @@ package im.vector.riotx.attachmentviewer -import android.graphics.drawable.Animatable -import android.graphics.drawable.Drawable import android.view.View import android.widget.ImageView -import android.widget.LinearLayout import android.widget.ProgressBar -import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams -import com.bumptech.glide.request.target.CustomViewTarget -import com.bumptech.glide.request.transition.Transition class AnimatedImageViewHolder constructor(itemView: View) : BaseViewHolder(itemView) { @@ -33,34 +26,5 @@ class AnimatedImageViewHolder constructor(itemView: View) : val touchImageView: ImageView = itemView.findViewById(R.id.imageView) val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress) - val customTargetView = object : CustomViewTarget(touchImageView) { - - override fun onResourceLoading(placeholder: Drawable?) { - imageLoaderProgress.isVisible = true - } - - override fun onLoadFailed(errorDrawable: Drawable?) { - imageLoaderProgress.isVisible = false - } - - override fun onResourceCleared(placeholder: Drawable?) { - touchImageView.setImageDrawable(placeholder) - } - - override fun onResourceReady(resource: Drawable, transition: Transition?) { - imageLoaderProgress.isVisible = false - // Glide mess up the view size :/ - touchImageView.updateLayoutParams { - width = LinearLayout.LayoutParams.MATCH_PARENT - height = LinearLayout.LayoutParams.MATCH_PARENT - } - touchImageView.setImageDrawable(resource) - if (resource is Animatable) { - resource.start() - } - } - } - - override fun bind(attachmentInfo: AttachmentInfo) { - } + internal val target = DefaultImageLoaderTarget(this, this.touchImageView) } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentSourceProvider.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentSourceProvider.kt index 930fc62658..ce725afec2 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentSourceProvider.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentSourceProvider.kt @@ -19,15 +19,12 @@ package im.vector.riotx.attachmentviewer import android.content.Context import android.view.View -sealed class AttachmentInfo { - data class Image(val url: String, val data: Any?) : AttachmentInfo() - data class AnimatedImage(val url: String, val data: Any?) : AttachmentInfo() - data class Video(val url: String, val data: Any, val thumbnail: Image?) : AttachmentInfo() - data class Audio(val url: String, val data: Any) : AttachmentInfo() - data class File(val url: String, val data: Any) : AttachmentInfo() - - fun bind() { - } +sealed class AttachmentInfo(open val uid: String) { + data class Image(override val uid: String, val url: String, val data: Any?) : AttachmentInfo(uid) + data class AnimatedImage(override val uid: String, val url: String, val data: Any?) : AttachmentInfo(uid) + data class Video(override val uid: String, val url: String, val data: Any, val thumbnail: Image?) : AttachmentInfo(uid) + data class Audio(override val uid: String, val url: String, val data: Any) : AttachmentInfo(uid) + data class File(override val uid: String, val url: String, val data: Any) : AttachmentInfo(uid) } interface AttachmentSourceProvider { @@ -36,11 +33,13 @@ interface AttachmentSourceProvider { fun getAttachmentInfoAt(position: Int): AttachmentInfo - fun loadImage(holder: ZoomableImageViewHolder, info: AttachmentInfo.Image) + fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.Image) - fun loadImage(holder: AnimatedImageViewHolder, info: AttachmentInfo.AnimatedImage) + fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.AnimatedImage) - fun loadVideo(holder: VideoViewHolder, info: AttachmentInfo.Video) + fun loadVideo(target: VideoLoaderTarget, info: AttachmentInfo.Video) - fun overlayViewAtPosition(context: Context, position: Int) : View? + fun overlayViewAtPosition(context: Context, position: Int): View? + + fun clear(id: String) } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt index b0cb5193e8..2f453b58a8 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt @@ -24,8 +24,10 @@ import androidx.recyclerview.widget.RecyclerView abstract class BaseViewHolder constructor(itemView: View) : RecyclerView.ViewHolder(itemView) { - open fun bind(attachmentInfo: AttachmentInfo) {} - open fun onRecycled() {} + open fun onRecycled() { + boundResourceUid = null + } + open fun onAttached() {} open fun onDetached() {} open fun entersBackground() {} @@ -33,16 +35,17 @@ abstract class BaseViewHolder constructor(itemView: View) : open fun onSelected(selected: Boolean) {} open fun handleCommand(commands: AttachmentCommands) {} -} -class AttachmentViewHolder constructor(itemView: View) : - BaseViewHolder(itemView) { + var boundResourceUid: String? = null - override fun bind(attachmentInfo: AttachmentInfo) { + open fun bind(attachmentInfo: AttachmentInfo) { + boundResourceUid = attachmentInfo.uid } } -// class AttachmentsAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : FragmentStateAdapter(fragmentManager, lifecycle) { +class AttachmentViewHolder constructor(itemView: View) : + BaseViewHolder(itemView) + class AttachmentsAdapter() : RecyclerView.Adapter() { var attachmentSourceProvider: AttachmentSourceProvider? = null @@ -65,21 +68,21 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { val inflater = LayoutInflater.from(parent.context) val itemView = inflater.inflate(viewType, parent, false) return when (viewType) { - R.layout.item_image_attachment -> ZoomableImageViewHolder(itemView) + R.layout.item_image_attachment -> ZoomableImageViewHolder(itemView) R.layout.item_animated_image_attachment -> AnimatedImageViewHolder(itemView) - R.layout.item_video_attachment -> VideoViewHolder(itemView) - else -> AttachmentViewHolder(itemView) + R.layout.item_video_attachment -> VideoViewHolder(itemView) + else -> AttachmentViewHolder(itemView) } } override fun getItemViewType(position: Int): Int { val info = attachmentSourceProvider!!.getAttachmentInfoAt(position) return when (info) { - is AttachmentInfo.Image -> R.layout.item_image_attachment - is AttachmentInfo.Video -> R.layout.item_video_attachment + is AttachmentInfo.Image -> R.layout.item_image_attachment + is AttachmentInfo.Video -> R.layout.item_video_attachment is AttachmentInfo.AnimatedImage -> R.layout.item_animated_image_attachment - is AttachmentInfo.Audio -> TODO() - is AttachmentInfo.File -> TODO() + is AttachmentInfo.Audio -> TODO() + is AttachmentInfo.File -> TODO() } } @@ -91,16 +94,17 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { attachmentSourceProvider?.getAttachmentInfoAt(position)?.let { holder.bind(it) when (it) { - is AttachmentInfo.Image -> { - attachmentSourceProvider?.loadImage(holder as ZoomableImageViewHolder, it) + is AttachmentInfo.Image -> { + attachmentSourceProvider?.loadImage((holder as ZoomableImageViewHolder).target, it) } is AttachmentInfo.AnimatedImage -> { - attachmentSourceProvider?.loadImage(holder as AnimatedImageViewHolder, it) + attachmentSourceProvider?.loadImage((holder as AnimatedImageViewHolder).target, it) } - is AttachmentInfo.Video -> { - attachmentSourceProvider?.loadVideo(holder as VideoViewHolder, it) + is AttachmentInfo.Video -> { + attachmentSourceProvider?.loadVideo((holder as VideoViewHolder).target, it) + } + else -> { } - else -> {} } } } @@ -134,35 +138,4 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { val holder = recyclerView?.findViewHolderForAdapterPosition(position) as? BaseViewHolder holder?.entersForeground() } -// override fun getItemCount(): Int { -// return 8 -// } -// -// override fun createFragment(position: Int): Fragment { -// // Return a NEW fragment instance in createFragment(int) -// val fragment = DemoObjectFragment() -// fragment.arguments = Bundle().apply { -// // Our object is just an integer :-P -// putInt(ARG_OBJECT, position + 1) -// } -// return fragment -// } } - -// private const val ARG_OBJECT = "object" -// -// // Instances of this class are fragments representing a single -// // object in our collection. -// class DemoObjectFragment : Fragment() { -// -// override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { -// return inflater.inflate(R.layout.view_image_attachment, container, false) -// } -// -// override fun onViewCreated(view: View, savedInstanceState: Bundle?) { -// arguments?.takeIf { it.containsKey(ARG_OBJECT) }?.apply { -// val textView: TextView = view.findViewById(R.id.testPage) -// textView.text = getInt(ARG_OBJECT).toString() -// } -// } -// } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ImageLoaderTarget.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ImageLoaderTarget.kt new file mode 100644 index 0000000000..bb59c9e01e --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ImageLoaderTarget.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2020 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.riotx.attachmentviewer + +import android.graphics.drawable.Animatable +import android.graphics.drawable.Drawable +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams + +interface ImageLoaderTarget { + + fun contextView(): ImageView + + fun onResourceLoading(uid: String, placeholder: Drawable?) + + fun onLoadFailed(uid: String, errorDrawable: Drawable?) + + fun onResourceCleared(uid: String, placeholder: Drawable?) + + fun onResourceReady(uid: String, resource: Drawable) +} + +internal class DefaultImageLoaderTarget(val holder: AnimatedImageViewHolder, private val contextView: ImageView) + : ImageLoaderTarget { + override fun contextView(): ImageView { + return contextView + } + + override fun onResourceLoading(uid: String, placeholder: Drawable?) { + if (holder.boundResourceUid != uid) return + holder.imageLoaderProgress.isVisible = true + } + + override fun onLoadFailed(uid: String, errorDrawable: Drawable?) { + if (holder.boundResourceUid != uid) return + holder.imageLoaderProgress.isVisible = false + } + + override fun onResourceCleared(uid: String, placeholder: Drawable?) { + if (holder.boundResourceUid != uid) return + holder.touchImageView.setImageDrawable(placeholder) + } + + override fun onResourceReady(uid: String, resource: Drawable) { + if (holder.boundResourceUid != uid) return + holder.imageLoaderProgress.isVisible = false + // Glide mess up the view size :/ + holder.touchImageView.updateLayoutParams { + width = LinearLayout.LayoutParams.MATCH_PARENT + height = LinearLayout.LayoutParams.MATCH_PARENT + } + holder.touchImageView.setImageDrawable(resource) + if (resource is Animatable) { + resource.start() + } + } + + internal class ZoomableImageTarget(val holder: ZoomableImageViewHolder, private val contextView: ImageView) : ImageLoaderTarget { + override fun contextView() = contextView + + override fun onResourceLoading(uid: String, placeholder: Drawable?) { + if (holder.boundResourceUid != uid) return + holder.imageLoaderProgress.isVisible = true + } + + override fun onLoadFailed(uid: String, errorDrawable: Drawable?) { + if (holder.boundResourceUid != uid) return + holder.imageLoaderProgress.isVisible = false + } + + override fun onResourceCleared(uid: String, placeholder: Drawable?) { + if (holder.boundResourceUid != uid) return + holder.touchImageView.setImageDrawable(placeholder) + } + + override fun onResourceReady(uid: String, resource: Drawable) { + if (holder.boundResourceUid != uid) return + holder.imageLoaderProgress.isVisible = false + // Glide mess up the view size :/ + holder.touchImageView.updateLayoutParams { + width = LinearLayout.LayoutParams.MATCH_PARENT + height = LinearLayout.LayoutParams.MATCH_PARENT + } + holder.touchImageView.setImageDrawable(resource) + } + } +} diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoLoaderTarget.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoLoaderTarget.kt new file mode 100644 index 0000000000..548c6431e5 --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoLoaderTarget.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2020 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.riotx.attachmentviewer + +import android.graphics.drawable.Drawable +import android.widget.ImageView +import androidx.core.view.isVisible +import java.io.File + +interface VideoLoaderTarget { + fun contextView(): ImageView + + fun onThumbnailResourceLoading(uid: String, placeholder: Drawable?) + + fun onThumbnailLoadFailed(uid: String, errorDrawable: Drawable?) + + fun onThumbnailResourceCleared(uid: String, placeholder: Drawable?) + + fun onThumbnailResourceReady(uid: String, resource: Drawable) + + fun onVideoFileLoading(uid: String) + fun onVideoFileLoadFailed(uid: String) + fun onVideoFileReady(uid: String, file: File) +} + +internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val contextView: ImageView) : VideoLoaderTarget { + override fun contextView(): ImageView = contextView + + override fun onThumbnailResourceLoading(uid: String, placeholder: Drawable?) { + } + + override fun onThumbnailLoadFailed(uid: String, errorDrawable: Drawable?) { + } + + override fun onThumbnailResourceCleared(uid: String, placeholder: Drawable?) { + } + + override fun onThumbnailResourceReady(uid: String, resource: Drawable) { + if (holder.boundResourceUid != uid) return + holder.thumbnailImage.setImageDrawable(resource) + } + + override fun onVideoFileLoading(uid: String) { + if (holder.boundResourceUid != uid) return + holder.thumbnailImage.isVisible = true + holder.loaderProgressBar.isVisible = true + holder.videoView.isVisible = false + } + + override fun onVideoFileLoadFailed(uid: String) { + if (holder.boundResourceUid != uid) return + holder.videoFileLoadError() + } + + override fun onVideoFileReady(uid: String, file: File) { + if (holder.boundResourceUid != uid) return + holder.thumbnailImage.isVisible = false + holder.loaderProgressBar.isVisible = false + holder.videoView.isVisible = true + holder.videoReady(file) + } +} diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt index 5718147bab..2b417baecc 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt @@ -44,38 +44,13 @@ class VideoViewHolder constructor(itemView: View) : var eventListener: WeakReference? = null -// interface Target { -// fun onResourceLoading(progress: Int, total: Int) -// fun onLoadFailed() -// fun onResourceReady(file: File) -// fun onThumbnailReady(thumbnail: Drawable?) -// } - - init { - } - val thumbnailImage: ImageView = itemView.findViewById(R.id.videoThumbnailImage) val videoView: VideoView = itemView.findViewById(R.id.videoView) val loaderProgressBar: ProgressBar = itemView.findViewById(R.id.videoLoaderProgress) val videoControlIcon: ImageView = itemView.findViewById(R.id.videoControlIcon) val errorTextView: TextView = itemView.findViewById(R.id.videoMediaViewerErrorView) -// val videoTarget = object : Target { -// override fun onResourceLoading(progress: Int, total: Int) { -// videoView.isVisible = false -// loaderProgressBar.isVisible = true -// } -// -// override fun onLoadFailed() { -// loaderProgressBar.isVisible = false -// } -// -// override fun onResourceReady(file: File) { -// } -// -// override fun onThumbnailReady(thumbnail: Drawable?) { -// } -// } + internal val target = DefaultVideoLoaderTarget(this, thumbnailImage) override fun onRecycled() { super.onRecycled() @@ -91,6 +66,9 @@ class VideoViewHolder constructor(itemView: View) : } } + fun videoFileLoadError() { + } + override fun entersBackground() { if (videoView.isPlaying) { progress = videoView.currentPosition @@ -162,7 +140,7 @@ class VideoViewHolder constructor(itemView: View) : wasPaused = true videoView.pause() } - is AttachmentCommands.SeekTo -> { + is AttachmentCommands.SeekTo -> { val duration = videoView.duration if (duration > 0) { val seekDuration = duration * (commands.percentProgress / 100f) @@ -173,6 +151,7 @@ class VideoViewHolder constructor(itemView: View) : } override fun bind(attachmentInfo: AttachmentInfo) { + super.bind(attachmentInfo) progress = 0 wasPaused = false } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ZoomableImageViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ZoomableImageViewHolder.kt index 00a8ad275a..aeaf612bbc 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ZoomableImageViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ZoomableImageViewHolder.kt @@ -16,15 +16,9 @@ package im.vector.riotx.attachmentviewer -import android.graphics.drawable.Drawable import android.util.Log import android.view.View -import android.widget.LinearLayout import android.widget.ProgressBar -import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams -import com.bumptech.glide.request.target.CustomViewTarget -import com.bumptech.glide.request.transition.Transition import com.github.chrisbanes.photoview.PhotoView class ZoomableImageViewHolder constructor(itemView: View) : @@ -45,31 +39,5 @@ class ZoomableImageViewHolder constructor(itemView: View) : touchImageView.setAllowParentInterceptOnEdge(true) } - val customTargetView = object : CustomViewTarget(touchImageView) { - - override fun onResourceLoading(placeholder: Drawable?) { - imageLoaderProgress.isVisible = true - } - - override fun onLoadFailed(errorDrawable: Drawable?) { - imageLoaderProgress.isVisible = false - } - - override fun onResourceCleared(placeholder: Drawable?) { - touchImageView.setImageDrawable(placeholder) - } - - override fun onResourceReady(resource: Drawable, transition: Transition?) { - imageLoaderProgress.isVisible = false - // Glide mess up the view size :/ - touchImageView.updateLayoutParams { - width = LinearLayout.LayoutParams.MATCH_PARENT - height = LinearLayout.LayoutParams.MATCH_PARENT - } - touchImageView.setImageDrawable(resource) - } - } - - override fun bind(attachmentInfo: AttachmentInfo) { - } + internal val target = DefaultImageLoaderTarget.ZoomableImageTarget(this, touchImageView) } diff --git a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt index 4e30e0179a..f7299bf714 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt @@ -35,11 +35,10 @@ import im.vector.matrix.android.api.session.room.model.message.MessageWithAttach import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt -import im.vector.riotx.attachmentviewer.AnimatedImageViewHolder import im.vector.riotx.attachmentviewer.AttachmentInfo import im.vector.riotx.attachmentviewer.AttachmentSourceProvider -import im.vector.riotx.attachmentviewer.VideoViewHolder -import im.vector.riotx.attachmentviewer.ZoomableImageViewHolder +import im.vector.riotx.attachmentviewer.ImageLoaderTarget +import im.vector.riotx.attachmentviewer.VideoLoaderTarget import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.extensions.localDateTime import java.io.File @@ -86,13 +85,15 @@ class RoomAttachmentProvider( ) if (content.mimeType == "image/gif") { AttachmentInfo.AnimatedImage( - content.url ?: "", - data + uid = it.eventId, + url = content.url ?: "", + data = data ) } else { AttachmentInfo.Image( - content.url ?: "", - data + uid = it.eventId, + url = content.url ?: "", + data = data ) } } else if (content is MessageVideoContent) { @@ -117,9 +118,11 @@ class RoomAttachmentProvider( thumbnailMediaData = thumbnailData ) AttachmentInfo.Video( - content.getFileUrl() ?: "", - data, - AttachmentInfo.Image( + uid = it.eventId, + url = content.getFileUrl() ?: "", + data = data, + thumbnail = AttachmentInfo.Image( + uid = it.eventId, url = content.videoInfo?.thumbnailFile?.url ?: content.videoInfo?.thumbnailUrl ?: "", data = thumbnailData @@ -128,49 +131,72 @@ class RoomAttachmentProvider( ) } else { AttachmentInfo.Image( - "", - null + uid = it.eventId, + url = "", + data = null ) } } } - override fun loadImage(holder: ZoomableImageViewHolder, info: AttachmentInfo.Image) { + override fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.Image) { (info.data as? ImageContentRenderer.Data)?.let { - imageContentRenderer.render(it, holder.touchImageView, holder.customTargetView as CustomViewTarget<*, Drawable>) + imageContentRenderer.render(it, target.contextView(), object : CustomViewTarget(target.contextView()) { + override fun onLoadFailed(errorDrawable: Drawable?) { + target.onLoadFailed(info.uid, errorDrawable) + } + + override fun onResourceCleared(placeholder: Drawable?) { + target.onResourceCleared(info.uid, placeholder) + } + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + target.onResourceReady(info.uid, resource) + } + }) } } - override fun loadImage(holder: AnimatedImageViewHolder, info: AttachmentInfo.AnimatedImage) { + override fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.AnimatedImage) { (info.data as? ImageContentRenderer.Data)?.let { - imageContentRenderer.render(it, holder.touchImageView, holder.customTargetView as CustomViewTarget<*, Drawable>) + imageContentRenderer.render(it, target.contextView(), object : CustomViewTarget(target.contextView()) { + override fun onLoadFailed(errorDrawable: Drawable?) { + target.onLoadFailed(info.uid, errorDrawable) + } + + override fun onResourceCleared(placeholder: Drawable?) { + target.onResourceCleared(info.uid, placeholder) + } + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + target.onResourceReady(info.uid, resource) + } + }) } } - override fun loadVideo(holder: VideoViewHolder, info: AttachmentInfo.Video) { + override fun loadVideo(target: VideoLoaderTarget, info: AttachmentInfo.Video) { val data = info.data as? VideoContentRenderer.Data ?: return // videoContentRenderer.render(data, // holder.thumbnailImage, // holder.loaderProgressBar, // holder.videoView, // holder.errorTextView) - imageContentRenderer.render(data.thumbnailMediaData, holder.thumbnailImage, object : CustomViewTarget(holder.thumbnailImage) { + imageContentRenderer.render(data.thumbnailMediaData, target.contextView(), object : CustomViewTarget(target.contextView()) { override fun onLoadFailed(errorDrawable: Drawable?) { - holder.thumbnailImage.setImageDrawable(errorDrawable) + target.onThumbnailLoadFailed(info.uid, errorDrawable) } override fun onResourceCleared(placeholder: Drawable?) { + target.onThumbnailResourceCleared(info.uid, placeholder) } override fun onResourceReady(resource: Drawable, transition: Transition?) { - holder.thumbnailImage.setImageDrawable(resource) + target.onThumbnailResourceReady(info.uid, resource) } }) - holder.thumbnailImage.isVisible = false - holder.loaderProgressBar.isVisible = false - holder.videoView.isVisible = false - + target.onVideoFileLoading(info.uid) fileService.downloadFile( downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, id = data.eventId, @@ -180,11 +206,11 @@ class RoomAttachmentProvider( url = data.url, callback = object : MatrixCallback { override fun onSuccess(data: File) { - holder.videoReady(data) + target.onVideoFileReady(info.uid, data) } override fun onFailure(failure: Throwable) { - holder.videoView.isVisible = false + target.onVideoFileLoadFailed(info.uid) } } ) @@ -214,6 +240,10 @@ class RoomAttachmentProvider( overlayView?.videoControlsGroup?.isVisible = item.root.isVideoMessage() return overlayView } + + override fun clear(id: String) { + // TODO("Not yet implemented") + } } class RoomAttachmentProviderFactory @Inject constructor( From 868d9cf55c06ac786d407ec4fce460fb308dfcaa Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 9 Jul 2020 10:11:10 +0200 Subject: [PATCH 036/122] Cleaning (remove audio and file as not supported yet) --- .../AttachmentSourceProvider.kt | 4 +- .../attachmentviewer/AttachmentsAdapter.kt | 38 +++------------- .../riotx/attachmentviewer/BaseViewHolder.kt | 45 +++++++++++++++++++ 3 files changed, 53 insertions(+), 34 deletions(-) create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/BaseViewHolder.kt diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentSourceProvider.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentSourceProvider.kt index ce725afec2..92a4f1d9e4 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentSourceProvider.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentSourceProvider.kt @@ -23,8 +23,8 @@ sealed class AttachmentInfo(open val uid: String) { data class Image(override val uid: String, val url: String, val data: Any?) : AttachmentInfo(uid) data class AnimatedImage(override val uid: String, val url: String, val data: Any?) : AttachmentInfo(uid) data class Video(override val uid: String, val url: String, val data: Any, val thumbnail: Image?) : AttachmentInfo(uid) - data class Audio(override val uid: String, val url: String, val data: Any) : AttachmentInfo(uid) - data class File(override val uid: String, val url: String, val data: Any) : AttachmentInfo(uid) +// data class Audio(override val uid: String, val url: String, val data: Any) : AttachmentInfo(uid) +// data class File(override val uid: String, val url: String, val data: Any) : AttachmentInfo(uid) } interface AttachmentSourceProvider { diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt index 2f453b58a8..90020f2cb0 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt @@ -17,36 +17,10 @@ package im.vector.riotx.attachmentviewer import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -abstract class BaseViewHolder constructor(itemView: View) : - RecyclerView.ViewHolder(itemView) { - - open fun onRecycled() { - boundResourceUid = null - } - - open fun onAttached() {} - open fun onDetached() {} - open fun entersBackground() {} - open fun entersForeground() {} - open fun onSelected(selected: Boolean) {} - - open fun handleCommand(commands: AttachmentCommands) {} - - var boundResourceUid: String? = null - - open fun bind(attachmentInfo: AttachmentInfo) { - boundResourceUid = attachmentInfo.uid - } -} - -class AttachmentViewHolder constructor(itemView: View) : - BaseViewHolder(itemView) - -class AttachmentsAdapter() : RecyclerView.Adapter() { +class AttachmentsAdapter : RecyclerView.Adapter() { var attachmentSourceProvider: AttachmentSourceProvider? = null set(value) { @@ -71,7 +45,7 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { R.layout.item_image_attachment -> ZoomableImageViewHolder(itemView) R.layout.item_animated_image_attachment -> AnimatedImageViewHolder(itemView) R.layout.item_video_attachment -> VideoViewHolder(itemView) - else -> AttachmentViewHolder(itemView) + else -> UnsupportedViewHolder(itemView) } } @@ -81,8 +55,8 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { is AttachmentInfo.Image -> R.layout.item_image_attachment is AttachmentInfo.Video -> R.layout.item_video_attachment is AttachmentInfo.AnimatedImage -> R.layout.item_animated_image_attachment - is AttachmentInfo.Audio -> TODO() - is AttachmentInfo.File -> TODO() +// is AttachmentInfo.Audio -> TODO() +// is AttachmentInfo.File -> TODO() } } @@ -103,8 +77,8 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { is AttachmentInfo.Video -> { attachmentSourceProvider?.loadVideo((holder as VideoViewHolder).target, it) } - else -> { - } +// else -> { +//// } } } } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/BaseViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/BaseViewHolder.kt new file mode 100644 index 0000000000..49b47c11ff --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/BaseViewHolder.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 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.riotx.attachmentviewer + +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +abstract class BaseViewHolder constructor(itemView: View) : + RecyclerView.ViewHolder(itemView) { + + open fun onRecycled() { + boundResourceUid = null + } + + open fun onAttached() {} + open fun onDetached() {} + open fun entersBackground() {} + open fun entersForeground() {} + open fun onSelected(selected: Boolean) {} + + open fun handleCommand(commands: AttachmentCommands) {} + + var boundResourceUid: String? = null + + open fun bind(attachmentInfo: AttachmentInfo) { + boundResourceUid = attachmentInfo.uid + } +} + +class UnsupportedViewHolder constructor(itemView: View) : + BaseViewHolder(itemView) From e38cb7c1a6ced6b7933d14bc0803ee26ce0a3b77 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 9 Jul 2020 10:16:38 +0200 Subject: [PATCH 037/122] Unwanted logs --- .../AttachmentViewerActivity.kt | 19 +++++++++---------- .../attachmentviewer/AttachmentsAdapter.kt | 2 +- .../riotx/attachmentviewer/VideoViewHolder.kt | 3 +-- .../ZoomableImageViewHolder.kt | 3 +-- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt index 029064e058..d6cf7c606a 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt @@ -18,7 +18,6 @@ package im.vector.riotx.attachmentviewer import android.graphics.Color import android.os.Bundle -import android.util.Log import android.view.GestureDetector import android.view.MotionEvent import android.view.ScaleGestureDetector @@ -161,26 +160,26 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi return true } - Log.v("ATTACHEMENTS", "================\ndispatchTouchEvent $ev") + // Log.v("ATTACHEMENTS", "================\ndispatchTouchEvent $ev") handleUpDownEvent(ev) - Log.v("ATTACHEMENTS", "scaleDetector is in progress ${scaleDetector.isInProgress}") - Log.v("ATTACHEMENTS", "pointerCount ${ev.pointerCount}") - Log.v("ATTACHEMENTS", "wasScaled $wasScaled") + // Log.v("ATTACHEMENTS", "scaleDetector is in progress ${scaleDetector.isInProgress}") + // Log.v("ATTACHEMENTS", "pointerCount ${ev.pointerCount}") + // Log.v("ATTACHEMENTS", "wasScaled $wasScaled") if (swipeDirection == null && (scaleDetector.isInProgress || ev.pointerCount > 1 || wasScaled)) { wasScaled = true - Log.v("ATTACHEMENTS", "dispatch to pager") +// Log.v("ATTACHEMENTS", "dispatch to pager") return attachmentPager.dispatchTouchEvent(ev) } - Log.v("ATTACHEMENTS", "is current item scaled ${isScaled()}") + // Log.v("ATTACHEMENTS", "is current item scaled ${isScaled()}") return (if (isScaled()) super.dispatchTouchEvent(ev) else handleTouchIfNotScaled(ev)).also { - Log.v("ATTACHEMENTS", "\n================") +// Log.v("ATTACHEMENTS", "\n================") } } private fun handleUpDownEvent(event: MotionEvent) { - Log.v("ATTACHEMENTS", "handleUpDownEvent $event") + // Log.v("ATTACHEMENTS", "handleUpDownEvent $event") if (event.action == MotionEvent.ACTION_UP) { handleEventActionUp(event) } @@ -232,7 +231,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi } private fun handleTouchIfNotScaled(event: MotionEvent): Boolean { - Log.v("ATTACHEMENTS", "handleTouchIfNotScaled $event") +// Log.v("ATTACHEMENTS", "handleTouchIfNotScaled $event") directionDetector.handleTouchEvent(event) return when (swipeDirection) { diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt index 90020f2cb0..27bdfdc91d 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt @@ -78,7 +78,7 @@ class AttachmentsAdapter : RecyclerView.Adapter() { attachmentSourceProvider?.loadVideo((holder as VideoViewHolder).target, it) } // else -> { -//// } +// // } } } } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt index 2b417baecc..e1a5a9864f 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt @@ -16,7 +16,6 @@ package im.vector.riotx.attachmentviewer -import android.util.Log import android.view.View import android.widget.ImageView import android.widget.ProgressBar @@ -115,7 +114,7 @@ class VideoViewHolder constructor(itemView: View) : val duration = videoView.duration val progress = videoView.currentPosition val isPlaying = videoView.isPlaying - Log.v("FOO", "isPlaying $isPlaying $progress/$duration") +// Log.v("FOO", "isPlaying $isPlaying $progress/$duration") eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration)) } } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ZoomableImageViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ZoomableImageViewHolder.kt index aeaf612bbc..3eb06e4c27 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ZoomableImageViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ZoomableImageViewHolder.kt @@ -16,7 +16,6 @@ package im.vector.riotx.attachmentviewer -import android.util.Log import android.view.View import android.widget.ProgressBar import com.github.chrisbanes.photoview.PhotoView @@ -30,7 +29,7 @@ class ZoomableImageViewHolder constructor(itemView: View) : init { touchImageView.setAllowParentInterceptOnEdge(false) touchImageView.setOnScaleChangeListener { scaleFactor, _, _ -> - Log.v("ATTACHEMENTS", "scaleFactor $scaleFactor") + // Log.v("ATTACHEMENTS", "scaleFactor $scaleFactor") // It's a bit annoying but when you pitch down the scaling // is not exactly one :/ touchImageView.setAllowParentInterceptOnEdge(scaleFactor <= 1.0008f) From 195e2703b91fe9c61b95de99516f0b98c51b1226 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 9 Jul 2020 15:22:34 +0200 Subject: [PATCH 038/122] Support open from upload media tab --- .../features/media/BaseAttachmentProvider.kt | 148 ++++++++++++++++ .../media/DataAttachmentRoomProvider.kt | 112 +++++++++++++ ...der.kt => RoomEventsAttachmentProvider.kt} | 158 ++++-------------- .../media/VectorAttachmentViewerActivity.kt | 96 +++++------ .../features/navigation/DefaultNavigator.kt | 33 ++-- .../riotx/features/navigation/Navigator.kt | 6 +- .../uploads/media/RoomUploadsMediaFragment.kt | 83 ++++++++- .../uploads/media/UploadsImageItem.kt | 2 + .../uploads/media/UploadsVideoItem.kt | 2 + .../main/res/layout/fragment_room_uploads.xml | 2 + 10 files changed, 455 insertions(+), 187 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/media/BaseAttachmentProvider.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/media/DataAttachmentRoomProvider.kt rename vector/src/main/java/im/vector/riotx/features/media/{RoomAttachmentProvider.kt => RoomEventsAttachmentProvider.kt} (53%) diff --git a/vector/src/main/java/im/vector/riotx/features/media/BaseAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/BaseAttachmentProvider.kt new file mode 100644 index 0000000000..d4c41c7cb3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/media/BaseAttachmentProvider.kt @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2020 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.riotx.features.media + +import android.content.Context +import android.graphics.drawable.Drawable +import android.view.View +import android.widget.ImageView +import com.bumptech.glide.request.target.CustomViewTarget +import com.bumptech.glide.request.transition.Transition +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.file.FileService +import im.vector.riotx.attachmentviewer.AttachmentInfo +import im.vector.riotx.attachmentviewer.AttachmentSourceProvider +import im.vector.riotx.attachmentviewer.ImageLoaderTarget +import im.vector.riotx.attachmentviewer.VideoLoaderTarget +import java.io.File + +abstract class BaseAttachmentProvider(val imageContentRenderer: ImageContentRenderer, val fileService: FileService) : AttachmentSourceProvider { + + interface InteractionListener { + fun onDismissTapped() + fun onShareTapped() + fun onPlayPause(play: Boolean) + fun videoSeekTo(percent: Int) + } + + var interactionListener: InteractionListener? = null + + protected var overlayView: AttachmentOverlayView? = null + + override fun overlayViewAtPosition(context: Context, position: Int): View? { + if (position == -1) return null + if (overlayView == null) { + overlayView = AttachmentOverlayView(context) + overlayView?.onBack = { + interactionListener?.onDismissTapped() + } + overlayView?.onShareCallback = { + interactionListener?.onShareTapped() + } + overlayView?.onPlayPause = { play -> + interactionListener?.onPlayPause(play) + } + overlayView?.videoSeekTo = { percent -> + interactionListener?.videoSeekTo(percent) + } + } + return overlayView + } + + override fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.Image) { + (info.data as? ImageContentRenderer.Data)?.let { + imageContentRenderer.render(it, target.contextView(), object : CustomViewTarget(target.contextView()) { + override fun onLoadFailed(errorDrawable: Drawable?) { + target.onLoadFailed(info.uid, errorDrawable) + } + + override fun onResourceCleared(placeholder: Drawable?) { + target.onResourceCleared(info.uid, placeholder) + } + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + target.onResourceReady(info.uid, resource) + } + }) + } + } + + override fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.AnimatedImage) { + (info.data as? ImageContentRenderer.Data)?.let { + imageContentRenderer.render(it, target.contextView(), object : CustomViewTarget(target.contextView()) { + override fun onLoadFailed(errorDrawable: Drawable?) { + target.onLoadFailed(info.uid, errorDrawable) + } + + override fun onResourceCleared(placeholder: Drawable?) { + target.onResourceCleared(info.uid, placeholder) + } + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + target.onResourceReady(info.uid, resource) + } + }) + } + } + + override fun loadVideo(target: VideoLoaderTarget, info: AttachmentInfo.Video) { + val data = info.data as? VideoContentRenderer.Data ?: return +// videoContentRenderer.render(data, +// holder.thumbnailImage, +// holder.loaderProgressBar, +// holder.videoView, +// holder.errorTextView) + imageContentRenderer.render(data.thumbnailMediaData, target.contextView(), object : CustomViewTarget(target.contextView()) { + override fun onLoadFailed(errorDrawable: Drawable?) { + target.onThumbnailLoadFailed(info.uid, errorDrawable) + } + + override fun onResourceCleared(placeholder: Drawable?) { + target.onThumbnailResourceCleared(info.uid, placeholder) + } + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + target.onThumbnailResourceReady(info.uid, resource) + } + }) + + target.onVideoFileLoading(info.uid) + fileService.downloadFile( + downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, + id = data.eventId, + mimeType = data.mimeType, + elementToDecrypt = data.elementToDecrypt, + fileName = data.filename, + url = data.url, + callback = object : MatrixCallback { + override fun onSuccess(data: File) { + target.onVideoFileReady(info.uid, data) + } + + override fun onFailure(failure: Throwable) { + target.onVideoFileLoadFailed(info.uid) + } + } + ) + } + + override fun clear(id: String) { + // TODO("Not yet implemented") + } + + abstract fun getFileForSharing(position: Int, callback: ((File?) -> Unit)) +} diff --git a/vector/src/main/java/im/vector/riotx/features/media/DataAttachmentRoomProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/DataAttachmentRoomProvider.kt new file mode 100644 index 0000000000..cb0039fc7e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/media/DataAttachmentRoomProvider.kt @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2020 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.riotx.features.media + +import android.content.Context +import android.view.View +import androidx.core.view.isVisible +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.events.model.isVideoMessage +import im.vector.matrix.android.api.session.file.FileService +import im.vector.matrix.android.api.session.room.Room +import im.vector.riotx.attachmentviewer.AttachmentInfo +import im.vector.riotx.core.date.VectorDateFormatter +import im.vector.riotx.core.extensions.localDateTime +import java.io.File + +class DataAttachmentRoomProvider( + private val attachments: List, + private val room: Room?, + private val initialIndex: Int, + imageContentRenderer: ImageContentRenderer, + private val dateFormatter: VectorDateFormatter, + fileService: FileService) : BaseAttachmentProvider(imageContentRenderer, fileService) { + + override fun getItemCount(): Int = attachments.size + + override fun getAttachmentInfoAt(position: Int): AttachmentInfo { + return attachments[position].let { + when (it) { + is ImageContentRenderer.Data -> { + if (it.mimeType == "image/gif") { + AttachmentInfo.AnimatedImage( + uid = it.eventId, + url = it.url ?: "", + data = it + ) + } else { + AttachmentInfo.Image( + uid = it.eventId, + url = it.url ?: "", + data = it + ) + } + } + is VideoContentRenderer.Data -> { + AttachmentInfo.Video( + uid = it.eventId, + url = it.url ?: "", + data = it, + thumbnail = AttachmentInfo.Image( + uid = it.eventId, + url = it.thumbnailMediaData.url ?: "", + data = it.thumbnailMediaData + ) + ) + } + else -> throw IllegalArgumentException() + } + } + } + + override fun overlayViewAtPosition(context: Context, position: Int): View? { + super.overlayViewAtPosition(context, position) + val item = attachments[position] + val timeLineEvent = room?.getTimeLineEvent(item.eventId) + if (timeLineEvent != null) { + val dateString = timeLineEvent.root.localDateTime().let { + "${dateFormatter.formatMessageDay(it)} at ${dateFormatter.formatMessageHour(it)} " + } + overlayView?.updateWith("${position + 1} of ${attachments.size}", "${timeLineEvent.senderInfo.displayName} $dateString") + overlayView?.videoControlsGroup?.isVisible = timeLineEvent.root.isVideoMessage() + } else { + overlayView?.updateWith("", "") + } + return overlayView + } + + override fun getFileForSharing(position: Int, callback: (File?) -> Unit) { + val item = attachments[position] + fileService.downloadFile( + downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, + id = item.eventId, + fileName = item.filename, + mimeType = item.mimeType, + url = item.url ?: "", + elementToDecrypt = item.elementToDecrypt, + callback = object : MatrixCallback { + override fun onSuccess(data: File) { + callback(data) + } + + override fun onFailure(failure: Throwable) { + callback(null) + } + } + ) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/RoomEventsAttachmentProvider.kt similarity index 53% rename from vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt rename to vector/src/main/java/im/vector/riotx/features/media/RoomEventsAttachmentProvider.kt index f7299bf714..7a7fea6dc4 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/RoomEventsAttachmentProvider.kt @@ -17,17 +17,14 @@ package im.vector.riotx.features.media import android.content.Context -import android.graphics.drawable.Drawable import android.view.View -import android.widget.ImageView import androidx.core.view.isVisible -import com.bumptech.glide.request.target.CustomViewTarget -import com.bumptech.glide.request.transition.Transition import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.isVideoMessage import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.file.FileService +import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageImageContent import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent @@ -36,33 +33,18 @@ import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.riotx.attachmentviewer.AttachmentInfo -import im.vector.riotx.attachmentviewer.AttachmentSourceProvider -import im.vector.riotx.attachmentviewer.ImageLoaderTarget -import im.vector.riotx.attachmentviewer.VideoLoaderTarget import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.extensions.localDateTime import java.io.File import javax.inject.Inject -class RoomAttachmentProvider( +class RoomEventsAttachmentProvider( private val attachments: List, private val initialIndex: Int, - private val imageContentRenderer: ImageContentRenderer, - private val videoContentRenderer: VideoContentRenderer, + imageContentRenderer: ImageContentRenderer, private val dateFormatter: VectorDateFormatter, - private val fileService: FileService -) : AttachmentSourceProvider { - - interface InteractionListener { - fun onDismissTapped() - fun onShareTapped() - fun onPlayPause(play: Boolean) - fun videoSeekTo(percent: Int) - } - - var interactionListener: InteractionListener? = null - - private var overlayView: AttachmentOverlayView? = null + fileService: FileService +) : BaseAttachmentProvider(imageContentRenderer, fileService) { override fun getItemCount(): Int { return attachments.size @@ -139,99 +121,8 @@ class RoomAttachmentProvider( } } - override fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.Image) { - (info.data as? ImageContentRenderer.Data)?.let { - imageContentRenderer.render(it, target.contextView(), object : CustomViewTarget(target.contextView()) { - override fun onLoadFailed(errorDrawable: Drawable?) { - target.onLoadFailed(info.uid, errorDrawable) - } - - override fun onResourceCleared(placeholder: Drawable?) { - target.onResourceCleared(info.uid, placeholder) - } - - override fun onResourceReady(resource: Drawable, transition: Transition?) { - target.onResourceReady(info.uid, resource) - } - }) - } - } - - override fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.AnimatedImage) { - (info.data as? ImageContentRenderer.Data)?.let { - imageContentRenderer.render(it, target.contextView(), object : CustomViewTarget(target.contextView()) { - override fun onLoadFailed(errorDrawable: Drawable?) { - target.onLoadFailed(info.uid, errorDrawable) - } - - override fun onResourceCleared(placeholder: Drawable?) { - target.onResourceCleared(info.uid, placeholder) - } - - override fun onResourceReady(resource: Drawable, transition: Transition?) { - target.onResourceReady(info.uid, resource) - } - }) - } - } - - override fun loadVideo(target: VideoLoaderTarget, info: AttachmentInfo.Video) { - val data = info.data as? VideoContentRenderer.Data ?: return -// videoContentRenderer.render(data, -// holder.thumbnailImage, -// holder.loaderProgressBar, -// holder.videoView, -// holder.errorTextView) - imageContentRenderer.render(data.thumbnailMediaData, target.contextView(), object : CustomViewTarget(target.contextView()) { - override fun onLoadFailed(errorDrawable: Drawable?) { - target.onThumbnailLoadFailed(info.uid, errorDrawable) - } - - override fun onResourceCleared(placeholder: Drawable?) { - target.onThumbnailResourceCleared(info.uid, placeholder) - } - - override fun onResourceReady(resource: Drawable, transition: Transition?) { - target.onThumbnailResourceReady(info.uid, resource) - } - }) - - target.onVideoFileLoading(info.uid) - fileService.downloadFile( - downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, - id = data.eventId, - mimeType = data.mimeType, - elementToDecrypt = data.elementToDecrypt, - fileName = data.filename, - url = data.url, - callback = object : MatrixCallback { - override fun onSuccess(data: File) { - target.onVideoFileReady(info.uid, data) - } - - override fun onFailure(failure: Throwable) { - target.onVideoFileLoadFailed(info.uid) - } - } - ) - } - override fun overlayViewAtPosition(context: Context, position: Int): View? { - if (overlayView == null) { - overlayView = AttachmentOverlayView(context) - overlayView?.onBack = { - interactionListener?.onDismissTapped() - } - overlayView?.onShareCallback = { - interactionListener?.onShareTapped() - } - overlayView?.onPlayPause = { play -> - interactionListener?.onPlayPause(play) - } - overlayView?.videoSeekTo = { percent -> - interactionListener?.videoSeekTo(percent) - } - } + super.overlayViewAtPosition(context, position) val item = attachments[position] val dateString = item.root.localDateTime().let { "${dateFormatter.formatMessageDay(it)} at ${dateFormatter.formatMessageHour(it)} " @@ -241,19 +132,44 @@ class RoomAttachmentProvider( return overlayView } - override fun clear(id: String) { - // TODO("Not yet implemented") + override fun getFileForSharing(position: Int, callback: (File?) -> Unit) { + attachments[position].let { timelineEvent -> + + val messageContent = timelineEvent.root.getClearContent().toModel() + as? MessageWithAttachmentContent + ?: return@let + fileService.downloadFile( + downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, + id = timelineEvent.eventId, + fileName = messageContent.body, + mimeType = messageContent.mimeType, + url = messageContent.getFileUrl(), + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), + callback = object : MatrixCallback { + override fun onSuccess(data: File) { + callback(data) + } + + override fun onFailure(failure: Throwable) { + callback(null) + } + } + ) + } } } -class RoomAttachmentProviderFactory @Inject constructor( +class AttachmentProviderFactory @Inject constructor( private val imageContentRenderer: ImageContentRenderer, private val vectorDateFormatter: VectorDateFormatter, - private val videoContentRenderer: VideoContentRenderer, private val session: Session ) { - fun createProvider(attachments: List, initialIndex: Int): RoomAttachmentProvider { - return RoomAttachmentProvider(attachments, initialIndex, imageContentRenderer, videoContentRenderer, vectorDateFormatter, session.fileService()) + fun createProvider(attachments: List, initialIndex: Int): RoomEventsAttachmentProvider { + return RoomEventsAttachmentProvider(attachments, initialIndex, imageContentRenderer, vectorDateFormatter, session.fileService()) + } + + fun createProvider(attachments: List, room: Room?, initialIndex: Int): DataAttachmentRoomProvider { + return DataAttachmentRoomProvider(attachments, room, initialIndex, imageContentRenderer, vectorDateFormatter, session.fileService()) } } diff --git a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt index 10483f3fa9..c0b822c13a 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt @@ -30,14 +30,6 @@ import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.transition.Transition -import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.file.FileService -import im.vector.matrix.android.api.session.room.model.message.MessageContent -import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent -import im.vector.matrix.android.api.session.room.model.message.getFileUrl -import im.vector.matrix.android.api.session.room.timeline.TimelineEvent -import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.riotx.R import im.vector.riotx.attachmentviewer.AttachmentCommands import im.vector.riotx.attachmentviewer.AttachmentViewerActivity @@ -52,11 +44,10 @@ import im.vector.riotx.features.themes.ActivityOtherThemes import im.vector.riotx.features.themes.ThemeUtils import kotlinx.android.parcel.Parcelize import timber.log.Timber -import java.io.File import javax.inject.Inject import kotlin.system.measureTimeMillis -class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmentProvider.InteractionListener { +class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmentProvider.InteractionListener { @Parcelize data class Args( @@ -69,7 +60,7 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen lateinit var sessionHolder: ActiveSessionHolder @Inject - lateinit var dataSourceFactory: RoomAttachmentProviderFactory + lateinit var dataSourceFactory: AttachmentProviderFactory @Inject lateinit var imageContentRenderer: ImageContentRenderer @@ -78,7 +69,8 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen private var initialIndex = 0 private var isAnimatingOut = false - private var eventList: List? = null + + var currentSourceProvider: BaseAttachmentProvider? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -92,13 +84,6 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen ThemeUtils.setActivityTheme(this, getOtherThemes()) val args = args() ?: throw IllegalArgumentException("Missing arguments") - val session = sessionHolder.getSafeActiveSession() ?: return Unit.also { finish() } - - val room = args.roomId?.let { session.getRoom(it) } - val events = room?.getAttachmentMessages() ?: emptyList() - eventList = events - val index = events.indexOfFirst { it.eventId == args.eventId } - initialIndex = index if (savedInstanceState == null && addTransitionListener()) { args.sharedTransitionName?.let { @@ -127,14 +112,41 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen } } - val sourceProvider = dataSourceFactory.createProvider(events, index) - sourceProvider.interactionListener = this - setSourceProvider(sourceProvider) - if (savedInstanceState == null) { - pager2.setCurrentItem(index, false) - // The page change listener is not notified of the change... - pager2.post { - onSelectedPositionChanged(index) + val session = sessionHolder.getSafeActiveSession() ?: return Unit.also { finish() } + + val room = args.roomId?.let { session.getRoom(it) } + + val inMemoryData = intent.getParcelableArrayListExtra(EXTRA_IN_MEMORY_DATA) + if (inMemoryData != null) { + val sourceProvider = dataSourceFactory.createProvider(inMemoryData, room, initialIndex) + val index = inMemoryData.indexOfFirst { it.eventId == args.eventId } + initialIndex = index + sourceProvider.interactionListener = this + setSourceProvider(sourceProvider) + this.currentSourceProvider = sourceProvider + if (savedInstanceState == null) { + pager2.setCurrentItem(index, false) + // The page change listener is not notified of the change... + pager2.post { + onSelectedPositionChanged(index) + } + } + } else { + val events = room?.getAttachmentMessages() + ?: emptyList() + val index = events.indexOfFirst { it.eventId == args.eventId } + initialIndex = index + + val sourceProvider = dataSourceFactory.createProvider(events, index) + sourceProvider.interactionListener = this + setSourceProvider(sourceProvider) + this.currentSourceProvider = sourceProvider + if (savedInstanceState == null) { + pager2.setCurrentItem(index, false) + // The page change listener is not notified of the change... + pager2.post { + onSelectedPositionChanged(index) + } } } @@ -228,14 +240,19 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen const val EXTRA_ARGS = "EXTRA_ARGS" const val EXTRA_IMAGE_DATA = "EXTRA_IMAGE_DATA" + const val EXTRA_IN_MEMORY_DATA = "EXTRA_IN_MEMORY_DATA" fun newIntent(context: Context, mediaData: AttachmentData, roomId: String?, eventId: String, + inMemoryData: List?, sharedTransitionName: String?) = Intent(context, VectorAttachmentViewerActivity::class.java).also { it.putExtra(EXTRA_ARGS, Args(roomId, eventId, sharedTransitionName)) it.putExtra(EXTRA_IMAGE_DATA, mediaData) + if (inMemoryData != null) { + it.putParcelableArrayListExtra(EXTRA_IN_MEMORY_DATA, ArrayList(inMemoryData)) + } } } @@ -252,27 +269,10 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen } override fun onShareTapped() { - // Share - eventList?.get(currentPosition)?.let { timelineEvent -> - - val messageContent = timelineEvent.root.getClearContent().toModel() - as? MessageWithAttachmentContent - ?: return@let - sessionHolder.getSafeActiveSession()?.fileService()?.downloadFile( - downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, - id = timelineEvent.eventId, - fileName = messageContent.body, - mimeType = messageContent.mimeType, - url = messageContent.getFileUrl(), - elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), - callback = object : MatrixCallback { - override fun onSuccess(data: File) { - if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { - shareMedia(this@VectorAttachmentViewerActivity, data, getMimeTypeFromUri(this@VectorAttachmentViewerActivity, data.toUri())) - } - } - } - ) + this.currentSourceProvider?.getFileForSharing(currentPosition) { data -> + if (data != null && lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { + shareMedia(this@VectorAttachmentViewerActivity, data, getMimeTypeFromUri(this@VectorAttachmentViewerActivity, data.toUri())) + } } } } diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 2b0b6175f5..8940ac6791 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -246,20 +246,25 @@ class DefaultNavigator @Inject constructor( } override fun openImageViewer(activity: Activity, - roomId: String?, + roomId: String, mediaData: AttachmentData, view: View, + inMemory: List?, options: ((MutableList>) -> Unit)?) { - VectorAttachmentViewerActivity.newIntent(activity, mediaData, roomId, mediaData.eventId, ViewCompat.getTransitionName(view)).let { intent -> + VectorAttachmentViewerActivity.newIntent(activity, + mediaData, + roomId, + mediaData.eventId, + inMemory, + ViewCompat.getTransitionName(view)).let { intent -> val pairs = ArrayList>() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let { - pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) - } - activity.window.decorView.findViewById(android.R.id.navigationBarBackground)?.let { - pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) - } + activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let { + pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) } + activity.window.decorView.findViewById(android.R.id.navigationBarBackground)?.let { + pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) + } + pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: "")) options?.invoke(pairs) @@ -284,12 +289,18 @@ class DefaultNavigator @Inject constructor( } override fun openVideoViewer(activity: Activity, - roomId: String?, mediaData: VideoContentRenderer.Data, + roomId: String, mediaData: VideoContentRenderer.Data, view: View, + inMemory: List?, options: ((MutableList>) -> Unit)?) { // val intent = VideoMediaViewerActivity.newIntent(activity, mediaData) // activity.startActivity(intent) - VectorAttachmentViewerActivity.newIntent(activity, mediaData, roomId, mediaData.eventId, ViewCompat.getTransitionName(view)).let { intent -> + VectorAttachmentViewerActivity.newIntent(activity, + mediaData, + roomId, + mediaData.eventId, + inMemory, + ViewCompat.getTransitionName(view)).let { intent -> val pairs = ArrayList>() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let { diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index f1be6e072b..f925344570 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -92,13 +92,15 @@ interface Navigator { fun openRoomWidget(context: Context, roomId: String, widget: Widget) fun openImageViewer(activity: Activity, - roomId: String?, + roomId: String, mediaData: AttachmentData, view: View, + inMemory: List? = null, options: ((MutableList>) -> Unit)?) fun openVideoViewer(activity: Activity, - roomId: String?, mediaData: VideoContentRenderer.Data, + roomId: String, mediaData: VideoContentRenderer.Data, view: View, + inMemory: List? = null, options: ((MutableList>) -> Unit)?) } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt index a5f126875a..e0758c7d72 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt @@ -20,23 +20,34 @@ import android.os.Bundle import android.util.DisplayMetrics import android.view.View import androidx.core.content.ContextCompat +import androidx.core.util.Pair +import androidx.core.view.ViewCompat import androidx.recyclerview.widget.GridLayoutManager import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState +import com.google.android.material.appbar.AppBarLayout +import im.vector.matrix.android.api.session.room.model.message.MessageImageContent +import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent +import im.vector.matrix.android.api.session.room.model.message.getFileUrl +import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.riotx.R import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.trackItemsVisibilityChange import im.vector.riotx.core.platform.StateView import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.utils.DimensionConverter +import im.vector.riotx.features.media.AttachmentData import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VideoContentRenderer import im.vector.riotx.features.roomprofile.uploads.RoomUploadsAction +import im.vector.riotx.features.roomprofile.uploads.RoomUploadsFragment import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewModel +import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewState import kotlinx.android.synthetic.main.fragment_generic_state_view_recycler.* +import kotlinx.android.synthetic.main.fragment_room_uploads.* import javax.inject.Inject class RoomUploadsMediaFragment @Inject constructor( @@ -76,13 +87,75 @@ class RoomUploadsMediaFragment @Inject constructor( controller.listener = null } - override fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) { - navigator.openImageViewer(requireActivity(), null, mediaData, view, null) + // It's very strange i can't just access + // the app bar using find by id... + private fun trickFindAppBar() : AppBarLayout? { + return activity?.supportFragmentManager?.fragments + ?.filterIsInstance() + ?.firstOrNull() + ?.roomUploadsAppBar } - override fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) { - // TODO - // navigator.openVideoViewer(requireActivity(), mediaData, null, ) + override fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) = withState(uploadsViewModel) { state -> + + val inMemory = getItemsArgs(state) + navigator.openImageViewer(requireActivity(), state.roomId, mediaData, view, inMemory) { pairs -> + trickFindAppBar()?.let { + pairs.add(Pair(it, ViewCompat.getTransitionName(it) ?: "")) + } + } + } + + private fun getItemsArgs(state: RoomUploadsViewState): List { + return state.mediaEvents.mapNotNull { + when (val content = it.contentWithAttachmentContent) { + is MessageImageContent -> { + ImageContentRenderer.Data( + eventId = it.eventId, + filename = content.body, + mimeType = content.mimeType, + url = content.getFileUrl(), + elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(), + maxHeight = -1, + maxWidth = -1, + width = null, + height = null + ) + } + is MessageVideoContent -> { + val thumbnailData = ImageContentRenderer.Data( + eventId = it.eventId, + filename = content.body, + mimeType = content.mimeType, + url = content.videoInfo?.thumbnailFile?.url + ?: content.videoInfo?.thumbnailUrl, + elementToDecrypt = content.videoInfo?.thumbnailFile?.toElementToDecrypt(), + height = content.videoInfo?.height, + maxHeight = -1, + width = content.videoInfo?.width, + maxWidth = -1 + ) + VideoContentRenderer.Data( + eventId = it.eventId, + filename = content.body, + mimeType = content.mimeType, + url = content.getFileUrl(), + elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(), + thumbnailMediaData = thumbnailData + ) + } + else -> null + } + } + } + + override fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) = withState(uploadsViewModel) { state -> + val inMemory = getItemsArgs(state) + navigator.openVideoViewer(requireActivity(), state.roomId, mediaData, view, inMemory) { pairs -> + trickFindAppBar()?.let { + pairs.add(Pair(it, ViewCompat.getTransitionName(it) ?: "")) + } + } } override fun loadMore() { diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt index 98026901cc..f994ad0110 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.roomprofile.uploads.media import android.view.View import android.widget.ImageView +import androidx.core.view.ViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R @@ -37,6 +38,7 @@ abstract class UploadsImageItem : VectorEpoxyModel() { super.bind(holder) holder.view.setOnClickListener { listener?.onItemClicked(holder.imageView, data) } imageContentRenderer.render(data, holder.imageView, IMAGE_SIZE_DP) + ViewCompat.setTransitionName(holder.imageView, "imagePreview_${id()}") } class Holder : VectorEpoxyHolder() { diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt index 82e33b76da..1c9ab4ae74 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.roomprofile.uploads.media import android.view.View import android.widget.ImageView +import androidx.core.view.ViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R @@ -38,6 +39,7 @@ abstract class UploadsVideoItem : VectorEpoxyModel() { super.bind(holder) holder.view.setOnClickListener { listener?.onItemClicked(holder.imageView, data) } imageContentRenderer.render(data.thumbnailMediaData, holder.imageView, IMAGE_SIZE_DP) + ViewCompat.setTransitionName(holder.imageView, "videoPreview_${id()}") } class Holder : VectorEpoxyHolder() { diff --git a/vector/src/main/res/layout/fragment_room_uploads.xml b/vector/src/main/res/layout/fragment_room_uploads.xml index 5e289d4724..f5d3658ee5 100644 --- a/vector/src/main/res/layout/fragment_room_uploads.xml +++ b/vector/src/main/res/layout/fragment_room_uploads.xml @@ -8,6 +8,8 @@ From a98b2ecce39147f50a8ac5b150401ce5a4a7f7f6 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 30 Jun 2020 15:59:43 +0200 Subject: [PATCH 039/122] Set server backup banner --- .../vector/riotx/core/di/ScreenComponent.kt | 2 + .../vector/riotx/core/di/ViewModelModule.kt | 7 +- .../riotx/core/ui/views/KeysBackupBanner.kt | 49 ++--- .../recover/BootstrapCrossSigningTask.kt | 30 ++- .../riotx/features/home/HomeActivity.kt | 15 +- .../riotx/features/home/HomeDetailFragment.kt | 52 ++---- .../features/navigation/DefaultNavigator.kt | 10 +- .../signout/ServerBackupStatusViewModel.kt | 176 ++++++++++++++++++ .../SignOutBottomSheetDialogFragment.kt | 24 ++- .../workers/signout/SignOutViewModel.kt | 74 -------- .../main/res/drawable/ic_secure_backup.xml | 20 ++ .../main/res/layout/fragment_home_detail.xml | 2 + .../res/layout/view_keys_backup_banner.xml | 12 +- vector/src/main/res/values/strings.xml | 7 +- 14 files changed, 312 insertions(+), 168 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt delete mode 100644 vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutViewModel.kt create mode 100644 vector/src/main/res/drawable/ic_secure_backup.xml diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index ceb276614a..4a6aee0f6f 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -72,6 +72,7 @@ import im.vector.riotx.features.terms.ReviewTermsActivity import im.vector.riotx.features.ui.UiStateRepository import im.vector.riotx.features.widgets.WidgetActivity import im.vector.riotx.features.widgets.permissions.RoomWidgetPermissionBottomSheet +import im.vector.riotx.features.workers.signout.SignOutBottomSheetDialogFragment @Component( dependencies = [ @@ -152,6 +153,7 @@ interface ScreenComponent { fun inject(bottomSheet: RoomWidgetPermissionBottomSheet) fun inject(bottomSheet: RoomWidgetsBottomSheet) fun inject(bottomSheet: CallControlsBottomSheet) + fun inject(bottomSheet: SignOutBottomSheetDialogFragment) /* ========================================================================================== * Others diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index badfdd96c1..2a3db0cf19 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -36,7 +36,7 @@ import im.vector.riotx.features.reactions.EmojiChooserViewModel import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.riotx.features.roomprofile.RoomProfileSharedActionViewModel import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel -import im.vector.riotx.features.workers.signout.SignOutViewModel +import im.vector.riotx.features.workers.signout.ServerBackupStatusViewModel @Module interface ViewModelModule { @@ -51,11 +51,6 @@ interface ViewModelModule { * Below are bindings for the androidx view models (which extend ViewModel). Will be converted to MvRx ViewModel in the future. */ - @Binds - @IntoMap - @ViewModelKey(SignOutViewModel::class) - fun bindSignOutViewModel(viewModel: SignOutViewModel): ViewModel - @Binds @IntoMap @ViewModelKey(EmojiChooserViewModel::class) diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt index 817575d91a..460c871288 100755 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt @@ -24,8 +24,10 @@ import android.view.ViewGroup import android.widget.AbsListView import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout +import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.edit import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView import androidx.transition.TransitionManager import butterknife.BindView import butterknife.ButterKnife @@ -58,22 +60,12 @@ class KeysBackupBanner @JvmOverloads constructor( var delegate: Delegate? = null private var state: State = State.Initial - private var scrollState = AbsListView.OnScrollListener.SCROLL_STATE_IDLE - set(value) { - field = value - - val pendingV = pendingVisibility - - if (pendingV != null) { - pendingVisibility = null - visibility = pendingV - } - } - - private var pendingVisibility: Int? = null - init { setupView() + PreferenceManager.getDefaultSharedPreferences(context).edit { + putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false) + putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, "") + } } /** @@ -91,7 +83,8 @@ class KeysBackupBanner @JvmOverloads constructor( state = newState hideAll() - + val parent = parent as ViewGroup + TransitionManager.beginDelayedTransition(parent) when (newState) { State.Initial -> renderInitial() State.Hidden -> renderHidden() @@ -102,22 +95,6 @@ class KeysBackupBanner @JvmOverloads constructor( } } - override fun setVisibility(visibility: Int) { - if (scrollState != AbsListView.OnScrollListener.SCROLL_STATE_IDLE) { - // Wait for scroll state to be idle - pendingVisibility = visibility - return - } - - if (visibility != getVisibility()) { - // Schedule animation - val parent = parent as ViewGroup - TransitionManager.beginDelayedTransition(parent) - } - - super.setVisibility(visibility) - } - override fun onClick(v: View?) { when (state) { is State.Setup -> { @@ -166,6 +143,8 @@ class KeysBackupBanner @JvmOverloads constructor( ButterKnife.bind(this) setOnClickListener(this) + textView1.setOnClickListener(this) + textView2.setOnClickListener(this) } private fun renderInitial() { @@ -218,10 +197,10 @@ class KeysBackupBanner @JvmOverloads constructor( } private fun renderBackingUp() { - // Do not render when backing up anymore - isVisible = false - - textView1.setText(R.string.keys_backup_banner_in_progress) + isVisible = true + textView1.setText(R.string.keys_backup_banner_setup_line1) + textView2.isVisible = true + textView2.setText(R.string.keys_backup_banner_in_progress) loading.isVisible = true } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt index 6a3fadbcb3..290a08bfad 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.securestorage.SsssKeySpec import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.matrix.android.internal.util.awaitCallback @@ -84,8 +85,10 @@ class BootstrapCrossSigningTask @Inject constructor( override suspend fun execute(params: Params): BootstrapResult { val crossSigningService = session.cryptoService().crossSigningService() + Timber.d("## BootstrapCrossSigningTask: initXSOnly:${params.initOnlyCrossSigning} Starting...") // Ensure cross-signing is initialized. Due to migration it is maybe not always correctly initialized if (!crossSigningService.isCrossSigningInitialized()) { + Timber.d("## BootstrapCrossSigningTask: Cross signing not enabled, so initialize") params.progressListener?.onProgress( WaitingViewData( stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing), @@ -104,8 +107,9 @@ class BootstrapCrossSigningTask @Inject constructor( return handleInitializeXSigningError(failure) } } else { - // not sure how this can happen?? + Timber.d("## BootstrapCrossSigningTask: Cross signing already setup, go to 4S setup") if (params.initOnlyCrossSigning) { + // not sure how this can happen?? return handleInitializeXSigningError(IllegalArgumentException("Cross signing already setup")) } } @@ -119,6 +123,8 @@ class BootstrapCrossSigningTask @Inject constructor( stringProvider.getString(R.string.bootstrap_crosssigning_progress_pbkdf2), isIndeterminate = true) ) + + Timber.d("## BootstrapCrossSigningTask: Creating 4S key with pass: ${params.passphrase != null}") try { keyInfo = awaitCallback { params.passphrase?.let { passphrase -> @@ -141,6 +147,7 @@ class BootstrapCrossSigningTask @Inject constructor( } } } catch (failure: Failure) { + Timber.e("## BootstrapCrossSigningTask: Creating 4S - Failed to generate key <${failure.localizedMessage}>") return BootstrapResult.FailedToCreateSSSSKey(failure) } @@ -149,19 +156,25 @@ class BootstrapCrossSigningTask @Inject constructor( stringProvider.getString(R.string.bootstrap_crosssigning_progress_default_key), isIndeterminate = true) ) + + Timber.d("## BootstrapCrossSigningTask: Creating 4S - Set default key") try { awaitCallback { ssssService.setDefaultKey(keyInfo.keyId, it) } } catch (failure: Failure) { // Maybe we could just ignore this error? + Timber.e("## BootstrapCrossSigningTask: Creating 4S - Set default key error <${failure.localizedMessage}>") return BootstrapResult.FailedToSetDefaultSSSSKey(failure) } + + Timber.d("## BootstrapCrossSigningTask: Creating 4S - gathering private keys") val xKeys = crossSigningService.getCrossSigningPrivateKeys() val mskPrivateKey = xKeys?.master ?: return BootstrapResult.MissingPrivateKey val sskPrivateKey = xKeys.selfSigned ?: return BootstrapResult.MissingPrivateKey val uskPrivateKey = xKeys.user ?: return BootstrapResult.MissingPrivateKey + Timber.d("## BootstrapCrossSigningTask: Creating 4S - gathering private keys success") try { params.progressListener?.onProgress( @@ -170,6 +183,7 @@ class BootstrapCrossSigningTask @Inject constructor( isIndeterminate = true ) ) + Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing MSK...") awaitCallback { ssssService.storeSecret( MASTER_KEY_SSSS_NAME, @@ -183,6 +197,7 @@ class BootstrapCrossSigningTask @Inject constructor( isIndeterminate = true ) ) + Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing USK...") awaitCallback { ssssService.storeSecret( USER_SIGNING_KEY_SSSS_NAME, @@ -196,6 +211,7 @@ class BootstrapCrossSigningTask @Inject constructor( stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_ssk), isIndeterminate = true ) ) + Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing SSK...") awaitCallback { ssssService.storeSecret( SELF_SIGNING_KEY_SSSS_NAME, @@ -204,6 +220,7 @@ class BootstrapCrossSigningTask @Inject constructor( ) } } catch (failure: Failure) { + Timber.e("## BootstrapCrossSigningTask: Creating 4S - Failed to store keys <${failure.localizedMessage}>") // Maybe we could just ignore this error? return BootstrapResult.FailedToStorePrivateKeyInSSSS(failure) } @@ -215,7 +232,14 @@ class BootstrapCrossSigningTask @Inject constructor( ) ) try { - if (session.cryptoService().keysBackupService().keysBackupVersion == null) { + Timber.d("## BootstrapCrossSigningTask: Creating 4S - Checking megolm backup") + + // First ensure that in sync + val serverVersion = awaitCallback { + session.cryptoService().keysBackupService().getCurrentVersion(it) + } + if (serverVersion == null) { + Timber.d("## BootstrapCrossSigningTask: Creating 4S - Create megolm backup") val creationInfo = awaitCallback { session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) } @@ -223,6 +247,7 @@ class BootstrapCrossSigningTask @Inject constructor( session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it) } // Save it for gossiping + Timber.d("## BootstrapCrossSigningTask: Creating 4S - Save megolm backup key for gossiping") session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) awaitCallback { @@ -239,6 +264,7 @@ class BootstrapCrossSigningTask @Inject constructor( Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup") } + Timber.d("## BootstrapCrossSigningTask: initXSOnly:${params.initOnlyCrossSigning} Finished") return BootstrapResult.Success(keyInfo) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index 8d5fc5f564..6991aaa493 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -46,7 +46,8 @@ import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.popup.VerificationVectorAlert import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotx.features.settings.VectorPreferences -import im.vector.riotx.features.workers.signout.SignOutViewModel +import im.vector.riotx.features.workers.signout.ServerBackupStatusViewModel +import im.vector.riotx.features.workers.signout.ServerBackupStatusViewState import im.vector.riotx.push.fcm.FcmHelper import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.activity_home.* @@ -60,13 +61,17 @@ data class HomeActivityArgs( val accountCreation: Boolean ) : Parcelable -class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory { +class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory, ServerBackupStatusViewModel.Factory { private lateinit var sharedActionViewModel: HomeSharedActionViewModel private val homeActivityViewModel: HomeActivityViewModel by viewModel() @Inject lateinit var viewModelFactory: HomeActivityViewModel.Factory + + private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel() + @Inject lateinit var serverBackupviewModelFactory: ServerBackupStatusViewModel.Factory + @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @Inject lateinit var pushManager: PushersManager @@ -92,6 +97,10 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet return unknownDeviceViewModelFactory.create(initialState) } + override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel { + return serverBackupviewModelFactory.create(initialState) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice()) @@ -230,7 +239,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet } // Force remote backup state update to update the banner if needed - viewModelProvider.get(SignOutViewModel::class.java).refreshRemoteStateIfNeeded() + serverBackupStatusViewModel.refreshRemoteStateIfNeeded() } override fun configure(toolbar: Toolbar) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index c92c28079f..c736c0c1ca 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -21,13 +21,13 @@ import android.view.LayoutInflater import android.view.View import androidx.core.content.ContextCompat import androidx.core.view.forEachIndexed +import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.bottomnavigation.BottomNavigationItemView import com.google.android.material.bottomnavigation.BottomNavigationMenuView -import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo @@ -49,13 +49,10 @@ import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.popup.VerificationVectorAlert import im.vector.riotx.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS -import im.vector.riotx.features.workers.signout.SignOutViewModel +import im.vector.riotx.features.workers.signout.BannerState +import im.vector.riotx.features.workers.signout.ServerBackupStatusViewModel +import im.vector.riotx.features.workers.signout.ServerBackupStatusViewState import kotlinx.android.synthetic.main.fragment_home_detail.* -import kotlinx.android.synthetic.main.fragment_home_detail.activeCallPiP -import kotlinx.android.synthetic.main.fragment_home_detail.activeCallPiPWrap -import kotlinx.android.synthetic.main.fragment_home_detail.activeCallView -import kotlinx.android.synthetic.main.fragment_home_detail.syncStateView -import kotlinx.android.synthetic.main.fragment_room_detail.* import timber.log.Timber import javax.inject.Inject @@ -65,15 +62,17 @@ private const val INDEX_ROOMS = 2 class HomeDetailFragment @Inject constructor( val homeDetailViewModelFactory: HomeDetailViewModel.Factory, + private val serverBackupStatusViewModelFactory: ServerBackupStatusViewModel.Factory, private val avatarRenderer: AvatarRenderer, private val alertManager: PopupAlertManager, private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager -) : VectorBaseFragment(), KeysBackupBanner.Delegate, ActiveCallView.Callback { +) : VectorBaseFragment(), KeysBackupBanner.Delegate, ActiveCallView.Callback, ServerBackupStatusViewModel.Factory { private val unreadCounterBadgeViews = arrayListOf() private val viewModel: HomeDetailViewModel by fragmentViewModel() private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel() + private val serverBackupStatusViewModel: ServerBackupStatusViewModel by activityViewModel() private lateinit var sharedActionViewModel: HomeSharedActionViewModel private lateinit var sharedCallActionViewModel: SharedActiveCallViewModel @@ -195,34 +194,15 @@ class HomeDetailFragment @Inject constructor( } private fun setupKeysBackupBanner() { - // Keys backup banner - // Use the SignOutViewModel, it observe the keys backup state and this is what we need here - val model = fragmentViewModelProvider.get(SignOutViewModel::class.java) - model.keysBackupState.observe(viewLifecycleOwner, Observer { keysBackupState -> - when (keysBackupState) { - null -> - homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false) - KeysBackupState.Disabled -> - homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(model.getNumberOfKeysToBackup()), false) - KeysBackupState.NotTrusted, - KeysBackupState.WrongBackUpVersion -> - // In this case, getCurrentBackupVersion() should not return "" - homeKeysBackupBanner.render(KeysBackupBanner.State.Recover(model.getCurrentBackupVersion()), false) - KeysBackupState.WillBackUp, - KeysBackupState.BackingUp -> - homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false) - KeysBackupState.ReadyToBackUp -> - if (model.canRestoreKeys()) { - homeKeysBackupBanner.render(KeysBackupBanner.State.Update(model.getCurrentBackupVersion()), false) - } else { - homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false) - } - else -> - homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false) + serverBackupStatusViewModel.subscribe(this) { + when (val banState = it.bannerState.invoke()) { + is BannerState.Setup -> homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false) + BannerState.BackingUp -> homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false) + null, + BannerState.Hidden -> homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false) } - }) - + }.disposeOnDestroyView() homeKeysBackupBanner.delegate = this } @@ -331,4 +311,8 @@ class HomeDetailFragment @Inject constructor( } } } + + override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel { + return serverBackupStatusViewModelFactory.create(initialState) + } } diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 0b89ab8ec4..79ba5121fc 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -199,7 +199,15 @@ class DefaultNavigator @Inject constructor( } override fun openKeysBackupSetup(context: Context, showManualExport: Boolean) { - context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport)) + // if cross signing is enabled we should propose full 4S + sessionHolder.getSafeActiveSession()?.let { session -> + if (session.cryptoService().crossSigningService().canCrossSign() && context is VectorBaseActivity) { + BootstrapBottomSheet.show(context.supportFragmentManager, false) + } else { + context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport)) + } + } + } override fun openKeysBackupManager(context: Context) { diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt new file mode 100644 index 0000000000..04ece8e407 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt @@ -0,0 +1,176 @@ +/* + * Copyright 2019 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.riotx.features.workers.signout + +import androidx.lifecycle.MutableLiveData +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo +import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener +import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData +import im.vector.matrix.rx.rx +import im.vector.riotx.core.platform.EmptyAction +import im.vector.riotx.core.platform.EmptyViewEvents +import im.vector.riotx.core.platform.VectorViewModel +import io.reactivex.Observable +import io.reactivex.functions.Function4 +import io.reactivex.subjects.PublishSubject +import java.util.concurrent.TimeUnit + +data class ServerBackupStatusViewState( + val bannerState: Async = Uninitialized +) : MvRxState + +/** + * The state representing the view + * It can take one state at a time + */ +sealed class BannerState { + + object Hidden : BannerState() + + // Keys backup is not setup, numberOfKeys is the number of locally stored keys + data class Setup(val numberOfKeys: Int) : BannerState() + + // Keys are backing up + object BackingUp : BannerState() +} + +class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialState: ServerBackupStatusViewState, + private val session: Session) + : VectorViewModel(initialState), KeysBackupStateListener { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: ServerBackupStatusViewState): ServerBackupStatusViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + // Keys exported manually + val keysExportedToFile = MutableLiveData() + val keysBackupState = MutableLiveData() + + private val keyBackupPublishSubject: PublishSubject = PublishSubject.create() + + init { + session.cryptoService().keysBackupService().addListener(this) + + keyBackupPublishSubject.onNext(session.cryptoService().keysBackupService().state) + keysBackupState.value = session.cryptoService().keysBackupService().state + session.rx().liveCrossSigningPrivateKeys() + Observable.combineLatest, Optional, KeysBackupState, Optional, BannerState>( + session.rx().liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME)), + session.rx().liveCrossSigningInfo(session.myUserId), + keyBackupPublishSubject, + session.rx().liveCrossSigningPrivateKeys(), + Function4 { _, crossSigningInfo, keyBackupState, pInfo -> + // first check if 4S is already setup + if (session.sharedSecretStorageService.isRecoverySetup()) { + // 4S is already setup sp we should not display anything + return@Function4 when (keyBackupState) { + KeysBackupState.BackingUp -> BannerState.BackingUp + else -> BannerState.Hidden + } + } + + // So recovery is not setup + // Check if cross signing is enabled and local secrets known + if (crossSigningInfo.getOrNull()?.isTrusted() == true + && pInfo.getOrNull()?.master != null + && pInfo.getOrNull()?.selfSigned != null + && pInfo.getOrNull()?.user != null + ) { + // So 4S is not setup and we have local secrets, + return@Function4 BannerState.Setup(numberOfKeys = getNumberOfKeysToBackup()) + } + + BannerState.Hidden + } + ) + .throttleLast(2000, TimeUnit.MILLISECONDS) // we don't want to flicker or catch transient states + .distinctUntilChanged() + .execute { async -> + copy( + bannerState = async + ) + } + } + + /** + * Safe way to get the current KeysBackup version + */ + fun getCurrentBackupVersion(): String { + return session.cryptoService().keysBackupService().currentBackupVersion ?: "" + } + + /** + * Safe way to get the number of keys to backup + */ + fun getNumberOfKeysToBackup(): Int { + return session.cryptoService().inboundGroupSessionsCount(false) + } + + /** + * Safe way to tell if there are more keys on the server + */ + fun canRestoreKeys(): Boolean { + return session.cryptoService().keysBackupService().canRestoreKeys() + } + + override fun onCleared() { + super.onCleared() + session.cryptoService().keysBackupService().removeListener(this) + } + + override fun onStateChange(newState: KeysBackupState) { + keyBackupPublishSubject.onNext(session.cryptoService().keysBackupService().state) + keysBackupState.value = newState + } + + fun refreshRemoteStateIfNeeded() { + if (keysBackupState.value == KeysBackupState.Disabled) { + session.cryptoService().keysBackupService().checkAndStartKeysBackup() + } + } + + override fun handle(action: EmptyAction) {} +} diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt index e1ef7bc07b..fa5c4119ed 100644 --- a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt @@ -31,16 +31,21 @@ import androidx.core.view.isVisible import androidx.lifecycle.Observer import androidx.transition.TransitionManager import butterknife.BindView +import com.airbnb.mvrx.fragmentViewModel import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.core.utils.toast +import im.vector.riotx.features.attachments.preview.AttachmentsPreviewViewModel import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity +import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsViewModel +import javax.inject.Inject -class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() { +class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), ServerBackupStatusViewModel.Factory { @BindView(R.id.bottom_sheet_signout_warning_text) lateinit var sheetTitle: TextView @@ -84,13 +89,23 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() { isCancelable = true } - private lateinit var viewModel: SignOutViewModel + @Inject + lateinit var viewModelFactory: ServerBackupStatusViewModel.Factory + + override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel { + return viewModelFactory.create(initialState) + } + + private val viewModel: ServerBackupStatusViewModel by fragmentViewModel(ServerBackupStatusViewModel::class) + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - viewModel = fragmentViewModelProvider.get(SignOutViewModel::class.java) - setupClickableView.setOnClickListener { context?.let { context -> startActivityForResult(KeysBackupSetupActivity.intent(context, true), EXPORT_REQ) @@ -234,4 +249,5 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() { } } } + } diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutViewModel.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutViewModel.kt deleted file mode 100644 index 2f26fdf377..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutViewModel.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2019 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.riotx.features.workers.signout - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import im.vector.matrix.android.api.session.Session -import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState -import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener -import javax.inject.Inject - -class SignOutViewModel @Inject constructor(private val session: Session) : ViewModel(), KeysBackupStateListener { - // Keys exported manually - var keysExportedToFile = MutableLiveData() - - var keysBackupState = MutableLiveData() - - init { - session.cryptoService().keysBackupService().addListener(this) - - keysBackupState.value = session.cryptoService().keysBackupService().state - } - - /** - * Safe way to get the current KeysBackup version - */ - fun getCurrentBackupVersion(): String { - return session.cryptoService().keysBackupService().currentBackupVersion ?: "" - } - - /** - * Safe way to get the number of keys to backup - */ - fun getNumberOfKeysToBackup(): Int { - return session.cryptoService().inboundGroupSessionsCount(false) - } - - /** - * Safe way to tell if there are more keys on the server - */ - fun canRestoreKeys(): Boolean { - return session.cryptoService().keysBackupService().canRestoreKeys() - } - - override fun onCleared() { - super.onCleared() - - session.cryptoService().keysBackupService().removeListener(this) - } - - override fun onStateChange(newState: KeysBackupState) { - keysBackupState.value = newState - } - - fun refreshRemoteStateIfNeeded() { - if (keysBackupState.value == KeysBackupState.Disabled) { - session.cryptoService().keysBackupService().checkAndStartKeysBackup() - } - } -} diff --git a/vector/src/main/res/drawable/ic_secure_backup.xml b/vector/src/main/res/drawable/ic_secure_backup.xml new file mode 100644 index 0000000000..899bb8d2ae --- /dev/null +++ b/vector/src/main/res/drawable/ic_secure_backup.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_home_detail.xml b/vector/src/main/res/layout/fragment_home_detail.xml index f90422dff9..aa7a76cf16 100644 --- a/vector/src/main/res/layout/fragment_home_detail.xml +++ b/vector/src/main/res/layout/fragment_home_detail.xml @@ -59,6 +59,8 @@ android:layout_height="wrap_content" android:background="?riotx_keys_backup_banner_accent_color" android:minHeight="67dp" + android:visibility="gone" + tools:visibility="visible" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/syncStateView" /> diff --git a/vector/src/main/res/layout/view_keys_backup_banner.xml b/vector/src/main/res/layout/view_keys_backup_banner.xml index 87c92cf8b4..4c3ec1da3f 100644 --- a/vector/src/main/res/layout/view_keys_backup_banner.xml +++ b/vector/src/main/res/layout/view_keys_backup_banner.xml @@ -10,11 +10,11 @@ New Key Backup A new secure message key backup has been detected.\n\nIf you didn’t set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings. It was me + - Never lose encrypted messages - Start using Key Backup + Secure Backup + Safeguard against losing access to encrypted messages & data Never lose encrypted messages Use Key Backup @@ -1503,7 +1504,7 @@ Why choose Riot.im? New secure message keys Manage in Key Backup - Backing up keys… + Backing up your keys. This may take several minutes… All keys backed up From 332f227bc1fa7a7b505d428638d767c1ccdcde13 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 8 Jul 2020 09:59:13 +0200 Subject: [PATCH 040/122] Signout to setup 4S --- .../crosssigning/CrossSigningService.kt | 2 + .../DefaultCrossSigningService.kt | 7 + .../vector/riotx/core/di/ViewModelModule.kt | 1 - .../vector/riotx/core/extensions/Fragment.kt | 33 +++ .../vector/riotx/core/extensions/Session.kt | 9 + .../riotx/core/ui/views/KeysBackupBanner.kt | 3 - .../features/crypto/keys/KeysExporter.kt | 25 +- .../setup/KeysBackupSetupActivity.kt | 82 ++++-- .../setup/KeysBackupSetupStep2Fragment.kt | 8 +- .../recover/BootstrapCrossSigningTask.kt | 1 - .../recover/BootstrapSharedViewModel.kt | 5 +- .../riotx/features/home/HomeActivity.kt | 1 - .../riotx/features/home/HomeDetailFragment.kt | 2 - .../features/navigation/DefaultNavigator.kt | 1 - .../VectorSettingsSecurityPrivacyFragment.kt | 117 ++++---- .../signout/ServerBackupStatusViewModel.kt | 7 +- .../SignOutBottomSheetDialogFragment.kt | 272 +++++++++++------- .../workers/signout/SignOutUiWorker.kt | 4 +- .../signout/SignoutBottomSheetActionButton.kt | 95 ++++++ .../workers/signout/SignoutCheckViewModel.kt | 148 ++++++++++ .../layout/bottom_sheet_logout_and_backup.xml | 157 +++------- .../main/res/layout/item_signout_action.xml | 36 +++ vector/src/main/res/values/attrs.xml | 6 + vector/src/main/res/values/strings.xml | 4 + 24 files changed, 684 insertions(+), 342 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/workers/signout/SignoutBottomSheetActionButton.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/workers/signout/SignoutCheckViewModel.kt create mode 100644 vector/src/main/res/layout/item_signout_action.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt index 8d856d0860..5709e66581 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt @@ -61,6 +61,8 @@ interface CrossSigningService { fun canCrossSign(): Boolean + fun allPrivateKeysKnown(): Boolean + fun trustUser(otherUserId: String, callback: MatrixCallback) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt index 7c5f64182c..fdecfe202e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -507,6 +507,13 @@ internal class DefaultCrossSigningService @Inject constructor( && cryptoStore.getCrossSigningPrivateKeys()?.user != null } + override fun allPrivateKeysKnown(): Boolean { + return checkSelfTrust().isVerified() + && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null + && cryptoStore.getCrossSigningPrivateKeys()?.user != null + && cryptoStore.getCrossSigningPrivateKeys()?.master != null + } + override fun trustUser(otherUserId: String, callback: MatrixCallback) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { Timber.d("## CrossSigning - Mark user $userId as trusted ") diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index 2a3db0cf19..6ac6fa03da 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -36,7 +36,6 @@ import im.vector.riotx.features.reactions.EmojiChooserViewModel import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.riotx.features.roomprofile.RoomProfileSharedActionViewModel import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel -import im.vector.riotx.features.workers.signout.ServerBackupStatusViewModel @Module interface ViewModelModule { diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt index c28dcf12d3..7c1cae3644 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt @@ -16,9 +16,16 @@ package im.vector.riotx.core.extensions +import android.content.ActivityNotFoundException +import android.content.Intent import android.os.Parcelable import androidx.fragment.app.Fragment +import im.vector.riotx.R import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.utils.toast +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale fun VectorBaseFragment.addFragment(frameId: Int, fragment: Fragment) { parentFragmentManager.commitTransactionNow { add(frameId, fragment) } @@ -89,3 +96,29 @@ fun Fragment.getAllChildFragments(): List { // Define a missing constant const val POP_BACK_STACK_EXCLUSIVE = 0 + +fun Fragment.queryExportKeys(userId: String, requestCode: Int) { + // We need WRITE_EXTERNAL permission +// if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, +// this, +// PERMISSION_REQUEST_CODE_EXPORT_KEYS, +// R.string.permissions_rationale_msg_keys_backup_export)) { + // WRITE permissions are not needed + val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).let { + it.format(Date()) + } + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "text/plain" + intent.putExtra( + Intent.EXTRA_TITLE, + "riot-megolm-export-$userId-$timestamp.txt" + ) + + try { + startActivityForResult(Intent.createChooser(intent, getString(R.string.keys_backup_setup_step1_manual_export)), requestCode) + } catch (activityNotFoundException: ActivityNotFoundException) { + activity?.toast(R.string.error_no_external_application_found) + } +// } +} diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt index 29b169ffd4..1ad6fb9090 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt @@ -65,3 +65,12 @@ fun Session.hasUnsavedKeys(): Boolean { return cryptoService().inboundGroupSessionsCount(false) > 0 && cryptoService().keysBackupService().state != KeysBackupState.ReadyToBackUp } + +fun Session.cannotLogoutSafely(): Boolean { + // has some encrypted chat + return hasUnsavedKeys() + // has local cross signing keys + || (cryptoService().crossSigningService().allPrivateKeysKnown() + // That are not backed up + && !sharedSecretStorageService.isRecoverySetup()) +} diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt index 460c871288..252eab02a6 100755 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt @@ -21,13 +21,10 @@ import androidx.preference.PreferenceManager import android.util.AttributeSet import android.view.View import android.view.ViewGroup -import android.widget.AbsListView import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout -import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.edit import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView import androidx.transition.TransitionManager import butterknife.BindView import butterknife.ButterKnife diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt index b9b75588f1..2467334f69 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt @@ -17,37 +17,34 @@ package im.vector.riotx.features.crypto.keys import android.content.Context -import android.os.Environment +import android.net.Uri import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.util.awaitCallback -import im.vector.riotx.core.files.addEntryToDownloadManager -import im.vector.riotx.core.files.writeToFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.io.File class KeysExporter(private val session: Session) { /** * Export keys and return the file path with the callback */ - fun export(context: Context, password: String, callback: MatrixCallback) { + fun export(context: Context, password: String, uri: Uri, callback: MatrixCallback) { GlobalScope.launch(Dispatchers.Main) { runCatching { - val data = awaitCallback { session.cryptoService().exportRoomKeys(password, it) } withContext(Dispatchers.IO) { - val parentDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) - val file = File(parentDir, "riotx-keys-" + System.currentTimeMillis() + ".txt") - - writeToFile(data, file) - - addEntryToDownloadManager(context, file, "text/plain") - - file.absolutePath + val data = awaitCallback { session.cryptoService().exportRoomKeys(password, it) } + val os = context.contentResolver?.openOutputStream(uri) + if (os == null) { + false + } else { + os.write(data) + os.flush() + true + } } }.foldToCallback(callback) } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt index c7d3da30ea..1371ce7021 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt @@ -15,6 +15,8 @@ */ package im.vector.riotx.features.crypto.keysbackup.setup +import android.app.Activity +import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import androidx.appcompat.app.AlertDialog @@ -132,36 +134,16 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() { this, PERMISSION_REQUEST_CODE_EXPORT_KEYS, R.string.permissions_rationale_msg_keys_backup_export)) { - ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener { - override fun onPassphrase(passphrase: String) { - showWaitingView() + try { + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "text/plain" + intent.putExtra(Intent.EXTRA_TITLE, "riot-megolm-export-${session.myUserId}-${System.currentTimeMillis()}.txt") - KeysExporter(session) - .export(this@KeysBackupSetupActivity, - passphrase, - object : MatrixCallback { - override fun onSuccess(data: String) { - hideWaitingView() - - AlertDialog.Builder(this@KeysBackupSetupActivity) - .setMessage(getString(R.string.encryption_export_saved_as, data)) - .setCancelable(false) - .setPositiveButton(R.string.ok) { _, _ -> - val resultIntent = Intent() - resultIntent.putExtra(MANUAL_EXPORT, true) - setResult(RESULT_OK, resultIntent) - finish() - } - .show() - } - - override fun onFailure(failure: Throwable) { - toast(failure.localizedMessage ?: getString(R.string.unexpected_error)) - hideWaitingView() - } - }) - } - }) + startActivityForResult(Intent.createChooser(intent, getString(R.string.keys_backup_setup_step1_manual_export)), REQUEST_CODE_SAVE_MEGOLM_EXPORT) + } catch (activityNotFoundException: ActivityNotFoundException) { + toast(R.string.error_no_external_application_found) + } } } @@ -173,6 +155,47 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() { } } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == REQUEST_CODE_SAVE_MEGOLM_EXPORT) { + val uri = data?.data + if (resultCode == Activity.RESULT_OK && uri != null) { + ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener { + override fun onPassphrase(passphrase: String) { + showWaitingView() + + KeysExporter(session) + .export(this@KeysBackupSetupActivity, + passphrase, + uri, + object : MatrixCallback { + override fun onSuccess(data: Boolean) { + if (data) { + toast(getString(R.string.encryption_exported_successfully)) + Intent().apply { + putExtra(MANUAL_EXPORT, true) + }.let { + setResult(Activity.RESULT_OK, it) + finish() + } + } + hideWaitingView() + } + + override fun onFailure(failure: Throwable) { + toast(failure.localizedMessage ?: getString(R.string.unexpected_error)) + hideWaitingView() + } + }) + } + }) + } else { + toast(getString(R.string.unexpected_error)) + hideWaitingView() + } + } + super.onActivityResult(requestCode, resultCode, data) + } + override fun onBackPressed() { if (viewModel.shouldPromptOnBack) { if (waitingView?.isVisible == true) { @@ -205,6 +228,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() { const val KEYS_VERSION = "KEYS_VERSION" const val MANUAL_EXPORT = "MANUAL_EXPORT" const val EXTRA_SHOW_MANUAL_EXPORT = "SHOW_MANUAL_EXPORT" + const val REQUEST_CODE_SAVE_MEGOLM_EXPORT = 101 fun intent(context: Context, showManualExport: Boolean): Intent { val intent = Intent(context, KeysBackupSetupActivity::class.java) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt index a3306677fe..40ea79eb6d 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt @@ -15,13 +15,13 @@ */ package im.vector.riotx.features.crypto.keysbackup.setup -import android.os.AsyncTask import android.os.Bundle import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.widget.EditText import android.widget.ImageView import androidx.lifecycle.Observer +import androidx.lifecycle.viewModelScope import androidx.transition.TransitionManager import butterknife.BindView import butterknife.OnClick @@ -33,6 +33,8 @@ import im.vector.riotx.core.extensions.showPassword import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.ui.views.PasswordStrengthBar import im.vector.riotx.features.settings.VectorLocale +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import javax.inject.Inject class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment() { @@ -117,9 +119,9 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment() if (newValue.isEmpty()) { viewModel.passwordStrength.value = null } else { - AsyncTask.execute { + viewModel.viewModelScope.launch(Dispatchers.IO) { val strength = zxcvbn.measure(newValue) - activity?.runOnUiThread { + launch(Dispatchers.Main) { viewModel.passwordStrength.value = strength } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt index 290a08bfad..9f68e09444 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -168,7 +168,6 @@ class BootstrapCrossSigningTask @Inject constructor( return BootstrapResult.FailedToSetDefaultSSSSKey(failure) } - Timber.d("## BootstrapCrossSigningTask: Creating 4S - gathering private keys") val xKeys = crossSigningService.getCrossSigningPrivateKeys() val mskPrivateKey = xKeys?.master ?: return BootstrapResult.MissingPrivateKey diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt index 3a95a575f4..22dcab217e 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt @@ -406,7 +406,10 @@ class BootstrapSharedViewModel @AssistedInject constructor( setState { copy( recoveryKeyCreationInfo = bootstrapResult.keyInfo, - step = BootstrapStep.SaveRecoveryKey(false) + step = BootstrapStep.SaveRecoveryKey( + // If a passphrase was used, saving key is optional + state.passphrase != null + ) ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index 6991aaa493..ef0757be21 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -68,7 +68,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet private val homeActivityViewModel: HomeActivityViewModel by viewModel() @Inject lateinit var viewModelFactory: HomeActivityViewModel.Factory - private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel() @Inject lateinit var serverBackupviewModelFactory: ServerBackupStatusViewModel.Factory diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index c736c0c1ca..f0fdc207f9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -21,7 +21,6 @@ import android.view.LayoutInflater import android.view.View import androidx.core.content.ContextCompat import androidx.core.view.forEachIndexed -import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel @@ -194,7 +193,6 @@ class HomeDetailFragment @Inject constructor( } private fun setupKeysBackupBanner() { - serverBackupStatusViewModel.subscribe(this) { when (val banState = it.bannerState.invoke()) { is BannerState.Setup -> homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false) diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 79ba5121fc..006af48133 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -207,7 +207,6 @@ class DefaultNavigator @Inject constructor( context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport)) } } - } override fun openKeysBackupManager(context: Context) { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt index 2b9338ccc8..3c2acb1693 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -34,16 +34,16 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.riotx.R +import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.dialogs.ExportKeysDialog +import im.vector.riotx.core.extensions.queryExportKeys import im.vector.riotx.core.intent.ExternalIntentData import im.vector.riotx.core.intent.analyseIntent import im.vector.riotx.core.intent.getFilenameFromUri import im.vector.riotx.core.platform.SimpleTextWatcher import im.vector.riotx.core.preference.VectorPreference -import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_EXPORT_KEYS import im.vector.riotx.core.utils.allGranted -import im.vector.riotx.core.utils.checkPermissions import im.vector.riotx.core.utils.openFileSelection import im.vector.riotx.core.utils.toast import im.vector.riotx.features.crypto.keys.KeysExporter @@ -52,7 +52,8 @@ import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActiv import javax.inject.Inject class VectorSettingsSecurityPrivacyFragment @Inject constructor( - private val vectorPreferences: VectorPreferences + private val vectorPreferences: VectorPreferences, + private val activeSessionHolder: ActiveSessionHolder ) : VectorSettingsBaseFragment() { override var titleRes = R.string.settings_security_and_privacy @@ -119,38 +120,69 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( } private fun refreshXSigningStatus() { - val xSigningIsEnableInAccount = session.cryptoService().crossSigningService().isCrossSigningInitialized() - val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified() - val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign() + val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys() + val xSigningIsEnableInAccount = crossSigningKeys != null + val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified() + val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign() - if (xSigningKeyCanSign) { - mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted) - mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete) - } else if (xSigningKeysAreTrusted) { - mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom) - mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted) - } else if (xSigningIsEnableInAccount) { - mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black) - mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted) - } else { - mCrossSigningStatePreference.setIcon(android.R.color.transparent) - mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_disabled) - } + if (xSigningKeyCanSign) { + mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted) + mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete) + } else if (xSigningKeysAreTrusted) { + mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom) + mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted) + } else if (xSigningIsEnableInAccount) { + mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black) + mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted) + } else { + mCrossSigningStatePreference.setIcon(android.R.color.transparent) + mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_disabled) + } - mCrossSigningStatePreference.isVisible = true + mCrossSigningStatePreference.isVisible = true } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { if (allGranted(grantResults)) { if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) { - exportKeys() + queryExportKeys(activeSessionHolder.getSafeActiveSession()?.myUserId ?: "", REQUEST_CODE_SAVE_MEGOLM_EXPORT) } } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQUEST_CODE_SAVE_MEGOLM_EXPORT) { + val uri = data?.data + if (resultCode == Activity.RESULT_OK && uri != null) { + activity?.let { activity -> + ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener { + override fun onPassphrase(passphrase: String) { + displayLoadingView() + KeysExporter(session) + .export(requireContext(), + passphrase, + uri, + object : MatrixCallback { + override fun onSuccess(data: Boolean) { + if (data) { + requireActivity().toast(getString(R.string.encryption_exported_successfully)) + } else { + requireActivity().toast(getString(R.string.unexpected_error)) + } + hideLoadingView() + } + + override fun onFailure(failure: Throwable) { + onCommonDone(failure.localizedMessage) + } + }) + } + }) + } + } + } if (resultCode == Activity.RESULT_OK) { when (requestCode) { REQUEST_E2E_FILE_REQUEST_CODE -> importKeys(data) @@ -169,7 +201,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( } exportPref.onPreferenceClickListener = Preference.OnPreferenceClickListener { - exportKeys() + queryExportKeys(activeSessionHolder.getSafeActiveSession()?.myUserId ?: "", REQUEST_CODE_SAVE_MEGOLM_EXPORT) true } @@ -179,46 +211,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( } } - /** - * Manage the e2e keys export. - */ - private fun exportKeys() { - // We need WRITE_EXTERNAL permission - if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, - this, - PERMISSION_REQUEST_CODE_EXPORT_KEYS, - R.string.permissions_rationale_msg_keys_backup_export)) { - activity?.let { activity -> - ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener { - override fun onPassphrase(passphrase: String) { - displayLoadingView() - - KeysExporter(session) - .export(requireContext(), - passphrase, - object : MatrixCallback { - override fun onSuccess(data: String) { - if (isAdded) { - hideLoadingView() - - AlertDialog.Builder(activity) - .setMessage(getString(R.string.encryption_export_saved_as, data)) - .setCancelable(false) - .setPositiveButton(R.string.ok, null) - .show() - } - } - - override fun onFailure(failure: Throwable) { - onCommonDone(failure.localizedMessage) - } - }) - } - }) - } - } - } - /** * Manage the e2e keys import. */ @@ -515,6 +507,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( companion object { private const val REQUEST_E2E_FILE_REQUEST_CODE = 123 + private const val REQUEST_CODE_SAVE_MEGOLM_EXPORT = 124 private const val PUSHER_PREFERENCE_KEY_BASE = "PUSHER_PREFERENCE_KEY_BASE" private const val DEVICES_PREFERENCE_KEY_BASE = "DEVICES_PREFERENCE_KEY_BASE" diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt index 04ece8e407..dca98c16b2 100644 --- a/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt @@ -94,9 +94,8 @@ class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialS init { session.cryptoService().keysBackupService().addListener(this) - keyBackupPublishSubject.onNext(session.cryptoService().keysBackupService().state) keysBackupState.value = session.cryptoService().keysBackupService().state - session.rx().liveCrossSigningPrivateKeys() + Observable.combineLatest, Optional, KeysBackupState, Optional, BannerState>( session.rx().liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME)), session.rx().liveCrossSigningInfo(session.myUserId), @@ -126,13 +125,15 @@ class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialS BannerState.Hidden } ) - .throttleLast(2000, TimeUnit.MILLISECONDS) // we don't want to flicker or catch transient states + .throttleLast(1000, TimeUnit.MILLISECONDS) // we don't want to flicker or catch transient states .distinctUntilChanged() .execute { async -> copy( bannerState = async ) } + + keyBackupPublishSubject.onNext(session.cryptoService().keysBackupService().state) } /** diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt index fa5c4119ed..16be661f06 100644 --- a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt @@ -28,24 +28,27 @@ import android.widget.ProgressBar import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible -import androidx.lifecycle.Observer -import androidx.transition.TransitionManager import butterknife.BindView +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog +import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.dialogs.ExportKeysDialog +import im.vector.riotx.core.extensions.queryExportKeys import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment -import im.vector.riotx.core.utils.toast -import im.vector.riotx.features.attachments.preview.AttachmentsPreviewViewModel -import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity -import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsViewModel +import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet +import timber.log.Timber import javax.inject.Inject -class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), ServerBackupStatusViewModel.Factory { +// TODO this needs to be refactored to current standard and remove legacy +class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), SignoutCheckViewModel.Factory { @BindView(R.id.bottom_sheet_signout_warning_text) lateinit var sheetTitle: TextView @@ -53,14 +56,20 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), @BindView(R.id.bottom_sheet_signout_backingup_status_group) lateinit var backingUpStatusGroup: ViewGroup - @BindView(R.id.keys_backup_setup) - lateinit var setupClickableView: View + @BindView(R.id.setupRecoveryButton) + lateinit var setupRecoveryButton: SignoutBottomSheetActionButton - @BindView(R.id.keys_backup_activate) - lateinit var activateClickableView: View + @BindView(R.id.setupMegolmBackupButton) + lateinit var setupMegolmBackupButton: SignoutBottomSheetActionButton - @BindView(R.id.keys_backup_dont_want) - lateinit var dontWantClickableView: View + @BindView(R.id.exportManuallyButton) + lateinit var exportManuallyButton: SignoutBottomSheetActionButton + + @BindView(R.id.exitAnywayButton) + lateinit var exitAnywayButton: SignoutBottomSheetActionButton + + @BindView(R.id.signOutButton) + lateinit var signOutButton: SignoutBottomSheetActionButton @BindView(R.id.bottom_sheet_signout_icon_progress_bar) lateinit var backupProgress: ProgressBar @@ -71,8 +80,8 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), @BindView(R.id.bottom_sheet_backup_status_text) lateinit var backupStatusTex: TextView - @BindView(R.id.bottom_sheet_signout_button) - lateinit var signoutClickableView: View + @BindView(R.id.signoutExportingLoading) + lateinit var signoutExportingLoading: View @BindView(R.id.root_layout) lateinit var rootLayout: ViewGroup @@ -83,6 +92,7 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), fun newInstance() = SignOutBottomSheetDialogFragment() private const val EXPORT_REQ = 0 + private const val QUERY_EXPORT_KEYS = 1 } init { @@ -90,65 +100,36 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), } @Inject - lateinit var viewModelFactory: ServerBackupStatusViewModel.Factory + lateinit var viewModelFactory: SignoutCheckViewModel.Factory - override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel { + override fun create(initialState: SignoutCheckViewState): SignoutCheckViewModel { return viewModelFactory.create(initialState) } - private val viewModel: ServerBackupStatusViewModel by fragmentViewModel(ServerBackupStatusViewModel::class) + private val viewModel: SignoutCheckViewModel by fragmentViewModel(SignoutCheckViewModel::class) override fun injectWith(injector: ScreenComponent) { injector.inject(this) } + override fun onResume() { + super.onResume() + viewModel.refreshRemoteStateIfNeeded() + } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - setupClickableView.setOnClickListener { - context?.let { context -> - startActivityForResult(KeysBackupSetupActivity.intent(context, true), EXPORT_REQ) - } + setupRecoveryButton.action = { + BootstrapBottomSheet.show(parentFragmentManager, false) } - activateClickableView.setOnClickListener { - context?.let { context -> - startActivity(KeysBackupManageActivity.intent(context)) - } - } - - signoutClickableView.setOnClickListener { - this.onSignOut?.run() - } - - dontWantClickableView.setOnClickListener { _ -> + exitAnywayButton.action = { context?.let { AlertDialog.Builder(it) .setTitle(R.string.are_you_sure) .setMessage(R.string.sign_out_bottom_sheet_will_lose_secure_messages) - .setPositiveButton(R.string.backup) { _, _ -> - when (viewModel.keysBackupState.value) { - KeysBackupState.NotTrusted -> { - context?.let { context -> - startActivity(KeysBackupManageActivity.intent(context)) - } - } - KeysBackupState.Disabled -> { - context?.let { context -> - startActivityForResult(KeysBackupSetupActivity.intent(context, true), EXPORT_REQ) - } - } - KeysBackupState.BackingUp, - KeysBackupState.WillBackUp -> { - // keys are already backing up please wait - context?.toast(R.string.keys_backup_is_not_finished_please_wait) - } - else -> { - // nop - } - } - } + .setPositiveButton(R.string.backup, null) .setNegativeButton(R.string.action_sign_out) { _, _ -> onSignOut?.run() } @@ -156,71 +137,143 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), } } - viewModel.keysExportedToFile.observe(viewLifecycleOwner, Observer { - val hasExportedToFile = it ?: false - if (hasExportedToFile) { - // We can allow to sign out - - sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple) - - signoutClickableView.isVisible = true - dontWantClickableView.isVisible = false - setupClickableView.isVisible = false - activateClickableView.isVisible = false - backingUpStatusGroup.isVisible = false + exportManuallyButton.action = { + withState(viewModel) { state -> + queryExportKeys(state.userId, QUERY_EXPORT_KEYS) } - }) + } - viewModel.keysBackupState.observe(viewLifecycleOwner, Observer { - if (viewModel.keysExportedToFile.value == true) { - // ignore this - return@Observer - } - TransitionManager.beginDelayedTransition(rootLayout) + setupMegolmBackupButton.action = { + startActivityForResult(KeysBackupSetupActivity.intent(requireContext(), true), EXPORT_REQ) + } + + viewModel.observeViewEvents { when (it) { - KeysBackupState.ReadyToBackUp -> { - signoutClickableView.isVisible = true - dontWantClickableView.isVisible = false - setupClickableView.isVisible = false - activateClickableView.isVisible = false - backingUpStatusGroup.isVisible = true + is SignoutCheckViewModel.ViewEvents.ExportKeys -> { + it.exporter + .export(requireContext(), + it.passphrase, + it.uri, + object : MatrixCallback { + override fun onSuccess(data: Boolean) { + if (data) { + viewModel.handle(SignoutCheckViewModel.Actions.KeySuccessfullyManuallyExported) + } else { + viewModel.handle(SignoutCheckViewModel.Actions.KeyExportFailed) + } + } + override fun onFailure(failure: Throwable) { + Timber.e("## Failed to export manually keys ${failure.localizedMessage}") + viewModel.handle(SignoutCheckViewModel.Actions.KeyExportFailed) + } + }) + } + } + } + } + + override fun invalidate() = withState(viewModel) { state -> + signoutExportingLoading.isVisible = false + if (state.crossSigningSetupAllKeysKnown && !state.backupIsSetup) { + sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_no_backup) + backingUpStatusGroup.isVisible = false + // we should show option to setup 4S + setupRecoveryButton.isVisible = true + setupMegolmBackupButton.isVisible = false + signOutButton.isVisible = false + // We let the option to ignore and quit + exportManuallyButton.isVisible = true + exitAnywayButton.isVisible = true + } else if (state.keysBackupState == KeysBackupState.Unknown || state.keysBackupState == KeysBackupState.Disabled) { + sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_no_backup) + backingUpStatusGroup.isVisible = false + // no key backup and cannot setup full 4S + // we propose to setup + // we should show option to setup 4S + setupRecoveryButton.isVisible = false + setupMegolmBackupButton.isVisible = true + signOutButton.isVisible = false + // We let the option to ignore and quit + exportManuallyButton.isVisible = true + exitAnywayButton.isVisible = true + } else { + // so keybackup is setup + // You should wait until all are uploaded + setupRecoveryButton.isVisible = false + + when (state.keysBackupState) { + KeysBackupState.ReadyToBackUp -> { + sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple) + + // Ok all keys are backedUp + backingUpStatusGroup.isVisible = true backupProgress.isVisible = false backupCompleteImage.isVisible = true backupStatusTex.text = getString(R.string.keys_backup_info_keys_all_backup_up) - sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple) + hideViews(setupMegolmBackupButton, exportManuallyButton, exitAnywayButton) + // You can signout + signOutButton.isVisible = true } - KeysBackupState.BackingUp, - KeysBackupState.WillBackUp -> { - backingUpStatusGroup.isVisible = true - sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_backing_up) - dontWantClickableView.isVisible = true - setupClickableView.isVisible = false - activateClickableView.isVisible = false + KeysBackupState.WillBackUp, + KeysBackupState.BackingUp -> { + sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_backing_up) + + // save in progress + backingUpStatusGroup.isVisible = true backupProgress.isVisible = true backupCompleteImage.isVisible = false backupStatusTex.text = getString(R.string.sign_out_bottom_sheet_backing_up_keys) + + hideViews(setupMegolmBackupButton, setupMegolmBackupButton, signOutButton, exportManuallyButton) + exitAnywayButton.isVisible = true } KeysBackupState.NotTrusted -> { - backingUpStatusGroup.isVisible = false - dontWantClickableView.isVisible = true - setupClickableView.isVisible = false - activateClickableView.isVisible = true sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_backup_not_active) + // It's not trusted and we know there are unsaved keys.. + backingUpStatusGroup.isVisible = false + + exportManuallyButton.isVisible = true + // option to enter pass/key + setupMegolmBackupButton.isVisible = true + exitAnywayButton.isVisible = true } else -> { - backingUpStatusGroup.isVisible = false - dontWantClickableView.isVisible = true - setupClickableView.isVisible = true - activateClickableView.isVisible = false - sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_no_backup) + // mmm.. strange state + + exitAnywayButton.isVisible = true } } + } - // updateSignOutSection() - }) + // final call if keys have been exported + when (state.hasBeenExportedToFile) { + is Loading -> { + signoutExportingLoading.isVisible = true + hideViews(setupRecoveryButton, + setupMegolmBackupButton, + exportManuallyButton, + backingUpStatusGroup, + signOutButton) + exitAnywayButton.isVisible = true + } + is Success -> { + if (state.hasBeenExportedToFile.invoke()) { + sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple) + hideViews(setupRecoveryButton, + setupMegolmBackupButton, + exportManuallyButton, + backingUpStatusGroup, + exitAnywayButton) + signOutButton.isVisible = true + } + } + else -> { + } + } + super.invalidate() } override fun getLayoutResId() = R.layout.bottom_sheet_logout_and_backup @@ -243,11 +296,26 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), super.onActivityResult(requestCode, resultCode, data) if (resultCode == Activity.RESULT_OK) { - if (requestCode == EXPORT_REQ) { - val manualExportDone = data?.getBooleanExtra(KeysBackupSetupActivity.MANUAL_EXPORT, false) - viewModel.keysExportedToFile.value = manualExportDone + if (requestCode == QUERY_EXPORT_KEYS) { + val uri = data?.data + if (resultCode == Activity.RESULT_OK && uri != null) { + activity?.let { activity -> + ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener { + override fun onPassphrase(passphrase: String) { + viewModel.handle(SignoutCheckViewModel.Actions.ExportKeys(passphrase, uri)) + } + }) + } + } + } else if (requestCode == EXPORT_REQ) { + if (data?.getBooleanExtra(KeysBackupSetupActivity.MANUAL_EXPORT, false) == true) { + viewModel.handle(SignoutCheckViewModel.Actions.KeySuccessfullyManuallyExported) + } } } } + private fun hideViews(vararg views: View) { + views.forEach { it.isVisible = false } + } } diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutUiWorker.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutUiWorker.kt index e51fda2be5..e06a47d3d4 100644 --- a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutUiWorker.kt +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutUiWorker.kt @@ -21,7 +21,7 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.FragmentActivity import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder -import im.vector.riotx.core.extensions.hasUnsavedKeys +import im.vector.riotx.core.extensions.cannotLogoutSafely import im.vector.riotx.core.extensions.vectorComponent import im.vector.riotx.features.MainActivity import im.vector.riotx.features.MainActivityArgs @@ -33,7 +33,7 @@ class SignOutUiWorker(private val activity: FragmentActivity) { fun perform(context: Context) { activeSessionHolder = context.vectorComponent().activeSessionHolder() val session = activeSessionHolder.getActiveSession() - if (session.hasUnsavedKeys()) { + if (session.cannotLogoutSafely()) { // The backup check on logout flow has to be displayed if there are keys in the store, and the keys backup state is not Ready val signOutDialog = SignOutBottomSheetDialogFragment.newInstance() signOutDialog.onSignOut = Runnable { diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignoutBottomSheetActionButton.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignoutBottomSheetActionButton.kt new file mode 100644 index 0000000000..cd5e4ed9da --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignoutBottomSheetActionButton.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2020 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.riotx.features.workers.signout + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.view.isVisible +import butterknife.BindView +import butterknife.ButterKnife +import im.vector.riotx.R +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.features.themes.ThemeUtils + +class SignoutBottomSheetActionButton @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + @BindView(R.id.actionTitleText) + lateinit var actionTextView: TextView + + @BindView(R.id.actionIconImageView) + lateinit var iconImageView: ImageView + + @BindView(R.id.signedOutActionClickable) + lateinit var clickableZone: View + + var action: (() -> Unit)? = null + + var title: String? = null + set(value) { + field = value + actionTextView.setTextOrHide(value) + } + + var leftIcon: Drawable? = null + set(value) { + field = value + if (value == null) { + iconImageView.isVisible = false + iconImageView.setImageDrawable(null) + } else { + iconImageView.isVisible = true + iconImageView.setImageDrawable(value) + } + } + + var tint: Int? = null + set(value) { + field = value + iconImageView.imageTintList = value?.let { ColorStateList.valueOf(value) } + } + + var textColor: Int? = null + set(value) { + field = value + textColor?.let { actionTextView.setTextColor(it) } + } + + init { + inflate(context, R.layout.item_signout_action, this) + ButterKnife.bind(this) + + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.SignoutBottomSheetActionButton, 0, 0) + title = typedArray.getString(R.styleable.SignoutBottomSheetActionButton_actionTitle) ?: "" + leftIcon = typedArray.getDrawable(R.styleable.SignoutBottomSheetActionButton_leftIcon) + tint = typedArray.getColor(R.styleable.SignoutBottomSheetActionButton_iconTint, ThemeUtils.getColor(context, android.R.attr.textColor)) + textColor = typedArray.getColor(R.styleable.SignoutBottomSheetActionButton_textColor, ThemeUtils.getColor(context, android.R.attr.textColor)) + + typedArray.recycle() + + clickableZone.setOnClickListener { + action?.invoke() + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignoutCheckViewModel.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignoutCheckViewModel.kt new file mode 100644 index 0000000000..47da7d4edc --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignoutCheckViewModel.kt @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2020 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.riotx.features.workers.signout + +import android.net.Uri +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener +import im.vector.matrix.rx.rx +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.platform.VectorViewEvents +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.platform.VectorViewModelAction +import im.vector.riotx.features.crypto.keys.KeysExporter + +data class SignoutCheckViewState( + val userId: String = "", + val backupIsSetup: Boolean = false, + val crossSigningSetupAllKeysKnown: Boolean = false, + val keysBackupState: KeysBackupState = KeysBackupState.Unknown, + val hasBeenExportedToFile: Async = Uninitialized +) : MvRxState + +class SignoutCheckViewModel @AssistedInject constructor(@Assisted initialState: SignoutCheckViewState, + private val session: Session) + : VectorViewModel(initialState), KeysBackupStateListener { + + sealed class Actions : VectorViewModelAction { + data class ExportKeys(val passphrase: String, val uri: Uri) : Actions() + object KeySuccessfullyManuallyExported : Actions() + object KeyExportFailed : Actions() + } + + sealed class ViewEvents : VectorViewEvents { + data class ExportKeys(val exporter: KeysExporter, val passphrase: String, val uri: Uri) : ViewEvents() + } + + @AssistedInject.Factory + interface Factory { + fun create(initialState: SignoutCheckViewState): SignoutCheckViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: SignoutCheckViewState): SignoutCheckViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + init { + session.cryptoService().keysBackupService().addListener(this) + session.cryptoService().keysBackupService().checkAndStartKeysBackup() + + val quad4SIsSetup = session.sharedSecretStorageService.isRecoverySetup() + val allKeysKnown = session.cryptoService().crossSigningService().allPrivateKeysKnown() + val backupState = session.cryptoService().keysBackupService().state + setState { + copy( + userId = session.myUserId, + crossSigningSetupAllKeysKnown = allKeysKnown, + backupIsSetup = quad4SIsSetup, + keysBackupState = backupState + ) + } + + session.rx().liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME)) + .map { + session.sharedSecretStorageService.isRecoverySetup() + } + .distinctUntilChanged() + .execute { + copy(backupIsSetup = it.invoke() == true) + } + } + + override fun onCleared() { + super.onCleared() + session.cryptoService().keysBackupService().removeListener(this) + } + + override fun onStateChange(newState: KeysBackupState) { + setState { + copy( + keysBackupState = newState + ) + } + } + + fun refreshRemoteStateIfNeeded() = withState { state -> + if (state.keysBackupState == KeysBackupState.Disabled) { + session.cryptoService().keysBackupService().checkAndStartKeysBackup() + } + } + + override fun handle(action: Actions) { + when (action) { + is Actions.ExportKeys -> { + setState { + copy(hasBeenExportedToFile = Loading()) + } + _viewEvents.post(ViewEvents.ExportKeys(KeysExporter(session), action.passphrase, action.uri)) + } + Actions.KeySuccessfullyManuallyExported -> { + setState { + copy(hasBeenExportedToFile = Success(true)) + } + } + Actions.KeyExportFailed -> { + setState { + copy(hasBeenExportedToFile = Uninitialized) + } + } + }.exhaustive + } +} diff --git a/vector/src/main/res/layout/bottom_sheet_logout_and_backup.xml b/vector/src/main/res/layout/bottom_sheet_logout_and_backup.xml index feaa79e1dc..c6605dfc05 100644 --- a/vector/src/main/res/layout/bottom_sheet_logout_and_backup.xml +++ b/vector/src/main/res/layout/bottom_sheet_logout_and_backup.xml @@ -70,137 +70,60 @@ + android:layout_height="44dp" + android:gravity="center"> - - - - + android:layout_height="wrap_content" /> - - - - - - - + app:actionTitle="@string/secure_backup_setup" + app:iconTint="?riotx_text_primary" + app:leftIcon="@drawable/ic_secure_backup" + app:textColor="?riotx_text_secondary" /> - + app:actionTitle="@string/keys_backup_setup" + app:iconTint="?riotx_text_primary" + app:leftIcon="@drawable/backup_keys" + app:textColor="?riotx_text_secondary" /> - - - - - - + app:actionTitle="@string/keys_backup_setup_step1_manual_export" + app:iconTint="?riotx_text_primary" + app:leftIcon="@drawable/ic_download" + app:textColor="?riotx_text_secondary" /> - - - - + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_signout_action.xml b/vector/src/main/res/layout/item_signout_action.xml new file mode 100644 index 0000000000..c5acc09e56 --- /dev/null +++ b/vector/src/main/res/layout/item_signout_action.xml @@ -0,0 +1,36 @@ + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/attrs.xml b/vector/src/main/res/values/attrs.xml index 27d53fe90e..80ecf32029 100644 --- a/vector/src/main/res/values/attrs.xml +++ b/vector/src/main/res/values/attrs.xml @@ -114,4 +114,10 @@ + + + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 01386dde56..8451191273 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1048,6 +1048,7 @@ Export Please create a passphrase to encrypt the exported keys. You will need to enter the same passphrase to be able to import the keys. The E2E room keys have been saved to \'%s\'.\n\nWarning: this file may be deleted if the application is uninstalled. + Keys successfully exported Encrypted Messages Recovery Manage Key Backup @@ -1506,6 +1507,9 @@ Why choose Riot.im? Backing up your keys. This may take several minutes… + + Set Up Secure Backup + All keys backed up From 0c2516ccf8390d7cc0db72df601a2e4c259a8234 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 9 Jul 2020 15:47:59 +0200 Subject: [PATCH 041/122] line too long --- .../crypto/keysbackup/setup/KeysBackupSetupActivity.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt index 1371ce7021..b99c0e4330 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt @@ -140,7 +140,13 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() { intent.type = "text/plain" intent.putExtra(Intent.EXTRA_TITLE, "riot-megolm-export-${session.myUserId}-${System.currentTimeMillis()}.txt") - startActivityForResult(Intent.createChooser(intent, getString(R.string.keys_backup_setup_step1_manual_export)), REQUEST_CODE_SAVE_MEGOLM_EXPORT) + startActivityForResult( + Intent.createChooser( + intent, + getString(R.string.keys_backup_setup_step1_manual_export) + ), + REQUEST_CODE_SAVE_MEGOLM_EXPORT + ) } catch (activityNotFoundException: ActivityNotFoundException) { toast(R.string.error_no_external_application_found) } @@ -172,7 +178,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() { if (data) { toast(getString(R.string.encryption_exported_successfully)) Intent().apply { - putExtra(MANUAL_EXPORT, true) + putExtra(MANUAL_EXPORT, true) }.let { setResult(Activity.RESULT_OK, it) finish() From 548879bd9f8f91b4423f20d5b0905fc406f3d5a1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 9 Jul 2020 17:20:51 +0200 Subject: [PATCH 042/122] Fix encryption enabling visible for all users --- CHANGES.md | 1 + .../room/powerlevels/PowerLevelsHelper.kt | 56 ------------------- .../roomprofile/RoomProfileViewModel.kt | 5 +- .../settings/RoomSettingsController.kt | 8 +++ .../settings/RoomSettingsViewModel.kt | 9 +-- .../settings/RoomSettingsViewState.kt | 3 +- 6 files changed, 20 insertions(+), 62 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index da6f0abf44..e0a0085af5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,7 @@ Bugfix 🐛: - Integration Manager: Wrong URL to review terms if URL in config contains path (#1606) - Regression Composer does not grow, crops out text (#1650) - Bug / Unwanted draft (#698) + - All users seems to be able to see the enable encryption option in room settings (#1341) Translations 🗣: - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsHelper.kt index 6361a46bac..f434859f6e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsHelper.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.api.session.room.powerlevels -import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.model.PowerLevelsContent /** @@ -124,59 +123,4 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { else -> Role.Moderator.value } } - - /** - * Check if user have the necessary power level to change room name - * @param userId the id of the user to check for. - * @return true if able to change room name - */ - fun isUserAbleToChangeRoomName(userId: String): Boolean { - val powerLevel = getUserPowerLevelValue(userId) - val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_NAME] ?: powerLevelsContent.stateDefault - return powerLevel >= minPowerLevel - } - - /** - * Check if user have the necessary power level to change room topic - * @param userId the id of the user to check for. - * @return true if able to change room topic - */ - fun isUserAbleToChangeRoomTopic(userId: String): Boolean { - val powerLevel = getUserPowerLevelValue(userId) - val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_TOPIC] ?: powerLevelsContent.stateDefault - return powerLevel >= minPowerLevel - } - - /** - * Check if user have the necessary power level to change room canonical alias - * @param userId the id of the user to check for. - * @return true if able to change room canonical alias - */ - fun isUserAbleToChangeRoomCanonicalAlias(userId: String): Boolean { - val powerLevel = getUserPowerLevelValue(userId) - val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_CANONICAL_ALIAS] ?: powerLevelsContent.stateDefault - return powerLevel >= minPowerLevel - } - - /** - * Check if user have the necessary power level to change room history readability - * @param userId the id of the user to check for. - * @return true if able to change room history readability - */ - fun isUserAbleToChangeRoomHistoryReadability(userId: String): Boolean { - val powerLevel = getUserPowerLevelValue(userId) - val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_HISTORY_VISIBILITY] ?: powerLevelsContent.stateDefault - return powerLevel >= minPowerLevel - } - - /** - * Check if user have the necessary power level to change room avatar - * @param userId the id of the user to check for. - * @return true if able to change room avatar - */ - fun isUserAbleToChangeRoomAvatar(userId: String): Boolean { - val powerLevel = getUserPowerLevelValue(userId) - val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_AVATAR] ?: powerLevelsContent.stateDefault - return powerLevel >= minPowerLevel - } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt index 373dd6b56c..925ab716a4 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt @@ -25,6 +25,7 @@ import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper import im.vector.matrix.rx.rx import im.vector.matrix.rx.unwrap @@ -71,7 +72,9 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted private val ini powerLevelsContentLive .subscribe { val powerLevelsHelper = PowerLevelsHelper(it) - setState { copy(canChangeAvatar = powerLevelsHelper.isUserAbleToChangeRoomAvatar(session.myUserId)) } + setState { + copy(canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, isState = true, eventType = EventType.STATE_ROOM_AVATAR)) + } } .disposeOnClear() } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt index 94177159f0..e9d2e5ccb5 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsController.kt @@ -20,6 +20,7 @@ import com.airbnb.epoxy.TypedEpoxyController import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent +import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.riotx.R import im.vector.riotx.core.epoxy.profiles.buildProfileAction import im.vector.riotx.core.epoxy.profiles.buildProfileSection @@ -104,6 +105,13 @@ class RoomSettingsController @Inject constructor( action = { if (data.actionPermissions.canChangeHistoryReadability) callback?.onHistoryVisibilityClicked() } ) + buildEncryptionAction(data.actionPermissions, roomSummary) + } + + private fun buildEncryptionAction(actionPermissions: RoomSettingsViewState.ActionPermissions, roomSummary: RoomSummary) { + if (!actionPermissions.canEnableEncryption) { + return + } if (roomSummary.isEncrypted) { buildProfileAction( id = "encryption", diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt index e198375cfb..4aa4a437ac 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -101,10 +101,11 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: .subscribe { val powerLevelsHelper = PowerLevelsHelper(it) val permissions = RoomSettingsViewState.ActionPermissions( - canChangeName = powerLevelsHelper.isUserAbleToChangeRoomName(session.myUserId), - canChangeTopic = powerLevelsHelper.isUserAbleToChangeRoomTopic(session.myUserId), - canChangeCanonicalAlias = powerLevelsHelper.isUserAbleToChangeRoomCanonicalAlias(session.myUserId), - canChangeHistoryReadability = powerLevelsHelper.isUserAbleToChangeRoomHistoryReadability(session.myUserId) + canChangeName = powerLevelsHelper.isUserAllowedToSend(session.myUserId, isState = true, eventType = EventType.STATE_ROOM_NAME), + canChangeTopic = powerLevelsHelper.isUserAllowedToSend(session.myUserId, isState = true, eventType = EventType.STATE_ROOM_TOPIC), + canChangeCanonicalAlias = powerLevelsHelper.isUserAllowedToSend(session.myUserId, isState = true, eventType = EventType.STATE_ROOM_CANONICAL_ALIAS), + canChangeHistoryReadability = powerLevelsHelper.isUserAllowedToSend(session.myUserId, isState = true, eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY), + canEnableEncryption = powerLevelsHelper.isUserAllowedToSend(session.myUserId, isState = true, eventType = EventType.STATE_ROOM_ENCRYPTION) ) setState { copy(actionPermissions = permissions) } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt index a86fbf8cfa..c8d81f3ead 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewState.kt @@ -43,6 +43,7 @@ data class RoomSettingsViewState( val canChangeName: Boolean = false, val canChangeTopic: Boolean = false, val canChangeCanonicalAlias: Boolean = false, - val canChangeHistoryReadability: Boolean = false + val canChangeHistoryReadability: Boolean = false, + val canEnableEncryption: Boolean = false ) } From da7c971927fb3b619d0c30945ae9fe951589015d Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 9 Jul 2020 17:46:59 +0200 Subject: [PATCH 043/122] Fragments: use commitTransaction instead of commitNow --- .../im/vector/riotx/core/extensions/Activity.kt | 8 ++++---- .../im/vector/riotx/core/extensions/Fragment.kt | 16 ++++++++-------- .../riotx/features/home/HomeDetailFragment.kt | 3 ++- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt index b74f143e17..cc6eb54154 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt @@ -23,21 +23,21 @@ import androidx.fragment.app.FragmentTransaction import im.vector.riotx.core.platform.VectorBaseActivity fun VectorBaseActivity.addFragment(frameId: Int, fragment: Fragment) { - supportFragmentManager.commitTransactionNow { add(frameId, fragment) } + supportFragmentManager.commitTransaction { add(frameId, fragment) } } fun VectorBaseActivity.addFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) { - supportFragmentManager.commitTransactionNow { + supportFragmentManager.commitTransaction { add(frameId, fragmentClass, params.toMvRxBundle(), tag) } } fun VectorBaseActivity.replaceFragment(frameId: Int, fragment: Fragment, tag: String? = null) { - supportFragmentManager.commitTransactionNow { replace(frameId, fragment, tag) } + supportFragmentManager.commitTransaction { replace(frameId, fragment, tag) } } fun VectorBaseActivity.replaceFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) { - supportFragmentManager.commitTransactionNow { + supportFragmentManager.commitTransaction { replace(frameId, fragmentClass, params.toMvRxBundle(), tag) } } diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt index c28dcf12d3..88c96578ae 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt @@ -21,21 +21,21 @@ import androidx.fragment.app.Fragment import im.vector.riotx.core.platform.VectorBaseFragment fun VectorBaseFragment.addFragment(frameId: Int, fragment: Fragment) { - parentFragmentManager.commitTransactionNow { add(frameId, fragment) } + parentFragmentManager.commitTransaction { add(frameId, fragment) } } fun VectorBaseFragment.addFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) { - parentFragmentManager.commitTransactionNow { + parentFragmentManager.commitTransaction { add(frameId, fragmentClass, params.toMvRxBundle(), tag) } } fun VectorBaseFragment.replaceFragment(frameId: Int, fragment: Fragment) { - parentFragmentManager.commitTransactionNow { replace(frameId, fragment) } + parentFragmentManager.commitTransaction { replace(frameId, fragment) } } fun VectorBaseFragment.replaceFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) { - parentFragmentManager.commitTransactionNow { + parentFragmentManager.commitTransaction { replace(frameId, fragmentClass, params.toMvRxBundle(), tag) } } @@ -51,21 +51,21 @@ fun VectorBaseFragment.addFragmentToBackstack(frameId: Int, fragm } fun VectorBaseFragment.addChildFragment(frameId: Int, fragment: Fragment, tag: String? = null) { - childFragmentManager.commitTransactionNow { add(frameId, fragment, tag) } + childFragmentManager.commitTransaction { add(frameId, fragment, tag) } } fun VectorBaseFragment.addChildFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) { - childFragmentManager.commitTransactionNow { + childFragmentManager.commitTransaction { add(frameId, fragmentClass, params.toMvRxBundle(), tag) } } fun VectorBaseFragment.replaceChildFragment(frameId: Int, fragment: Fragment, tag: String? = null) { - childFragmentManager.commitTransactionNow { replace(frameId, fragment, tag) } + childFragmentManager.commitTransaction { replace(frameId, fragment, tag) } } fun VectorBaseFragment.replaceChildFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) { - childFragmentManager.commitTransactionNow { + childFragmentManager.commitTransaction { replace(frameId, fragmentClass, params.toMvRxBundle(), tag) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index c92c28079f..435ff7a9ab 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.riotx.R +import im.vector.riotx.core.extensions.commitTransaction import im.vector.riotx.core.extensions.commitTransactionNow import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.platform.ToolbarConfigurable @@ -275,7 +276,7 @@ class HomeDetailFragment @Inject constructor( private fun updateSelectedFragment(displayMode: RoomListDisplayMode) { val fragmentTag = "FRAGMENT_TAG_${displayMode.name}" val fragmentToShow = childFragmentManager.findFragmentByTag(fragmentTag) - childFragmentManager.commitTransactionNow { + childFragmentManager.commitTransaction { childFragmentManager.fragments .filter { it != fragmentToShow } .forEach { From 9c595b6c027f92dd3c7cd018cad9b6cd30b1b637 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 10 Jul 2020 08:54:41 +0200 Subject: [PATCH 044/122] Fix "Leave room only leaves the current version" --- CHANGES.md | 1 + .../room/membership/leaving/LeaveRoomTask.kt | 21 ++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e0a0085af5..0be18b8927 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,7 @@ Bugfix 🐛: - Regression Composer does not grow, crops out text (#1650) - Bug / Unwanted draft (#698) - All users seems to be able to see the enable encryption option in room settings (#1341) + - Leave room only leaves the current version (#1656) Translations 🗣: - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt index 08eb71fc89..2d56e5bd47 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt @@ -16,8 +16,13 @@ package im.vector.matrix.android.internal.session.room.membership.leaving +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.session.room.state.StateEventDataSource import im.vector.matrix.android.internal.task.Task import org.greenrobot.eventbus.EventBus import javax.inject.Inject @@ -31,12 +36,22 @@ internal interface LeaveRoomTask : Task { internal class DefaultLeaveRoomTask @Inject constructor( private val roomAPI: RoomAPI, - private val eventBus: EventBus + private val eventBus: EventBus, + private val stateEventDataSource: StateEventDataSource ) : LeaveRoomTask { override suspend fun execute(params: LeaveRoomTask.Params) { - return executeRequest(eventBus) { - apiCall = roomAPI.leave(params.roomId, mapOf("reason" to params.reason)) + leaveRoom(params.roomId, params.reason) + } + + private suspend fun leaveRoom(roomId: String, reason: String?) { + val roomCreateStateEvent = stateEventDataSource.getStateEvent(roomId, eventType = EventType.STATE_ROOM_CREATE, stateKey = QueryStringValue.NoCondition) + val predecessorRoomId = roomCreateStateEvent?.getClearContent()?.toModel()?.predecessor?.roomId + executeRequest(eventBus) { + apiCall = roomAPI.leave(roomId, mapOf("reason" to reason)) + } + if (predecessorRoomId != null) { + leaveRoom(predecessorRoomId, reason) } } } From 8814364497360417fef16e718e7a113b78d47693 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 10 Jul 2020 11:32:28 +0200 Subject: [PATCH 045/122] Invite: we shouldn't be able to open room details --- .../home/room/detail/RoomDetailFragment.kt | 3 +++ .../home/room/detail/RoomDetailViewModel.kt | 27 +++++++++++-------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index e0a1b10f9e..7483d978a3 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -293,6 +293,7 @@ class RoomDetailFragment @Inject constructor( setupJumpToBottomView() setupWidgetsBannerView() + roomToolbarContentView.isClickable = false roomToolbarContentView.debouncedClicks { navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId) } @@ -858,6 +859,7 @@ class RoomDetailFragment @Inject constructor( val summary = state.asyncRoomSummary() val inviter = state.asyncInviter() if (summary?.membership == Membership.JOIN) { + roomToolbarContentView.isClickable = true roomWidgetsBannerView.render(state.activeRoomWidgets()) scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline timelineEventController.update(state) @@ -879,6 +881,7 @@ class RoomDetailFragment @Inject constructor( notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent)) } } else if (summary?.membership == Membership.INVITE && inviter != null) { + roomToolbarContentView.isClickable = false inviteView.visibility = View.VISIBLE inviteView.render(inviter, VectorInviteView.Mode.LARGE) // Intercept click event diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 6276089145..bed8b4867d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -405,17 +405,22 @@ class RoomDetailViewModel @AssistedInject constructor( private fun isIntegrationEnabled() = session.integrationManagerService().isIntegrationEnabled() - fun isMenuItemVisible(@IdRes itemId: Int) = when (itemId) { - R.id.clear_message_queue -> - // For now always disable when not in developer mode, worker cancellation is not working properly - timeline.pendingEventCount() > 0 && vectorPreferences.developerMode() - R.id.resend_all -> timeline.failedToDeliverEventCount() > 0 - R.id.clear_all -> timeline.failedToDeliverEventCount() > 0 - R.id.open_matrix_apps -> true - R.id.voice_call, - R.id.video_call -> room.canStartCall() && webRtcPeerConnectionManager.currentCall == null - R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null - else -> false + fun isMenuItemVisible(@IdRes itemId: Int): Boolean = com.airbnb.mvrx.withState(this) { state -> + if(state.asyncRoomSummary()?.membership != Membership.JOIN){ + return@withState false + } + when (itemId) { + R.id.clear_message_queue -> + // For now always disable when not in developer mode, worker cancellation is not working properly + timeline.pendingEventCount() > 0 && vectorPreferences.developerMode() + R.id.resend_all -> timeline.failedToDeliverEventCount() > 0 + R.id.clear_all -> timeline.failedToDeliverEventCount() > 0 + R.id.open_matrix_apps -> true + R.id.voice_call, + R.id.video_call -> room.canStartCall() && webRtcPeerConnectionManager.currentCall == null + R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null + else -> false + } } // PRIVATE METHODS ***************************************************************************** From 28869f4382d4ef369cf3cb13e8a730443a4b63e4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 10 Jul 2020 12:19:25 +0200 Subject: [PATCH 046/122] Small cleanup before merge --- .../crypto/verification/DefaultVerificationService.kt | 2 +- .../vector/riotx/features/navigation/DefaultNavigator.kt | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt index dcee12c165..4d4eeb21fd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt @@ -1234,7 +1234,7 @@ internal class DefaultVerificationService @Inject constructor( ) // We can SCAN or SHOW QR codes only if cross-signing is enabled - val methodValues = if (crossSigningService.getMyCrossSigningKeys() != null) { + val methodValues = if (crossSigningService.isCrossSigningInitialized()) { // Add reciprocate method if application declares it can scan or show QR codes // Not sure if it ok to do that (?) val reciprocateMethod = methods diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 6907544252..83f0baa12c 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -118,9 +118,10 @@ class DefaultNavigator @Inject constructor( override fun requestSelfSessionVerification(context: Context) { val session = sessionHolder.getSafeActiveSession() ?: return - val otherSessions = session.cryptoService().getCryptoDeviceInfo(session.myUserId).filter { - it.deviceId != session.sessionParams.deviceId - }.map { it.deviceId } + val otherSessions = session.cryptoService() + .getCryptoDeviceInfo(session.myUserId) + .filter { it.deviceId != session.sessionParams.deviceId } + .map { it.deviceId } if (context is VectorBaseActivity) { if (otherSessions.isNotEmpty()) { val pr = session.cryptoService().verificationService().requestKeyVerification( From eff08955f147faf809dea6df30cfa88c970047f9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 10 Jul 2020 12:37:48 +0200 Subject: [PATCH 047/122] Fix a11y --- vector/src/main/res/layout/merge_image_attachment_overlay.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/res/layout/merge_image_attachment_overlay.xml b/vector/src/main/res/layout/merge_image_attachment_overlay.xml index db22c0112c..6188ad564a 100644 --- a/vector/src/main/res/layout/merge_image_attachment_overlay.xml +++ b/vector/src/main/res/layout/merge_image_attachment_overlay.xml @@ -24,7 +24,7 @@ android:layout_marginEnd="16dp" android:background="?attr/selectableItemBackgroundBorderless" android:clickable="true" - android:contentDescription="@string/share" + android:contentDescription="@string/action_close" android:focusable="true" android:padding="6dp" android:scaleType="centerInside" From e979bee9206b6100b0f3d112d3deff94a4210bb0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 10 Jul 2020 12:37:59 +0200 Subject: [PATCH 048/122] Format --- .../riotx/features/navigation/DefaultNavigator.kt | 3 ++- .../im/vector/riotx/features/navigation/Navigator.kt | 3 ++- .../res/layout/merge_image_attachment_overlay.xml | 12 ++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 8940ac6791..6acd041ecf 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -289,7 +289,8 @@ class DefaultNavigator @Inject constructor( } override fun openVideoViewer(activity: Activity, - roomId: String, mediaData: VideoContentRenderer.Data, + roomId: String, + mediaData: AttachmentData, view: View, inMemory: List?, options: ((MutableList>) -> Unit)?) { diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index f925344570..6d036f1468 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -99,7 +99,8 @@ interface Navigator { options: ((MutableList>) -> Unit)?) fun openVideoViewer(activity: Activity, - roomId: String, mediaData: VideoContentRenderer.Data, + roomId: String, + mediaData: VideoContentRenderer.Data, view: View, inMemory: List? = null, options: ((MutableList>) -> Unit)?) diff --git a/vector/src/main/res/layout/merge_image_attachment_overlay.xml b/vector/src/main/res/layout/merge_image_attachment_overlay.xml index 6188ad564a..b0e769579c 100644 --- a/vector/src/main/res/layout/merge_image_attachment_overlay.xml +++ b/vector/src/main/res/layout/merge_image_attachment_overlay.xml @@ -74,8 +74,8 @@ android:layout_marginEnd="16dp" android:background="?attr/selectableItemBackgroundBorderless" android:clickable="true" - android:focusable="true" android:contentDescription="@string/share" + android:focusable="true" android:padding="6dp" android:tint="@color/white" app:layout_constraintBottom_toBottomOf="@id/overlayTopBackground" @@ -85,12 +85,12 @@ + app:constraint_referenced_ids="overlayBottomBackground,overlayBackButton,overlayPlayPauseButton,overlaySeekBar" + tools:visibility="visible" /> Date: Fri, 10 Jul 2020 12:48:35 +0200 Subject: [PATCH 049/122] Cleanup Navigator --- .../home/room/detail/RoomDetailFragment.kt | 16 ++++-- .../media/VectorAttachmentViewerActivity.kt | 5 +- .../features/navigation/DefaultNavigator.kt | 52 +------------------ .../riotx/features/navigation/Navigator.kt | 11 +--- .../uploads/media/RoomUploadsMediaFragment.kt | 19 +++++-- 5 files changed, 34 insertions(+), 69 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index a457587aa8..938ae6a1bb 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -1171,14 +1171,24 @@ class RoomDetailFragment @Inject constructor( } override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) { - navigator.openImageViewer(requireActivity(), roomDetailArgs.roomId, mediaData, view) { pairs -> + navigator.openMediaViewer( + activity = requireActivity(), + roomId = roomDetailArgs.roomId, + mediaData = mediaData, + view = view + ) { pairs -> pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: "")) pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: "")) } } override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) { - navigator.openVideoViewer(requireActivity(), roomDetailArgs.roomId, mediaData, view) { pairs -> + navigator.openMediaViewer( + activity = requireActivity(), + roomId = roomDetailArgs.roomId, + mediaData = mediaData, + view = view + ) { pairs -> pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: "")) pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: "")) } @@ -1199,7 +1209,7 @@ class RoomDetailFragment @Inject constructor( override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { if (allGranted(grantResults)) { when (requestCode) { - SAVE_ATTACHEMENT_REQUEST_CODE -> { + SAVE_ATTACHEMENT_REQUEST_CODE -> { sharedActionViewModel.pendingAction?.let { handleActions(it) sharedActionViewModel.pendingAction = null diff --git a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt index c0b822c13a..38e3ccc69c 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt @@ -237,7 +237,6 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen } companion object { - const val EXTRA_ARGS = "EXTRA_ARGS" const val EXTRA_IMAGE_DATA = "EXTRA_IMAGE_DATA" const val EXTRA_IN_MEMORY_DATA = "EXTRA_IN_MEMORY_DATA" @@ -246,11 +245,11 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen mediaData: AttachmentData, roomId: String?, eventId: String, - inMemoryData: List?, + inMemoryData: List, sharedTransitionName: String?) = Intent(context, VectorAttachmentViewerActivity::class.java).also { it.putExtra(EXTRA_ARGS, Args(roomId, eventId, sharedTransitionName)) it.putExtra(EXTRA_IMAGE_DATA, mediaData) - if (inMemoryData != null) { + if (inMemoryData.isNotEmpty()) { it.putParcelableArrayListExtra(EXTRA_IN_MEMORY_DATA, ArrayList(inMemoryData)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 6acd041ecf..99eb69f539 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -19,7 +19,6 @@ package im.vector.riotx.features.navigation import android.app.Activity import android.content.Context import android.content.Intent -import android.os.Build import android.view.View import android.view.Window import androidx.core.app.ActivityOptionsCompat @@ -52,7 +51,6 @@ import im.vector.riotx.features.invite.InviteUsersToRoomActivity import im.vector.riotx.features.media.AttachmentData import im.vector.riotx.features.media.BigImageViewerActivity import im.vector.riotx.features.media.VectorAttachmentViewerActivity -import im.vector.riotx.features.media.VideoContentRenderer import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity @@ -245,11 +243,11 @@ class DefaultNavigator @Inject constructor( context.startActivity(WidgetActivity.newIntent(context, widgetArgs)) } - override fun openImageViewer(activity: Activity, + override fun openMediaViewer(activity: Activity, roomId: String, mediaData: AttachmentData, view: View, - inMemory: List?, + inMemory: List, options: ((MutableList>) -> Unit)?) { VectorAttachmentViewerActivity.newIntent(activity, mediaData, @@ -268,52 +266,6 @@ class DefaultNavigator @Inject constructor( pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: "")) options?.invoke(pairs) - val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle() - activity.startActivity(intent, bundle) - } -// val intent = ImageMediaViewerActivity.newIntent(activity, mediaData, ViewCompat.getTransitionName(view)) -// val pairs = ArrayList>() -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { -// activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let { -// pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) -// } -// activity.window.decorView.findViewById(android.R.id.navigationBarBackground)?.let { -// pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) -// } -// } -// pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: "")) -// options?.invoke(pairs) -// -// val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle() -// activity.startActivity(intent, bundle) - } - - override fun openVideoViewer(activity: Activity, - roomId: String, - mediaData: AttachmentData, - view: View, - inMemory: List?, - options: ((MutableList>) -> Unit)?) { -// val intent = VideoMediaViewerActivity.newIntent(activity, mediaData) -// activity.startActivity(intent) - VectorAttachmentViewerActivity.newIntent(activity, - mediaData, - roomId, - mediaData.eventId, - inMemory, - ViewCompat.getTransitionName(view)).let { intent -> - val pairs = ArrayList>() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let { - pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) - } - activity.window.decorView.findViewById(android.R.id.navigationBarBackground)?.let { - pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) - } - } - pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: "")) - options?.invoke(pairs) - val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle() activity.startActivity(intent, bundle) } diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index 6d036f1468..273734916d 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -91,17 +91,10 @@ interface Navigator { fun openRoomWidget(context: Context, roomId: String, widget: Widget) - fun openImageViewer(activity: Activity, + fun openMediaViewer(activity: Activity, roomId: String, mediaData: AttachmentData, view: View, - inMemory: List? = null, - options: ((MutableList>) -> Unit)?) - - fun openVideoViewer(activity: Activity, - roomId: String, - mediaData: VideoContentRenderer.Data, - view: View, - inMemory: List? = null, + inMemory: List = emptyList(), options: ((MutableList>) -> Unit)?) } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt index e0758c7d72..dda070bf48 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt @@ -89,7 +89,7 @@ class RoomUploadsMediaFragment @Inject constructor( // It's very strange i can't just access // the app bar using find by id... - private fun trickFindAppBar() : AppBarLayout? { + private fun trickFindAppBar(): AppBarLayout? { return activity?.supportFragmentManager?.fragments ?.filterIsInstance() ?.firstOrNull() @@ -97,9 +97,14 @@ class RoomUploadsMediaFragment @Inject constructor( } override fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) = withState(uploadsViewModel) { state -> - val inMemory = getItemsArgs(state) - navigator.openImageViewer(requireActivity(), state.roomId, mediaData, view, inMemory) { pairs -> + navigator.openMediaViewer( + activity = requireActivity(), + roomId = state.roomId, + mediaData = mediaData, + view = view, + inMemory = inMemory + ) { pairs -> trickFindAppBar()?.let { pairs.add(Pair(it, ViewCompat.getTransitionName(it) ?: "")) } @@ -151,7 +156,13 @@ class RoomUploadsMediaFragment @Inject constructor( override fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) = withState(uploadsViewModel) { state -> val inMemory = getItemsArgs(state) - navigator.openVideoViewer(requireActivity(), state.roomId, mediaData, view, inMemory) { pairs -> + navigator.openMediaViewer( + activity = requireActivity(), + roomId = state.roomId, + mediaData = mediaData, + view = view, + inMemory = inMemory + ) { pairs -> trickFindAppBar()?.let { pairs.add(Pair(it, ViewCompat.getTransitionName(it) ?: "")) } From ea3e467dc44b042f56a7482db3468d1019b68379 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 10 Jul 2020 12:52:54 +0200 Subject: [PATCH 050/122] Format --- settings.gradle | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/settings.gradle b/settings.gradle index 3a7aa9ac1c..76a15a206d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,6 @@ -include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch', ':attachment-viewer' -include ':multipicker' \ No newline at end of file +include ':vector' +include ':matrix-sdk-android' +include ':matrix-sdk-android-rx' +include ':diff-match-patch' +include ':attachment-viewer' +include ':multipicker' From 6c0f775c4b1058d589eabfd1dc0ea2444999ba7e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 10 Jul 2020 13:07:14 +0200 Subject: [PATCH 051/122] Cleanup --- attachment-viewer/src/main/AndroidManifest.xml | 11 +---------- attachment-viewer/src/main/res/values/dimens.xml | 3 --- attachment-viewer/src/main/res/values/strings.xml | 11 ----------- attachment-viewer/src/main/res/values/styles.xml | 12 ------------ 4 files changed, 1 insertion(+), 36 deletions(-) delete mode 100644 attachment-viewer/src/main/res/values/dimens.xml delete mode 100644 attachment-viewer/src/main/res/values/strings.xml delete mode 100644 attachment-viewer/src/main/res/values/styles.xml diff --git a/attachment-viewer/src/main/AndroidManifest.xml b/attachment-viewer/src/main/AndroidManifest.xml index 4c48526635..ff8ec394d2 100644 --- a/attachment-viewer/src/main/AndroidManifest.xml +++ b/attachment-viewer/src/main/AndroidManifest.xml @@ -1,11 +1,2 @@ - - - - - - - \ No newline at end of file + diff --git a/attachment-viewer/src/main/res/values/dimens.xml b/attachment-viewer/src/main/res/values/dimens.xml deleted file mode 100644 index 125df87119..0000000000 --- a/attachment-viewer/src/main/res/values/dimens.xml +++ /dev/null @@ -1,3 +0,0 @@ - - 16dp - \ No newline at end of file diff --git a/attachment-viewer/src/main/res/values/strings.xml b/attachment-viewer/src/main/res/values/strings.xml deleted file mode 100644 index 6dcb56555a..0000000000 --- a/attachment-viewer/src/main/res/values/strings.xml +++ /dev/null @@ -1,11 +0,0 @@ - - AttachementViewerActivity - - First Fragment - Second Fragment - Next - Previous - - Hello first fragment - Hello second fragment. Arg: %1$s - \ No newline at end of file diff --git a/attachment-viewer/src/main/res/values/styles.xml b/attachment-viewer/src/main/res/values/styles.xml deleted file mode 100644 index a81174782e..0000000000 --- a/attachment-viewer/src/main/res/values/styles.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - \ No newline at end of file From e8b1e418fa116ebe0a9464d1d6f55b3ec70b6e93 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 10 Jul 2020 14:37:57 +0200 Subject: [PATCH 052/122] ktlint --- .../main/java/im/vector/riotx/features/navigation/Navigator.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index 273734916d..2403cfa0a9 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -23,11 +23,10 @@ import androidx.core.util.Pair import androidx.fragment.app.Fragment import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.terms.TermsService -import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.session.widgets.model.Widget +import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes import im.vector.riotx.features.media.AttachmentData -import im.vector.riotx.features.media.VideoContentRenderer import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.share.SharedData import im.vector.riotx.features.terms.ReviewTermsActivity From 9f2631110eb84c824ba6ae91ad670ac3f6c82764 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 10 Jul 2020 14:38:23 +0200 Subject: [PATCH 053/122] Missing copyrights --- .../vector/riotx/attachmentviewer/AttachmentViewerActivity.kt | 1 + .../java/im/vector/riotx/attachmentviewer/SwipeDirection.kt | 1 + .../im/vector/riotx/attachmentviewer/SwipeDirectionDetector.kt | 1 + .../im/vector/riotx/attachmentviewer/SwipeToDismissHandler.kt | 1 + vector/src/main/assets/open_source_licenses.html | 3 +++ 5 files changed, 7 insertions(+) diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt index d6cf7c606a..8c2d4e9833 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2020 New Vector Ltd + * Copyright (C) 2018 stfalcon.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeDirection.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeDirection.kt index e552d55efb..ebe8784e15 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeDirection.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeDirection.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2020 New Vector Ltd + * Copyright (C) 2018 stfalcon.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeDirectionDetector.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeDirectionDetector.kt index cedbcd0180..0cf9a19ab1 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeDirectionDetector.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeDirectionDetector.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2020 New Vector Ltd + * Copyright (C) 2018 stfalcon.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeToDismissHandler.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeToDismissHandler.kt index e52c72cba0..ca93d4f73a 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeToDismissHandler.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeToDismissHandler.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2020 New Vector Ltd + * Copyright (C) 2018 stfalcon.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index 3af564aaca..58d63bf5a7 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -389,6 +389,9 @@ SOFTWARE.
  • BillCarsonFr/JsonViewer
  • +
  • + Copyright (C) 2018 stfalcon.com +
  •  Apache License
    
    From 1b6b71ed986a793314ecf9ccb063a69042ffaede Mon Sep 17 00:00:00 2001
    From: Valere 
    Date: Fri, 10 Jul 2020 14:38:31 +0200
    Subject: [PATCH 054/122] Debounce clicks
    
    ---
     .../features/roomprofile/uploads/media/UploadsImageItem.kt | 7 ++++++-
     .../features/roomprofile/uploads/media/UploadsVideoItem.kt | 7 ++++++-
     2 files changed, 12 insertions(+), 2 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt
    index f994ad0110..3b83e99656 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt
    @@ -24,6 +24,7 @@ import com.airbnb.epoxy.EpoxyModelClass
     import im.vector.riotx.R
     import im.vector.riotx.core.epoxy.VectorEpoxyHolder
     import im.vector.riotx.core.epoxy.VectorEpoxyModel
    +import im.vector.riotx.core.utils.DebouncedClickListener
     import im.vector.riotx.features.media.ImageContentRenderer
     
     @EpoxyModelClass(layout = R.layout.item_uploads_image)
    @@ -36,7 +37,11 @@ abstract class UploadsImageItem : VectorEpoxyModel() {
     
         override fun bind(holder: Holder) {
             super.bind(holder)
    -        holder.view.setOnClickListener { listener?.onItemClicked(holder.imageView, data) }
    +        holder.view.setOnClickListener(
    +                DebouncedClickListener(View.OnClickListener { _ ->
    +                    listener?.onItemClicked(holder.imageView, data)
    +                })
    +        )
             imageContentRenderer.render(data, holder.imageView, IMAGE_SIZE_DP)
             ViewCompat.setTransitionName(holder.imageView, "imagePreview_${id()}")
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt
    index 1c9ab4ae74..f20f6ed5b1 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt
    @@ -24,6 +24,7 @@ import com.airbnb.epoxy.EpoxyModelClass
     import im.vector.riotx.R
     import im.vector.riotx.core.epoxy.VectorEpoxyHolder
     import im.vector.riotx.core.epoxy.VectorEpoxyModel
    +import im.vector.riotx.core.utils.DebouncedClickListener
     import im.vector.riotx.features.media.ImageContentRenderer
     import im.vector.riotx.features.media.VideoContentRenderer
     
    @@ -37,7 +38,11 @@ abstract class UploadsVideoItem : VectorEpoxyModel() {
     
         override fun bind(holder: Holder) {
             super.bind(holder)
    -        holder.view.setOnClickListener { listener?.onItemClicked(holder.imageView, data) }
    +        holder.view.setOnClickListener(
    +            DebouncedClickListener(View.OnClickListener { _ ->
    +                listener?.onItemClicked(holder.imageView, data)
    +            })
    +        )
             imageContentRenderer.render(data.thumbnailMediaData, holder.imageView, IMAGE_SIZE_DP)
             ViewCompat.setTransitionName(holder.imageView, "videoPreview_${id()}")
         }
    
    From 08bc487f170ca1de284d1d601e262d28660df12c Mon Sep 17 00:00:00 2001
    From: Valere 
    Date: Fri, 10 Jul 2020 14:39:08 +0200
    Subject: [PATCH 055/122] klint
    
    ---
     .../main/java/im/vector/riotx/features/navigation/Navigator.kt   | 1 -
     1 file changed, 1 deletion(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
    index 273734916d..c86aa0aca9 100644
    --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
    @@ -27,7 +27,6 @@ import im.vector.matrix.android.api.util.MatrixItem
     import im.vector.matrix.android.api.session.widgets.model.Widget
     import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes
     import im.vector.riotx.features.media.AttachmentData
    -import im.vector.riotx.features.media.VideoContentRenderer
     import im.vector.riotx.features.settings.VectorSettingsActivity
     import im.vector.riotx.features.share.SharedData
     import im.vector.riotx.features.terms.ReviewTermsActivity
    
    From 10f8aebde25b190ca2b74e50939c76cb0109fb73 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 3 Jul 2020 11:14:41 +0200
    Subject: [PATCH 056/122] Update comment
    
    ---
     .../java/im/vector/matrix/android/api/MatrixConfiguration.kt    | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixConfiguration.kt
    index e7c24fadc8..d80a940675 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixConfiguration.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixConfiguration.kt
    @@ -33,7 +33,7 @@ data class MatrixConfiguration(
             ),
             /**
              * Optional proxy to connect to the matrix servers
    -         * You can create one using for instance Proxy(proxyType, InetSocketAddress(hostname, port)
    +         * You can create one using for instance Proxy(proxyType, InetSocketAddress.createUnresolved(hostname, port)
              */
             val proxy: Proxy? = null
     ) {
    
    From 5f60d7fd3bf9ca683fe7875516763d72f0e18c44 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 3 Jul 2020 11:16:58 +0200
    Subject: [PATCH 057/122] Session.configureAndStart now handle registering to
     webRtcPeerConnectionManager...
    
    ---
     vector/src/main/java/im/vector/riotx/VectorApplication.kt | 8 ++++++--
     .../main/java/im/vector/riotx/core/extensions/Session.kt  | 3 +++
     .../java/im/vector/riotx/features/login/LoginViewModel.kt | 7 +++++--
     3 files changed, 14 insertions(+), 4 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    index ab7c3e1bf7..d49c7a4c7b 100644
    --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    @@ -137,8 +137,12 @@ class VectorApplication :
             if (authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
                 val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
                 activeSessionHolder.setActiveSession(lastAuthenticatedSession)
    -            lastAuthenticatedSession.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener)
    -            lastAuthenticatedSession.callSignalingService().addCallListener(webRtcPeerConnectionManager)
    +            lastAuthenticatedSession.configureAndStart(
    +                    applicationContext,
    +                    pushRuleTriggerListener,
    +                    webRtcPeerConnectionManager,
    +                    sessionListener
    +            )
             }
             ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
                 @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
    index 29b169ffd4..788ab01c4d 100644
    --- a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
    @@ -24,12 +24,14 @@ import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
     import im.vector.matrix.android.api.session.sync.FilterService
     import im.vector.riotx.core.services.VectorSyncService
    +import im.vector.riotx.features.call.WebRtcPeerConnectionManager
     import im.vector.riotx.features.notifications.PushRuleTriggerListener
     import im.vector.riotx.features.session.SessionListener
     import timber.log.Timber
     
     fun Session.configureAndStart(context: Context,
                                   pushRuleTriggerListener: PushRuleTriggerListener,
    +                              webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
                                   sessionListener: SessionListener) {
         open()
         addListener(sessionListener)
    @@ -38,6 +40,7 @@ fun Session.configureAndStart(context: Context,
         startSyncing(context)
         refreshPushers()
         pushRuleTriggerListener.startWithSession(this)
    +    callSignalingService().addCallListener(webRtcPeerConnectionManager)
     }
     
     fun Session.startSyncing(context: Context) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt
    index 7edc674b11..f85d91d9c5 100644
    --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt
    @@ -667,8 +667,11 @@ class LoginViewModel @AssistedInject constructor(
     
         private fun onSessionCreated(session: Session) {
             activeSessionHolder.setActiveSession(session)
    -        session.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener)
    -        session.callSignalingService().addCallListener(webRtcPeerConnectionManager)
    +        session.configureAndStart(
    +                applicationContext,
    +                pushRuleTriggerListener,
    +                webRtcPeerConnectionManager,
    +                sessionListener)
             setState {
                 copy(
                         asyncLoginAction = Success(Unit)
    
    From 6569ee5d109189513d3c19b1d5e8a57a1adbd332 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 3 Jul 2020 15:36:04 +0200
    Subject: [PATCH 058/122] Use Set instead of List
    
    ---
     .../vector/matrix/android/internal/session/SessionListeners.kt  | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionListeners.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionListeners.kt
    index ff3bc0b073..83b90b16b9 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionListeners.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionListeners.kt
    @@ -22,7 +22,7 @@ import javax.inject.Inject
     
     internal class SessionListeners @Inject constructor() {
     
    -    private val listeners = ArrayList()
    +    private val listeners = mutableSetOf()
     
         fun addListener(listener: Session.Listener) {
             synchronized(listeners) {
    
    From 811cbb2e201318f1b964ac136e2e1fce05e33b05 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 3 Jul 2020 15:42:31 +0200
    Subject: [PATCH 059/122] ActiveSessionHolder to more things related to other
     @Singleton, and especially some missing cleanup Such as calling
     `removeListener()` and `callSignalingService().removeCallListener()`
     `Session.configureAndStart()` do less thing now
    
    ---
     .../java/im/vector/riotx/VectorApplication.kt | 13 +----
     .../riotx/core/di/ActiveSessionHolder.kt      | 23 ++++++++-
     .../vector/riotx/core/extensions/Session.kt   | 13 +----
     .../im/vector/riotx/core/utils/DataSource.kt  |  3 ++
     .../call/WebRtcPeerConnectionManager.kt       | 51 ++++++++++---------
     .../timeline/format/NoticeEventFormatter.kt   | 12 +++--
     .../riotx/features/login/LoginViewModel.kt    | 16 ++----
     .../NotificationDrawerManager.kt              | 15 ++++--
     .../notifications/OutdatedEventDetector.kt    | 10 ++--
     .../notifications/PushRuleTriggerListener.kt  |  8 +--
     10 files changed, 87 insertions(+), 77 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    index d49c7a4c7b..d7fe2a054d 100644
    --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    @@ -44,15 +44,12 @@ import im.vector.riotx.core.di.HasVectorInjector
     import im.vector.riotx.core.di.VectorComponent
     import im.vector.riotx.core.extensions.configureAndStart
     import im.vector.riotx.core.rx.RxConfig
    -import im.vector.riotx.features.call.WebRtcPeerConnectionManager
     import im.vector.riotx.features.configuration.VectorConfiguration
     import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks
     import im.vector.riotx.features.notifications.NotificationDrawerManager
     import im.vector.riotx.features.notifications.NotificationUtils
    -import im.vector.riotx.features.notifications.PushRuleTriggerListener
     import im.vector.riotx.features.popup.PopupAlertManager
     import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
    -import im.vector.riotx.features.session.SessionListener
     import im.vector.riotx.features.settings.VectorPreferences
     import im.vector.riotx.features.version.VersionProvider
     import im.vector.riotx.push.fcm.FcmHelper
    @@ -79,16 +76,13 @@ class VectorApplication :
         @Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper
         @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
         @Inject lateinit var activeSessionHolder: ActiveSessionHolder
    -    @Inject lateinit var sessionListener: SessionListener
         @Inject lateinit var notificationDrawerManager: NotificationDrawerManager
    -    @Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener
         @Inject lateinit var vectorPreferences: VectorPreferences
         @Inject lateinit var versionProvider: VersionProvider
         @Inject lateinit var notificationUtils: NotificationUtils
         @Inject lateinit var appStateHandler: AppStateHandler
         @Inject lateinit var rxConfig: RxConfig
         @Inject lateinit var popupAlertManager: PopupAlertManager
    -    @Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
     
         lateinit var vectorComponent: VectorComponent
     
    @@ -137,12 +131,7 @@ class VectorApplication :
             if (authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
                 val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
                 activeSessionHolder.setActiveSession(lastAuthenticatedSession)
    -            lastAuthenticatedSession.configureAndStart(
    -                    applicationContext,
    -                    pushRuleTriggerListener,
    -                    webRtcPeerConnectionManager,
    -                    sessionListener
    -            )
    +            lastAuthenticatedSession.configureAndStart(applicationContext)
             }
             ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
                 @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
    index ff9865c3ea..9fbd51984f 100644
    --- a/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
    @@ -20,8 +20,12 @@ import arrow.core.Option
     import im.vector.matrix.android.api.auth.AuthenticationService
     import im.vector.matrix.android.api.session.Session
     import im.vector.riotx.ActiveSessionDataSource
    +import im.vector.riotx.features.call.WebRtcPeerConnectionManager
     import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
     import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler
    +import im.vector.riotx.features.notifications.PushRuleTriggerListener
    +import im.vector.riotx.features.session.SessionListener
    +import timber.log.Timber
     import java.util.concurrent.atomic.AtomicReference
     import javax.inject.Inject
     import javax.inject.Singleton
    @@ -30,23 +34,40 @@ import javax.inject.Singleton
     class ActiveSessionHolder @Inject constructor(private val authenticationService: AuthenticationService,
                                                   private val sessionObservableStore: ActiveSessionDataSource,
                                                   private val keyRequestHandler: KeyRequestHandler,
    -                                              private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler
    +                                              private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler,
    +                                              private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
    +                                              private val pushRuleTriggerListener: PushRuleTriggerListener,
    +                                              private val sessionListener: SessionListener
     ) {
     
         private var activeSession: AtomicReference = AtomicReference()
     
         fun setActiveSession(session: Session) {
    +        Timber.w("setActiveSession of ${session.myUserId}")
             activeSession.set(session)
             sessionObservableStore.post(Option.just(session))
    +
             keyRequestHandler.start(session)
             incomingVerificationRequestHandler.start(session)
    +        session.addListener(sessionListener)
    +        pushRuleTriggerListener.startWithSession(session)
    +        session.callSignalingService().addCallListener(webRtcPeerConnectionManager)
         }
     
         fun clearActiveSession() {
    +        // Do some cleanup first
    +        getSafeActiveSession()?.let {
    +            Timber.w("clearActiveSession of ${it.myUserId}")
    +            it.callSignalingService().removeCallListener(webRtcPeerConnectionManager)
    +            it.removeListener(sessionListener)
    +        }
    +
             activeSession.set(null)
             sessionObservableStore.post(Option.empty())
    +
             keyRequestHandler.stop()
             incomingVerificationRequestHandler.stop()
    +        pushRuleTriggerListener.stop()
         }
     
         fun hasActiveSession(): Boolean {
    diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
    index 788ab01c4d..d212380da4 100644
    --- a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
    @@ -24,23 +24,14 @@ import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
     import im.vector.matrix.android.api.session.sync.FilterService
     import im.vector.riotx.core.services.VectorSyncService
    -import im.vector.riotx.features.call.WebRtcPeerConnectionManager
    -import im.vector.riotx.features.notifications.PushRuleTriggerListener
    -import im.vector.riotx.features.session.SessionListener
     import timber.log.Timber
     
    -fun Session.configureAndStart(context: Context,
    -                              pushRuleTriggerListener: PushRuleTriggerListener,
    -                              webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
    -                              sessionListener: SessionListener) {
    +fun Session.configureAndStart(context: Context) {
    +    Timber.i("Configure and start session for $myUserId")
         open()
    -    addListener(sessionListener)
         setFilter(FilterService.FilterPreset.RiotFilter)
    -    Timber.i("Configure and start session for ${this.myUserId}")
         startSyncing(context)
         refreshPushers()
    -    pushRuleTriggerListener.startWithSession(this)
    -    callSignalingService().addCallListener(webRtcPeerConnectionManager)
     }
     
     fun Session.startSyncing(context: Context) {
    diff --git a/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt b/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt
    index 4c4a553e5c..6f6057cb43 100644
    --- a/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt
    @@ -36,6 +36,9 @@ open class BehaviorDataSource(private val defaultValue: T? = null) : MutableD
     
         private val behaviorRelay = createRelay()
     
    +    val currentValue: T?
    +        get() = behaviorRelay.value
    +
         override fun observe(): Observable {
             return behaviorRelay.hide().observeOn(AndroidSchedulers.mainThread())
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt b/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt
    index 05f14ae4f2..070375d201 100644
    --- a/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt
    @@ -22,6 +22,7 @@ import android.os.Build
     import androidx.annotation.RequiresApi
     import im.vector.matrix.android.api.MatrixCallback
     import im.vector.matrix.android.api.extensions.tryThis
    +import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.call.CallState
     import im.vector.matrix.android.api.session.call.CallsListener
     import im.vector.matrix.android.api.session.call.EglUtils
    @@ -31,7 +32,7 @@ import im.vector.matrix.android.api.session.room.model.call.CallAnswerContent
     import im.vector.matrix.android.api.session.room.model.call.CallCandidatesContent
     import im.vector.matrix.android.api.session.room.model.call.CallHangupContent
     import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
    -import im.vector.riotx.core.di.ActiveSessionHolder
    +import im.vector.riotx.ActiveSessionDataSource
     import im.vector.riotx.core.services.BluetoothHeadsetReceiver
     import im.vector.riotx.core.services.CallService
     import im.vector.riotx.core.services.WiredHeadsetStateReceiver
    @@ -71,9 +72,12 @@ import javax.inject.Singleton
     @Singleton
     class WebRtcPeerConnectionManager @Inject constructor(
             private val context: Context,
    -        private val sessionHolder: ActiveSessionHolder
    +        private val activeSessionDataSource: ActiveSessionDataSource
     ) : CallsListener {
     
    +    private val currentSession: Session?
    +        get() = activeSessionDataSource.currentValue?.orNull()
    +
         interface CurrentCallListener {
             fun onCurrentCallChange(call: MxCall?)
             fun onCaptureStateChanged(mgr: WebRtcPeerConnectionManager) {}
    @@ -288,15 +292,16 @@ class WebRtcPeerConnectionManager @Inject constructor(
         }
     
         private fun getTurnServer(callback: ((TurnServerResponse?) -> Unit)) {
    -        sessionHolder.getActiveSession().callSignalingService().getTurnServer(object : MatrixCallback {
    -            override fun onSuccess(data: TurnServerResponse?) {
    -                callback(data)
    -            }
    +        currentSession?.callSignalingService()
    +                ?.getTurnServer(object : MatrixCallback {
    +                    override fun onSuccess(data: TurnServerResponse?) {
    +                        callback(data)
    +                    }
     
    -            override fun onFailure(failure: Throwable) {
    -                callback(null)
    -            }
    -        })
    +                    override fun onFailure(failure: Throwable) {
    +                        callback(null)
    +                    }
    +                })
         }
     
         fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) {
    @@ -310,7 +315,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
             currentCall?.mxCall
                     ?.takeIf { it.state is CallState.Connected }
                     ?.let { mxCall ->
    -                    val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName()
    +                    val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
                                 ?: mxCall.roomId
                         // Start background service with notification
                         CallService.onPendingCall(
    @@ -318,7 +323,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
                                 isVideo = mxCall.isVideoCall,
                                 roomName = name,
                                 roomId = mxCall.roomId,
    -                            matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
    +                            matrixId = currentSession?.myUserId ?: "",
                                 callId = mxCall.callId)
                     }
     
    @@ -373,14 +378,14 @@ class WebRtcPeerConnectionManager @Inject constructor(
             val mxCall = callContext.mxCall
             // Update service state
     
    -        val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName()
    +        val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
                     ?: mxCall.roomId
             CallService.onPendingCall(
                     context = context,
                     isVideo = mxCall.isVideoCall,
                     roomName = name,
                     roomId = mxCall.roomId,
    -                matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
    +                matrixId = currentSession?.myUserId ?: "",
                     callId = mxCall.callId
             )
             executor.execute {
    @@ -563,14 +568,14 @@ class WebRtcPeerConnectionManager @Inject constructor(
                         ?.let { mxCall ->
                             // Start background service with notification
     
    -                        val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName()
    +                        val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
                                     ?: mxCall.otherUserId
                             CallService.onOnGoingCallBackground(
                                     context = context,
                                     isVideo = mxCall.isVideoCall,
                                     roomName = name,
                                     roomId = mxCall.roomId,
    -                                matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
    +                                matrixId = currentSession?.myUserId ?: "",
                                     callId = mxCall.callId
                             )
                         }
    @@ -631,20 +636,20 @@ class WebRtcPeerConnectionManager @Inject constructor(
             }
     
             Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall")
    -        val createdCall = sessionHolder.getSafeActiveSession()?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return
    +        val createdCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return
             val callContext = CallContext(createdCall)
     
             audioManager.startForCall(createdCall)
             currentCall = callContext
     
    -        val name = sessionHolder.getSafeActiveSession()?.getUser(createdCall.otherUserId)?.getBestName()
    +        val name = currentSession?.getUser(createdCall.otherUserId)?.getBestName()
                     ?: createdCall.otherUserId
             CallService.onOutgoingCallRinging(
                     context = context.applicationContext,
                     isVideo = createdCall.isVideoCall,
                     roomName = name,
                     roomId = createdCall.roomId,
    -                matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
    +                matrixId = currentSession?.myUserId ?: "",
                     callId = createdCall.callId)
     
             executor.execute {
    @@ -693,14 +698,14 @@ class WebRtcPeerConnectionManager @Inject constructor(
             }
     
             // Start background service with notification
    -        val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName()
    +        val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
                     ?: mxCall.otherUserId
             CallService.onIncomingCallRinging(
                     context = context,
                     isVideo = mxCall.isVideoCall,
                     roomName = name,
                     roomId = mxCall.roomId,
    -                matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
    +                matrixId = currentSession?.myUserId ?: "",
                     callId = mxCall.callId
             )
     
    @@ -818,14 +823,14 @@ class WebRtcPeerConnectionManager @Inject constructor(
             }
             val mxCall = call.mxCall
             // Update service state
    -        val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName()
    +        val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
                     ?: mxCall.otherUserId
             CallService.onPendingCall(
                     context = context,
                     isVideo = mxCall.isVideoCall,
                     roomName = name,
                     roomId = mxCall.roomId,
    -                matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
    +                matrixId = currentSession?.myUserId ?: "",
                     callId = mxCall.callId
             )
             executor.execute {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
    index c1f4187e0b..655621f9ad 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
    @@ -40,17 +40,20 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
     import im.vector.matrix.android.api.session.widgets.model.WidgetContent
     import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
     import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
    +import im.vector.riotx.ActiveSessionDataSource
     import im.vector.riotx.R
    -import im.vector.riotx.core.di.ActiveSessionHolder
     import im.vector.riotx.core.resources.StringProvider
     import timber.log.Timber
     import javax.inject.Inject
     
    -class NoticeEventFormatter @Inject constructor(private val sessionHolder: ActiveSessionHolder,
    +class NoticeEventFormatter @Inject constructor(private val activeSessionDataSource: ActiveSessionDataSource,
                                                    private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
                                                    private val sp: StringProvider) {
     
    -    private fun Event.isSentByCurrentUser() = senderId != null && senderId == sessionHolder.getSafeActiveSession()?.myUserId
    +    private val currentUserId: String?
    +        get() = activeSessionDataSource.currentValue?.orNull()?.myUserId
    +
    +    private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId
     
         fun format(timelineEvent: TimelineEvent): CharSequence? {
             return when (val type = timelineEvent.root.getClearType()) {
    @@ -449,7 +452,6 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
             val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: event.stateKey ?: ""
             return when (eventContent?.membership) {
                 Membership.INVITE -> {
    -                val selfUserId = sessionHolder.getSafeActiveSession()?.myUserId
                     when {
                         eventContent.thirdPartyInvite != null -> {
                             val userWhoHasAccepted = eventContent.thirdPartyInvite?.signed?.mxid ?: event.stateKey
    @@ -466,7 +468,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
                                 sp.getString(R.string.notice_room_third_party_registered_invite, userWhoHasAccepted, threePidDisplayName)
                             }
                         }
    -                    event.stateKey == selfUserId          ->
    +                    event.stateKey == currentUserId       ->
                             eventContent.safeReason?.let { reason ->
                                 sp.getString(R.string.notice_room_invite_you_with_reason, senderDisplayName, reason)
                             } ?: sp.getString(R.string.notice_room_invite_you, senderDisplayName)
    diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt
    index f85d91d9c5..071e23c252 100644
    --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt
    @@ -49,9 +49,6 @@ import im.vector.riotx.core.extensions.exhaustive
     import im.vector.riotx.core.platform.VectorViewModel
     import im.vector.riotx.core.resources.StringProvider
     import im.vector.riotx.core.utils.ensureTrailingSlash
    -import im.vector.riotx.features.call.WebRtcPeerConnectionManager
    -import im.vector.riotx.features.notifications.PushRuleTriggerListener
    -import im.vector.riotx.features.session.SessionListener
     import im.vector.riotx.features.signout.soft.SoftLogoutActivity
     import timber.log.Timber
     import java.util.concurrent.CancellationException
    @@ -64,13 +61,10 @@ class LoginViewModel @AssistedInject constructor(
             private val applicationContext: Context,
             private val authenticationService: AuthenticationService,
             private val activeSessionHolder: ActiveSessionHolder,
    -        private val pushRuleTriggerListener: PushRuleTriggerListener,
             private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory,
    -        private val sessionListener: SessionListener,
             private val reAuthHelper: ReAuthHelper,
    -        private val stringProvider: StringProvider,
    -        private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager)
    -    : VectorViewModel(initialState) {
    +        private val stringProvider: StringProvider
    +) : VectorViewModel(initialState) {
     
         @AssistedInject.Factory
         interface Factory {
    @@ -667,11 +661,7 @@ class LoginViewModel @AssistedInject constructor(
     
         private fun onSessionCreated(session: Session) {
             activeSessionHolder.setActiveSession(session)
    -        session.configureAndStart(
    -                applicationContext,
    -                pushRuleTriggerListener,
    -                webRtcPeerConnectionManager,
    -                sessionListener)
    +        session.configureAndStart(applicationContext)
             setState {
                 copy(
                         asyncLoginAction = Success(Unit)
    diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt
    index 6fc396b264..d0839795dd 100644
    --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt
    @@ -22,10 +22,11 @@ import android.os.HandlerThread
     import androidx.annotation.WorkerThread
     import androidx.core.app.NotificationCompat
     import androidx.core.app.Person
    +import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.content.ContentUrlResolver
    +import im.vector.riotx.ActiveSessionDataSource
     import im.vector.riotx.BuildConfig
     import im.vector.riotx.R
    -import im.vector.riotx.core.di.ActiveSessionHolder
     import im.vector.riotx.core.resources.StringProvider
     import im.vector.riotx.features.settings.VectorPreferences
     import me.gujun.android.span.span
    @@ -46,7 +47,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
                                                         private val notificationUtils: NotificationUtils,
                                                         private val vectorPreferences: VectorPreferences,
                                                         private val stringProvider: StringProvider,
    -                                                    private val activeSessionHolder: ActiveSessionHolder,
    +                                                    private val activeSessionDataSource: ActiveSessionDataSource,
                                                         private val iconLoader: IconLoader,
                                                         private val bitmapLoader: BitmapLoader,
                                                         private val outdatedDetector: OutdatedEventDetector?) {
    @@ -68,6 +69,10 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
     
         private var currentRoomId: String? = null
     
    +    // TODO Multi-session: this will have to be improved
    +    private val currentSession: Session?
    +        get() = activeSessionDataSource.currentValue?.orNull()
    +
         /**
         Should be called as soon as a new event is ready to be displayed.
         The notification corresponding to this event will not be displayed until
    @@ -204,7 +209,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
         private fun refreshNotificationDrawerBg() {
             Timber.v("refreshNotificationDrawerBg()")
     
    -        val session = activeSessionHolder.getSafeActiveSession() ?: return
    +        val session = currentSession ?: return
     
             val user = session.getUser(session.myUserId)
             // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash
    @@ -474,7 +479,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
                     val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
                     if (!file.exists()) file.createNewFile()
                     FileOutputStream(file).use {
    -                    activeSessionHolder.getSafeActiveSession()?.securelyStoreObject(eventList, KEY_ALIAS_SECRET_STORAGE, it)
    +                    currentSession?.securelyStoreObject(eventList, KEY_ALIAS_SECRET_STORAGE, it)
                     }
                 } catch (e: Throwable) {
                     Timber.e(e, "## Failed to save cached notification info")
    @@ -487,7 +492,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
                 val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
                 if (file.exists()) {
                     FileInputStream(file).use {
    -                    val events: ArrayList? = activeSessionHolder.getSafeActiveSession()?.loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE)
    +                    val events: ArrayList? = currentSession?.loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE)
                         if (events != null) {
                             return events.toMutableList()
                         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/OutdatedEventDetector.kt b/vector/src/main/java/im/vector/riotx/features/notifications/OutdatedEventDetector.kt
    index 6b8d3dae49..d2b939bc99 100644
    --- a/vector/src/main/java/im/vector/riotx/features/notifications/OutdatedEventDetector.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/notifications/OutdatedEventDetector.kt
    @@ -15,10 +15,12 @@
      */
     package im.vector.riotx.features.notifications
     
    -import im.vector.riotx.core.di.ActiveSessionHolder
    +import im.vector.riotx.ActiveSessionDataSource
     import javax.inject.Inject
     
    -class OutdatedEventDetector @Inject constructor(private val activeSessionHolder: ActiveSessionHolder) {
    +class OutdatedEventDetector @Inject constructor(
    +        private val activeSessionDataSource: ActiveSessionDataSource
    +) {
     
         /**
          * Returns true if the given event is outdated.
    @@ -26,10 +28,12 @@ class OutdatedEventDetector @Inject constructor(private val activeSessionHolder:
          * other device.
          */
         fun isMessageOutdated(notifiableEvent: NotifiableEvent): Boolean {
    +        val session = activeSessionDataSource.currentValue?.orNull() ?: return false
    +
             if (notifiableEvent is NotifiableMessageEvent) {
                 val eventID = notifiableEvent.eventId
                 val roomID = notifiableEvent.roomId
    -            val room = activeSessionHolder.getSafeActiveSession()?.getRoom(roomID) ?: return false
    +            val room = session.getRoom(roomID) ?: return false
                 return room.isEventRead(eventID)
             }
             return false
    diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt b/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt
    index 4ba89c02e2..adef246151 100644
    --- a/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt
    @@ -30,17 +30,17 @@ class PushRuleTriggerListener @Inject constructor(
             private val notificationDrawerManager: NotificationDrawerManager
     ) : PushRuleService.PushRuleListener {
     
    -    var session: Session? = null
    +    private var session: Session? = null
     
         override fun onMatchRule(event: Event, actions: List) {
             Timber.v("Push rule match for event ${event.eventId}")
    -        if (session == null) {
    +        val safeSession = session ?: return Unit.also {
                 Timber.e("Called without active session")
    -            return
             }
    +
             val notificationAction = actions.toNotificationAction()
             if (notificationAction.shouldNotify) {
    -            val notifiableEvent = resolver.resolveEvent(event, session!!)
    +            val notifiableEvent = resolver.resolveEvent(event, safeSession)
                 if (notifiableEvent == null) {
                     Timber.v("## Failed to resolve event")
                     // TODO
    
    From 633548f190696507fc3ffdd27c380367c78827fc Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 3 Jul 2020 16:00:39 +0200
    Subject: [PATCH 060/122] Create ImageManager to be able to (re-)configure the
     lib
    
    ---
     .../java/im/vector/riotx/VectorApplication.kt |  3 --
     .../riotx/core/di/ActiveSessionHolder.kt      |  4 ++-
     .../im/vector/riotx/core/di/ImageManager.kt   | 35 +++++++++++++++++++
     3 files changed, 38 insertions(+), 4 deletions(-)
     create mode 100644 vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt
    
    diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    index d7fe2a054d..db14dba93d 100644
    --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    @@ -32,8 +32,6 @@ import com.airbnb.epoxy.EpoxyAsyncUtil
     import com.airbnb.epoxy.EpoxyController
     import com.facebook.stetho.Stetho
     import com.gabrielittner.threetenbp.LazyThreeTen
    -import com.github.piasy.biv.BigImageViewer
    -import com.github.piasy.biv.loader.glide.GlideImageLoader
     import im.vector.matrix.android.api.Matrix
     import im.vector.matrix.android.api.MatrixConfiguration
     import im.vector.matrix.android.api.auth.AuthenticationService
    @@ -108,7 +106,6 @@ class VectorApplication :
             logInfo()
             LazyThreeTen.init(this)
     
    -        BigImageViewer.initialize(GlideImageLoader.with(applicationContext))
             EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
             EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
             registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks(popupAlertManager))
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
    index 9fbd51984f..2dc7b24ebf 100644
    --- a/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
    @@ -37,7 +37,8 @@ class ActiveSessionHolder @Inject constructor(private val authenticationService:
                                                   private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler,
                                                   private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
                                                   private val pushRuleTriggerListener: PushRuleTriggerListener,
    -                                              private val sessionListener: SessionListener
    +                                              private val sessionListener: SessionListener,
    +                                              private val imageManager: ImageManager
     ) {
     
         private var activeSession: AtomicReference = AtomicReference()
    @@ -52,6 +53,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticationService:
             session.addListener(sessionListener)
             pushRuleTriggerListener.startWithSession(session)
             session.callSignalingService().addCallListener(webRtcPeerConnectionManager)
    +        imageManager.onSessionStarted(session)
         }
     
         fun clearActiveSession() {
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt b/vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt
    new file mode 100644
    index 0000000000..7972ebb163
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt
    @@ -0,0 +1,35 @@
    +/*
    + * Copyright (c) 2020 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.riotx.core.di
    +
    +import android.content.Context
    +import com.github.piasy.biv.BigImageViewer
    +import com.github.piasy.biv.loader.glide.GlideImageLoader
    +import im.vector.matrix.android.api.session.Session
    +import javax.inject.Inject
    +
    +/**
    + * This class is used to configure the library we use for images
    + */
    +class ImageManager @Inject constructor(
    +        private val context: Context
    +) {
    +
    +    fun onSessionStarted(session: Session) {
    +        BigImageViewer.initialize(GlideImageLoader.with(context))
    +    }
    +}
    
    From eda29e3fefb9388cafd06374011ba54ad29e591a Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 3 Jul 2020 17:27:00 +0200
    Subject: [PATCH 061/122] Add link for WebRTC
    
    ---
     docs/voip_signaling.md | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/docs/voip_signaling.md b/docs/voip_signaling.md
    index c80cdd6b96..e055b4cd35 100644
    --- a/docs/voip_signaling.md
    +++ b/docs/voip_signaling.md
    @@ -1,5 +1,6 @@
     Useful links:
     - https://codelabs.developers.google.com/codelabs/webrtc-web/#0
    +- http://webrtc.github.io/webrtc-org/native-code/android/
     
     
        ╔════════════════════════════════════════════════╗
    
    From f179fc523d8bcbfd4e0006379ed4fc1f5f8291eb Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 3 Jul 2020 18:35:59 +0200
    Subject: [PATCH 062/122] Give configured OkHttpClient to Glide and
     BigImageViewer
    
    ---
     .../matrix/android/api/session/Session.kt     |  8 ++++
     .../internal/session/DefaultSession.kt        | 12 +++++-
     .../im/vector/riotx/core/di/ImageManager.kt   | 16 +++++++-
     .../im/vector/riotx/core/glide/FactoryUrl.kt  | 38 +++++++++++++++++++
     .../core/glide/VectorGlideModelLoader.kt      |  2 +-
     5 files changed, 71 insertions(+), 5 deletions(-)
     create mode 100644 vector/src/main/java/im/vector/riotx/core/glide/FactoryUrl.kt
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt
    index 5b0f24aed7..8d97dfc01b 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt
    @@ -47,6 +47,7 @@ import im.vector.matrix.android.api.session.terms.TermsService
     import im.vector.matrix.android.api.session.typing.TypingUsersTracker
     import im.vector.matrix.android.api.session.user.UserService
     import im.vector.matrix.android.api.session.widgets.WidgetService
    +import okhttp3.OkHttpClient
     
     /**
      * This interface defines interactions with a session.
    @@ -205,6 +206,13 @@ interface Session :
          */
         fun removeListener(listener: Listener)
     
    +    /**
    +     * Will return a OkHttpClient which will manage pinned certificates and Proxy if configured.
    +     * It will not add any access-token to the request.
    +     * So it is exposed to let the app be able to download image with Glide or any other libraries which accept an OkHttp client.
    +     */
    +    fun getOkHttpClient(): OkHttpClient
    +
         /**
          * A global session listener to get notified for some events.
          */
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt
    index 83ba76d5b8..16179dd64a 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt
    @@ -52,6 +52,7 @@ import im.vector.matrix.android.internal.auth.SessionParamsStore
     import im.vector.matrix.android.internal.crypto.DefaultCryptoService
     import im.vector.matrix.android.internal.di.SessionDatabase
     import im.vector.matrix.android.internal.di.SessionId
    +import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate
     import im.vector.matrix.android.internal.di.WorkManagerProvider
     import im.vector.matrix.android.internal.session.identity.DefaultIdentityService
     import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecryptor
    @@ -64,6 +65,7 @@ import im.vector.matrix.android.internal.util.createUIHandler
     import io.realm.RealmConfiguration
     import kotlinx.coroutines.Dispatchers
     import kotlinx.coroutines.launch
    +import okhttp3.OkHttpClient
     import org.greenrobot.eventbus.EventBus
     import org.greenrobot.eventbus.Subscribe
     import org.greenrobot.eventbus.ThreadMode
    @@ -113,8 +115,10 @@ internal class DefaultSession @Inject constructor(
             private val defaultIdentityService: DefaultIdentityService,
             private val integrationManagerService: IntegrationManagerService,
             private val taskExecutor: TaskExecutor,
    -        private val callSignalingService: Lazy)
    -    : Session,
    +        private val callSignalingService: Lazy,
    +        @UnauthenticatedWithCertificate
    +        private val unauthenticatedWithCertificateOkHttpClient: Lazy
    +) : Session,
             RoomService by roomService.get(),
             RoomDirectoryService by roomDirectoryService.get(),
             GroupService by groupService.get(),
    @@ -255,6 +259,10 @@ internal class DefaultSession @Inject constructor(
     
         override fun callSignalingService(): CallSignalingService = callSignalingService.get()
     
    +    override fun getOkHttpClient(): OkHttpClient {
    +        return unauthenticatedWithCertificateOkHttpClient.get()
    +    }
    +
         override fun addListener(listener: Session.Listener) {
             sessionListeners.addListener(listener)
         }
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt b/vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt
    index 7972ebb163..74a01e76ec 100644
    --- a/vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt
    @@ -17,19 +17,31 @@
     package im.vector.riotx.core.di
     
     import android.content.Context
    +import com.bumptech.glide.Glide
    +import com.bumptech.glide.load.model.GlideUrl
     import com.github.piasy.biv.BigImageViewer
     import com.github.piasy.biv.loader.glide.GlideImageLoader
     import im.vector.matrix.android.api.session.Session
    +import im.vector.riotx.ActiveSessionDataSource
    +import im.vector.riotx.core.glide.FactoryUrl
    +import java.io.InputStream
     import javax.inject.Inject
     
     /**
      * This class is used to configure the library we use for images
      */
     class ImageManager @Inject constructor(
    -        private val context: Context
    +        private val context: Context,
    +        private val activeSessionDataSource: ActiveSessionDataSource
     ) {
     
         fun onSessionStarted(session: Session) {
    -        BigImageViewer.initialize(GlideImageLoader.with(context))
    +        // Do this call first
    +        BigImageViewer.initialize(GlideImageLoader.with(context, session.getOkHttpClient()))
    +
    +        val glide = Glide.get(context)
    +
    +        // And this one. FIXME But are losing what BigImageViewer has done to add a Progress listener
    +        glide.registry.replace(GlideUrl::class.java, InputStream::class.java, FactoryUrl(activeSessionDataSource))
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/core/glide/FactoryUrl.kt b/vector/src/main/java/im/vector/riotx/core/glide/FactoryUrl.kt
    new file mode 100644
    index 0000000000..fc037894db
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/core/glide/FactoryUrl.kt
    @@ -0,0 +1,38 @@
    +/*
    + * Copyright (c) 2020 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.riotx.core.glide
    +
    +import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
    +import com.bumptech.glide.load.model.GlideUrl
    +import com.bumptech.glide.load.model.ModelLoader
    +import com.bumptech.glide.load.model.ModelLoaderFactory
    +import com.bumptech.glide.load.model.MultiModelLoaderFactory
    +import im.vector.riotx.ActiveSessionDataSource
    +import okhttp3.OkHttpClient
    +import java.io.InputStream
    +
    +class FactoryUrl(private val activeSessionDataSource: ActiveSessionDataSource) : ModelLoaderFactory {
    +
    +    override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader {
    +        val client = activeSessionDataSource.currentValue?.orNull()?.getOkHttpClient() ?: OkHttpClient()
    +        return OkHttpUrlLoader(client)
    +    }
    +
    +    override fun teardown() {
    +        // Do nothing, this instance doesn't own the client.
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/core/glide/VectorGlideModelLoader.kt b/vector/src/main/java/im/vector/riotx/core/glide/VectorGlideModelLoader.kt
    index 191ab6d972..510eef71e1 100644
    --- a/vector/src/main/java/im/vector/riotx/core/glide/VectorGlideModelLoader.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/glide/VectorGlideModelLoader.kt
    @@ -65,7 +65,7 @@ class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolde
                                  private val height: Int)
         : DataFetcher {
     
    -    val client = OkHttpClient()
    +    private val client = activeSessionHolder.getSafeActiveSession()?.getOkHttpClient() ?: OkHttpClient()
     
         override fun getDataClass(): Class {
             return InputStream::class.java
    
    From d63f00851a26a88b7b6cced6aeb4b4ee94ee587b Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 3 Jul 2020 21:17:47 +0200
    Subject: [PATCH 063/122] Rename parameters
    
    ---
     .../vector/riotx/features/home/AvatarRenderer.kt | 16 ++++++++--------
     1 file changed, 8 insertions(+), 8 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
    index 687c280910..f917b5a9f9 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
    @@ -65,19 +65,19 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
     
         @UiThread
         fun render(context: Context,
    -               glideRequest: GlideRequests,
    +               glideRequests: GlideRequests,
                    matrixItem: MatrixItem,
                    target: Target) {
             val placeholder = getPlaceholderDrawable(context, matrixItem)
    -        buildGlideRequest(glideRequest, matrixItem.avatarUrl)
    +        buildGlideRequest(glideRequests, matrixItem.avatarUrl)
                     .placeholder(placeholder)
                     .into(target)
         }
     
         @AnyThread
         @Throws
    -    fun shortcutDrawable(context: Context, glideRequest: GlideRequests, matrixItem: MatrixItem, iconSize: Int): Bitmap {
    -        return glideRequest
    +    fun shortcutDrawable(context: Context, glideRequests: GlideRequests, matrixItem: MatrixItem, iconSize: Int): Bitmap {
    +        return glideRequests
                     .asBitmap()
                     .apply {
                         val resolvedUrl = resolvedUrl(matrixItem.avatarUrl)
    @@ -98,8 +98,8 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
         }
     
         @AnyThread
    -    fun getCachedDrawable(glideRequest: GlideRequests, matrixItem: MatrixItem): Drawable {
    -        return buildGlideRequest(glideRequest, matrixItem.avatarUrl)
    +    fun getCachedDrawable(glideRequests: GlideRequests, matrixItem: MatrixItem): Drawable {
    +        return buildGlideRequest(glideRequests, matrixItem.avatarUrl)
                     .onlyRetrieveFromCache(true)
                     .submit()
                     .get()
    @@ -117,9 +117,9 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
     
         // PRIVATE API *********************************************************************************
     
    -    private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest {
    +    private fun buildGlideRequest(glideRequests: GlideRequests, avatarUrl: String?): GlideRequest {
             val resolvedUrl = resolvedUrl(avatarUrl)
    -        return glideRequest
    +        return glideRequests
                     .load(resolvedUrl)
                     .apply(RequestOptions.circleCropTransform())
         }
    
    From 51898a810937baa2285a926bded3de33f873e3cb Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 10 Jul 2020 15:06:34 +0200
    Subject: [PATCH 064/122] Create new strings for change translations
    
    ---
     .../im/vector/riotx/core/ui/views/KeysBackupBanner.kt     | 8 ++++----
     vector/src/main/res/layout/view_keys_backup_banner.xml    | 4 ++--
     vector/src/main/res/values/strings.xml                    | 7 +++++--
     3 files changed, 11 insertions(+), 8 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt
    index 252eab02a6..d0cea6194b 100755
    --- a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt
    @@ -17,7 +17,6 @@
     package im.vector.riotx.core.ui.views
     
     import android.content.Context
    -import androidx.preference.PreferenceManager
     import android.util.AttributeSet
     import android.view.View
     import android.view.ViewGroup
    @@ -25,6 +24,7 @@ import android.widget.TextView
     import androidx.constraintlayout.widget.ConstraintLayout
     import androidx.core.content.edit
     import androidx.core.view.isVisible
    +import androidx.preference.PreferenceManager
     import androidx.transition.TransitionManager
     import butterknife.BindView
     import butterknife.ButterKnife
    @@ -160,9 +160,9 @@ class KeysBackupBanner @JvmOverloads constructor(
             } else {
                 isVisible = true
     
    -            textView1.setText(R.string.keys_backup_banner_setup_line1)
    +            textView1.setText(R.string.secure_backup_banner_setup_line1)
                 textView2.isVisible = true
    -            textView2.setText(R.string.keys_backup_banner_setup_line2)
    +            textView2.setText(R.string.secure_backup_banner_setup_line2)
                 close.isVisible = true
             }
         }
    @@ -195,7 +195,7 @@ class KeysBackupBanner @JvmOverloads constructor(
     
         private fun renderBackingUp() {
             isVisible = true
    -        textView1.setText(R.string.keys_backup_banner_setup_line1)
    +        textView1.setText(R.string.secure_backup_banner_setup_line1)
             textView2.isVisible = true
             textView2.setText(R.string.keys_backup_banner_in_progress)
             loading.isVisible = true
    diff --git a/vector/src/main/res/layout/view_keys_backup_banner.xml b/vector/src/main/res/layout/view_keys_backup_banner.xml
    index 4c3ec1da3f..6c8fc2b5a1 100644
    --- a/vector/src/main/res/layout/view_keys_backup_banner.xml
    +++ b/vector/src/main/res/layout/view_keys_backup_banner.xml
    @@ -34,7 +34,7 @@
             android:layout_height="wrap_content"
             android:layout_marginStart="27dp"
             android:layout_marginLeft="27dp"
    -        android:text="@string/keys_backup_banner_setup_line1"
    +        android:text="@string/secure_backup_banner_setup_line1"
             android:textColor="?riotx_text_primary"
             android:textSize="18sp"
             app:layout_constraintBottom_toTopOf="@id/view_keys_backup_banner_text_2"
    @@ -48,7 +48,7 @@
             android:layout_height="wrap_content"
             android:layout_marginStart="27dp"
             android:layout_marginLeft="27dp"
    -        android:text="@string/keys_backup_banner_setup_line2"
    +        android:text="@string/secure_backup_banner_setup_line2"
             android:textColor="?riotx_text_secondary"
             android:textSize="14sp"
             android:visibility="gone"
    diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
    index 8451191273..f34224b246 100644
    --- a/vector/src/main/res/values/strings.xml
    +++ b/vector/src/main/res/values/strings.xml
    @@ -1496,8 +1496,11 @@ Why choose Riot.im?
         It was me
     
         
    -    Secure Backup
    -    Safeguard against losing access to encrypted messages & data 
    +    Never lose encrypted messages
    +    Start using Key Backup
    +
    +    Secure Backup
    +    Safeguard against losing access to encrypted messages & data
     
         Never lose encrypted messages
         Use Key Backup
    
    From 179474b975a2628c8b108dd85e6cd06661cab51a Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 10 Jul 2020 17:51:57 +0200
    Subject: [PATCH 065/122] Cleanup
    
    ---
     .../riotx/features/reactions/widget/ReactionButton.kt  | 10 +++++-----
     1 file changed, 5 insertions(+), 5 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt b/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt
    index ec5aba8ee5..140edaf03a 100644
    --- a/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt
    @@ -45,7 +45,8 @@ import javax.inject.Inject
      * An animated reaction button.
      * Displays a String reaction (emoji), with a count, and that can be selected or not (toggle)
      */
    -class ReactionButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
    +class ReactionButton @JvmOverloads constructor(context: Context,
    +                                               attrs: AttributeSet? = null,
                                                    defStyleAttr: Int = 0)
         : FrameLayout(context, attrs, defStyleAttr), View.OnClickListener, View.OnLongClickListener {
     
    @@ -110,9 +111,7 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut
             countTextView?.text = TextUtils.formatCountToShortDecimal(reactionCount)
     
     //        emojiView?.typeface = this.emojiTypeFace ?: Typeface.DEFAULT
    -
             context.withStyledAttributes(attrs, R.styleable.ReactionButton, defStyleAttr) {
    -
                 onDrawable = ContextCompat.getDrawable(context, R.drawable.rounded_rect_shape)
                 offDrawable = ContextCompat.getDrawable(context, R.drawable.rounded_rect_shape_off)
     
    @@ -143,9 +142,10 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut
     
                 val status = getBoolean(R.styleable.ReactionButton_toggled, false)
                 setChecked(status)
    -            setOnClickListener(this@ReactionButton)
    -            setOnLongClickListener(this@ReactionButton)
             }
    +
    +        setOnClickListener(this)
    +        setOnLongClickListener(this)
         }
     
         private fun getDrawableFromResource(array: TypedArray, styleableIndexId: Int): Drawable? {
    
    From 150d44aafd3afa6639c6d8dc834b6626ab490b95 Mon Sep 17 00:00:00 2001
    From: ganfra 
    Date: Fri, 10 Jul 2020 20:08:51 +0200
    Subject: [PATCH 066/122] Improve a bit how joining/leaving are handled
    
    ---
     .../java/im/vector/matrix/rx/RxSession.kt     |  5 ++
     .../api/session/room/RoomDirectoryService.kt  |  7 --
     .../android/api/session/room/RoomService.kt   |  9 +++
     .../session/room/RoomSummaryQueryParams.kt    |  3 +
     .../room/members/ChangeMembershipState.kt     | 34 +++++++++
     .../room/DefaultRoomDirectoryService.kt       |  8 --
     .../session/room/DefaultRoomService.kt        |  7 ++
     .../RoomChangeMembershipStateDataSource.kt    | 67 +++++++++++++++++
     .../room/membership/joining/JoinRoomTask.kt   | 13 +++-
     .../room/membership/leaving/LeaveRoomTask.kt  | 32 ++++++--
     .../room/summary/RoomSummaryDataSource.kt     |  1 +
     .../internal/session/sync/RoomSyncHandler.kt  |  6 ++
     .../home/room/detail/RoomDetailFragment.kt    |  2 +-
     .../home/room/detail/RoomDetailViewModel.kt   | 19 ++++-
     .../home/room/detail/RoomDetailViewState.kt   |  2 +
     .../home/room/list/RoomInvitationItem.kt      | 68 +++++++----------
     .../home/room/list/RoomListViewModel.kt       | 62 +++++-----------
     .../home/room/list/RoomListViewState.kt       | 10 +--
     .../home/room/list/RoomSummaryController.kt   | 21 ++----
     .../home/room/list/RoomSummaryItemFactory.kt  | 23 +++---
     .../invite/InviteButtonStateBinder.kt         | 48 ++++++++++++
     .../riotx/features/invite/VectorInviteView.kt | 26 ++++++-
     .../features/navigation/DefaultNavigator.kt   |  5 +-
     .../riotx/features/navigation/Navigator.kt    |  3 +-
     .../roomdirectory/PublicRoomsController.kt    | 12 +--
     .../roomdirectory/PublicRoomsFragment.kt      | 24 +++---
     .../roomdirectory/PublicRoomsViewState.kt     | 10 +--
     .../roomdirectory/RoomDirectoryAction.kt      |  2 +-
     .../roomdirectory/RoomDirectoryViewModel.kt   | 73 +++++++------------
     .../roompreview/RoomPreviewAction.kt          |  2 +-
     .../roompreview/RoomPreviewActivity.kt        |  9 ++-
     .../RoomPreviewNoPreviewFragment.kt           |  2 +-
     .../roompreview/RoomPreviewViewModel.kt       | 69 +++++++++---------
     .../roompreview/RoomPreviewViewState.kt       | 12 ++-
     .../roomprofile/RoomProfileFragment.kt        |  1 -
     .../roomprofile/RoomProfileViewEvents.kt      |  1 -
     .../roomprofile/RoomProfileViewModel.kt       |  2 +-
     .../main/res/layout/vector_invite_view.xml    | 37 +++++-----
     .../src/main/res/layout/view_button_state.xml |  1 +
     39 files changed, 454 insertions(+), 284 deletions(-)
     create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/ChangeMembershipState.kt
     create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomChangeMembershipStateDataSource.kt
     create mode 100644 vector/src/main/java/im/vector/riotx/features/invite/InviteButtonStateBinder.kt
    
    diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt
    index e8fef1361d..54dacb5614 100644
    --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt
    +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt
    @@ -25,6 +25,7 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary
     import im.vector.matrix.android.api.session.identity.ThreePid
     import im.vector.matrix.android.api.session.pushers.Pusher
     import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
    +import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
     import im.vector.matrix.android.api.session.room.model.RoomSummary
     import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
     import im.vector.matrix.android.api.session.sync.SyncState
    @@ -165,6 +166,10 @@ class RxSession(private val session: Session) {
                         session.widgetService().getRoomWidgets(roomId, widgetId, widgetTypes, excludedTypes)
                     }
         }
    +
    +    fun liveRoomChangeMembershipState(): Observable> {
    +        return session.getChangeMembershipsLive().asObservable()
    +    }
     }
     
     fun Session.rx(): RxSession {
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt
    index 0273c789dd..7014fbff37 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt
    @@ -34,13 +34,6 @@ interface RoomDirectoryService {
                            publicRoomsParams: PublicRoomsParams,
                            callback: MatrixCallback): Cancelable
     
    -    /**
    -     * Join a room by id, or room alias
    -     */
    -    fun joinRoom(roomIdOrAlias: String,
    -                 reason: String? = null,
    -                 callback: MatrixCallback): Cancelable
    -
         /**
          * Fetches the overall metadata about protocols supported by the homeserver.
          * Includes both the available protocols and all fields required for queries against each protocol.
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt
    index bc6c17a130..3093fb8ec5 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt
    @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room
     
     import androidx.lifecycle.LiveData
     import im.vector.matrix.android.api.MatrixCallback
    +import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
     import im.vector.matrix.android.api.session.room.model.RoomSummary
     import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
     import im.vector.matrix.android.api.util.Cancelable
    @@ -104,5 +105,13 @@ interface RoomService {
                              searchOnServer: Boolean,
                              callback: MatrixCallback>): Cancelable
     
    +    /**
    +     * Return a live data of all local changes membership who happened since the session has been opened.
    +     * It allows you to track this in your client to known what is currently being processed by the SDK.
    +     * It won't know anything about change being done in other client.
    +     * Keys are roomId or roomAlias, depending of what you used as parameter for the join/leave action
    +     */
    +    fun getChangeMembershipsLive(): LiveData>
    +
         fun getExistingDirectRoomWithUser(otherUserId: String) : Room?
     }
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomSummaryQueryParams.kt
    index 6983bda225..51df30ad75 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomSummaryQueryParams.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomSummaryQueryParams.kt
    @@ -28,6 +28,7 @@ fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {
      * [im.vector.matrix.android.api.session.room.Room] and [im.vector.matrix.android.api.session.room.RoomService]
      */
     data class RoomSummaryQueryParams(
    +        val roomId: QueryStringValue,
             val displayName: QueryStringValue,
             val canonicalAlias: QueryStringValue,
             val memberships: List
    @@ -35,11 +36,13 @@ data class RoomSummaryQueryParams(
     
         class Builder {
     
    +        var roomId: QueryStringValue = QueryStringValue.IsNotEmpty
             var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
             var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition
             var memberships: List = Membership.all()
     
             fun build() = RoomSummaryQueryParams(
    +                roomId = roomId,
                     displayName = displayName,
                     canonicalAlias = canonicalAlias,
                     memberships = memberships
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/ChangeMembershipState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/ChangeMembershipState.kt
    new file mode 100644
    index 0000000000..8f42e310bf
    --- /dev/null
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/ChangeMembershipState.kt
    @@ -0,0 +1,34 @@
    +/*
    + * Copyright (c) 2020 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.api.session.room.members
    +
    +sealed class ChangeMembershipState() {
    +    object Unknown : ChangeMembershipState()
    +    object Joining : ChangeMembershipState()
    +    data class FailedJoining(val throwable: Throwable) : ChangeMembershipState()
    +    object Joined : ChangeMembershipState()
    +    object Leaving : ChangeMembershipState()
    +    data class FailedLeaving(val throwable: Throwable) : ChangeMembershipState()
    +    object Left : ChangeMembershipState()
    +
    +    fun isInProgress() = this is Joining || this is Leaving
    +
    +    fun isSuccessful() = this is Joined || this is Left
    +
    +    fun isFailed() = this is FailedJoining || this is FailedLeaving
    +
    +}
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt
    index ef55702de6..e89fae647f 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt
    @@ -44,14 +44,6 @@ internal class DefaultRoomDirectoryService @Inject constructor(private val getPu
                     .executeBy(taskExecutor)
         }
     
    -    override fun joinRoom(roomIdOrAlias: String, reason: String?, callback: MatrixCallback): Cancelable {
    -        return joinRoomTask
    -                .configureWith(JoinRoomTask.Params(roomIdOrAlias, reason)) {
    -                    this.callback = callback
    -                }
    -                .executeBy(taskExecutor)
    -    }
    -
         override fun getThirdPartyProtocol(callback: MatrixCallback>): Cancelable {
             return getThirdPartyProtocolsTask
                     .configureWith {
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt
    index c773682c0f..b8b4c968b1 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt
    @@ -21,12 +21,14 @@ import im.vector.matrix.android.api.MatrixCallback
     import im.vector.matrix.android.api.session.room.Room
     import im.vector.matrix.android.api.session.room.RoomService
     import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
    +import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
     import im.vector.matrix.android.api.session.room.model.RoomSummary
     import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
     import im.vector.matrix.android.api.util.Cancelable
     import im.vector.matrix.android.api.util.Optional
     import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
     import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
    +import im.vector.matrix.android.internal.session.room.membership.RoomChangeMembershipStateDataSource
     import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
     import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
     import im.vector.matrix.android.internal.session.room.summary.RoomSummaryDataSource
    @@ -43,6 +45,7 @@ internal class DefaultRoomService @Inject constructor(
             private val roomIdByAliasTask: GetRoomIdByAliasTask,
             private val roomGetter: RoomGetter,
             private val roomSummaryDataSource: RoomSummaryDataSource,
    +        private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
             private val taskExecutor: TaskExecutor
     ) : RoomService {
     
    @@ -111,4 +114,8 @@ internal class DefaultRoomService @Inject constructor(
                     }
                     .executeBy(taskExecutor)
         }
    +
    +    override fun getChangeMembershipsLive(): LiveData> {
    +        return roomChangeMembershipStateDataSource.getLiveStates()
    +    }
     }
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomChangeMembershipStateDataSource.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomChangeMembershipStateDataSource.kt
    new file mode 100644
    index 0000000000..5cf75c3bbd
    --- /dev/null
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomChangeMembershipStateDataSource.kt
    @@ -0,0 +1,67 @@
    +/*
    + * Copyright (c) 2020 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.session.room.membership
    +
    +import androidx.lifecycle.LiveData
    +import androidx.lifecycle.MutableLiveData
    +import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
    +import im.vector.matrix.android.api.session.room.model.Membership
    +import im.vector.matrix.android.internal.session.SessionScope
    +import javax.inject.Inject
    +
    +/**
    + * This class holds information about rooms that current user is joining or leaving.
    + */
    +@SessionScope
    +internal class RoomChangeMembershipStateDataSource @Inject constructor() {
    +
    +    private val mutableLiveStates = MutableLiveData>(emptyMap())
    +    private val states = HashMap()
    +
    +    /**
    +     * This will update local states to be synced with the server.
    +     */
    +    fun setMembershipFromSync(roomId: String, membership: Membership) {
    +        if (states.containsKey(roomId)) {
    +            val newState = membership.toMembershipChangeState()
    +            updateState(roomId, newState)
    +        }
    +    }
    +
    +    fun updateState(roomId: String, state: ChangeMembershipState) {
    +        states[roomId] = state
    +        mutableLiveStates.postValue(states.toMap())
    +    }
    +
    +    fun getLiveStates(): LiveData> {
    +        return mutableLiveStates
    +    }
    +
    +    fun getState(roomId: String): ChangeMembershipState {
    +        return states.getOrElse(roomId) {
    +            ChangeMembershipState.Unknown
    +        }
    +    }
    +
    +    private fun Membership.toMembershipChangeState(): ChangeMembershipState {
    +        return when {
    +            this == Membership.JOIN -> ChangeMembershipState.Joined
    +            this.isLeft()           -> ChangeMembershipState.Left
    +            else                    -> ChangeMembershipState.Unknown
    +        }
    +    }
    +}
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt
    index 635f3955c2..7467a595bc 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt
    @@ -17,6 +17,7 @@
     package im.vector.matrix.android.internal.session.room.membership.joining
     
     import im.vector.matrix.android.api.session.room.failure.JoinRoomFailure
    +import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
     import im.vector.matrix.android.api.session.room.model.create.JoinRoomResponse
     import im.vector.matrix.android.internal.database.awaitNotEmptyResult
     import im.vector.matrix.android.internal.database.model.RoomEntity
    @@ -24,6 +25,7 @@ import im.vector.matrix.android.internal.database.model.RoomEntityFields
     import im.vector.matrix.android.internal.di.SessionDatabase
     import im.vector.matrix.android.internal.network.executeRequest
     import im.vector.matrix.android.internal.session.room.RoomAPI
    +import im.vector.matrix.android.internal.session.room.membership.RoomChangeMembershipStateDataSource
     import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
     import im.vector.matrix.android.internal.task.Task
     import io.realm.RealmConfiguration
    @@ -45,12 +47,19 @@ internal class DefaultJoinRoomTask @Inject constructor(
             private val readMarkersTask: SetReadMarkersTask,
             @SessionDatabase
             private val realmConfiguration: RealmConfiguration,
    +        private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
             private val eventBus: EventBus
     ) : JoinRoomTask {
     
         override suspend fun execute(params: JoinRoomTask.Params) {
    -        val joinRoomResponse = executeRequest(eventBus) {
    -            apiCall = roomAPI.join(params.roomIdOrAlias, params.viaServers, mapOf("reason" to params.reason))
    +        roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.Joining)
    +        val joinRoomResponse = try {
    +            executeRequest(eventBus) {
    +                apiCall = roomAPI.join(params.roomIdOrAlias, params.viaServers, mapOf("reason" to params.reason))
    +            }
    +        } catch (failure: Throwable) {
    +            roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.FailedJoining(failure))
    +            throw failure
             }
             // Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before)
             val roomId = joinRoomResponse.roomId
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt
    index 2d56e5bd47..94645f3d98 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt
    @@ -19,12 +19,16 @@ package im.vector.matrix.android.internal.session.room.membership.leaving
     import im.vector.matrix.android.api.query.QueryStringValue
     import im.vector.matrix.android.api.session.events.model.EventType
     import im.vector.matrix.android.api.session.events.model.toModel
    +import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
     import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent
     import im.vector.matrix.android.internal.network.executeRequest
     import im.vector.matrix.android.internal.session.room.RoomAPI
    +import im.vector.matrix.android.internal.session.room.membership.RoomChangeMembershipStateDataSource
     import im.vector.matrix.android.internal.session.room.state.StateEventDataSource
    +import im.vector.matrix.android.internal.session.room.summary.RoomSummaryDataSource
     import im.vector.matrix.android.internal.task.Task
     import org.greenrobot.eventbus.EventBus
    +import timber.log.Timber
     import javax.inject.Inject
     
     internal interface LeaveRoomTask : Task {
    @@ -37,7 +41,9 @@ internal interface LeaveRoomTask : Task {
     internal class DefaultLeaveRoomTask @Inject constructor(
             private val roomAPI: RoomAPI,
             private val eventBus: EventBus,
    -        private val stateEventDataSource: StateEventDataSource
    +        private val stateEventDataSource: StateEventDataSource,
    +        private val roomSummaryDataSource: RoomSummaryDataSource,
    +        private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource
     ) : LeaveRoomTask {
     
         override suspend fun execute(params: LeaveRoomTask.Params) {
    @@ -45,13 +51,29 @@ internal class DefaultLeaveRoomTask @Inject constructor(
         }
     
         private suspend fun leaveRoom(roomId: String, reason: String?) {
    -        val roomCreateStateEvent = stateEventDataSource.getStateEvent(roomId, eventType = EventType.STATE_ROOM_CREATE, stateKey = QueryStringValue.NoCondition)
    -        val predecessorRoomId = roomCreateStateEvent?.getClearContent()?.toModel()?.predecessor?.roomId
    -        executeRequest(eventBus) {
    -            apiCall = roomAPI.leave(roomId, mapOf("reason" to reason))
    +        val roomSummary = roomSummaryDataSource.getRoomSummary(roomId)
    +        if (roomSummary?.membership?.isActive() == false) {
    +            Timber.v("Room $roomId is not joined so can't be left")
    +            return
             }
    +        roomChangeMembershipStateDataSource.updateState(roomId, ChangeMembershipState.Leaving)
    +        val roomCreateStateEvent = stateEventDataSource.getStateEvent(
    +                roomId = roomId,
    +                eventType = EventType.STATE_ROOM_CREATE,
    +                stateKey = QueryStringValue.NoCondition
    +        )
    +        // Server is not cleaning predecessor rooms, so we also try to left them
    +        val predecessorRoomId = roomCreateStateEvent?.getClearContent()?.toModel()?.predecessor?.roomId
             if (predecessorRoomId != null) {
                 leaveRoom(predecessorRoomId, reason)
             }
    +        try {
    +            executeRequest(eventBus) {
    +                apiCall = roomAPI.leave(roomId, mapOf("reason" to reason))
    +            }
    +        } catch (failure: Throwable) {
    +            roomChangeMembershipStateDataSource.updateState(roomId, ChangeMembershipState.FailedLeaving(failure))
    +            throw failure
    +        }
         }
     }
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/summary/RoomSummaryDataSource.kt
    index 7c579a2719..b1518b085d 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/summary/RoomSummaryDataSource.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/summary/RoomSummaryDataSource.kt
    @@ -100,6 +100,7 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
     
         private fun roomSummariesQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery {
             val query = RoomSummaryEntity.where(realm)
    +        query.process(RoomSummaryEntityFields.ROOM_ID, queryParams.roomId)
             query.process(RoomSummaryEntityFields.DISPLAY_NAME, queryParams.displayName)
             query.process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias)
             query.process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships)
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
    index f3af24001d..de2c3cda57 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
    @@ -48,6 +48,7 @@ import im.vector.matrix.android.internal.di.MoshiProvider
     import im.vector.matrix.android.internal.di.UserId
     import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
     import im.vector.matrix.android.internal.session.mapWithProgress
    +import im.vector.matrix.android.internal.session.room.membership.RoomChangeMembershipStateDataSource
     import im.vector.matrix.android.internal.session.room.membership.RoomMemberEventHandler
     import im.vector.matrix.android.internal.session.room.read.FullyReadContent
     import im.vector.matrix.android.internal.session.room.summary.RoomSummaryUpdater
    @@ -73,6 +74,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                                                        private val cryptoService: DefaultCryptoService,
                                                        private val roomMemberEventHandler: RoomMemberEventHandler,
                                                        private val roomTypingUsersHandler: RoomTypingUsersHandler,
    +                                                   private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
                                                        @UserId private val userId: String,
                                                        private val eventBus: EventBus,
                                                        private val timelineEventDecryptor: TimelineEventDecryptor) {
    @@ -185,6 +187,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
             } != null
     
             roomTypingUsersHandler.handle(realm, roomId, ephemeralResult)
    +        roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.JOIN)
             roomSummaryUpdater.update(
                     realm,
                     roomId,
    @@ -221,6 +224,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
             val inviterEvent = roomSync.inviteState?.events?.lastOrNull {
                 it.type == EventType.STATE_ROOM_MEMBER
             }
    +        roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.INVITE)
             roomSummaryUpdater.update(realm, roomId, Membership.INVITE, updateMembers = true, inviterId = inviterEvent?.senderId)
             return roomEntity
         }
    @@ -263,6 +267,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
             val membership = leftMember?.membership ?: Membership.LEAVE
             roomEntity.membership = membership
             roomEntity.chunks.deleteAllFromRealm()
    +        roomTypingUsersHandler.handle(realm, roomId, null)
    +        roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.LEAVE)
             roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary, roomSync.unreadNotifications)
             return roomEntity
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    index 7483d978a3..2f8c72a996 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    @@ -883,7 +883,7 @@ class RoomDetailFragment @Inject constructor(
             } else if (summary?.membership == Membership.INVITE && inviter != null) {
                 roomToolbarContentView.isClickable = false
                 inviteView.visibility = View.VISIBLE
    -            inviteView.render(inviter, VectorInviteView.Mode.LARGE)
    +            inviteView.render(inviter, VectorInviteView.Mode.LARGE, state.changeMembershipState)
                 // Intercept click event
                 inviteView.setOnClickListener { }
             } else if (state.asyncInviter.complete) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    index bed8b4867d..fb1cb8e666 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    @@ -40,6 +40,7 @@ import im.vector.matrix.android.api.session.events.model.toContent
     import im.vector.matrix.android.api.session.events.model.toModel
     import im.vector.matrix.android.api.session.file.FileService
     import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
    +import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
     import im.vector.matrix.android.api.session.room.members.roomMemberQueryParams
     import im.vector.matrix.android.api.session.room.model.Membership
     import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
    @@ -166,6 +167,7 @@ class RoomDetailViewModel @AssistedInject constructor(
             timeline.start()
             timeline.addListener(this)
             observeRoomSummary()
    +        observeMembershipChanges()
             observeSummaryState()
             getUnreadState()
             observeSyncState()
    @@ -406,7 +408,7 @@ class RoomDetailViewModel @AssistedInject constructor(
         private fun isIntegrationEnabled() = session.integrationManagerService().isIntegrationEnabled()
     
         fun isMenuItemVisible(@IdRes itemId: Int): Boolean = com.airbnb.mvrx.withState(this) { state ->
    -        if(state.asyncRoomSummary()?.membership != Membership.JOIN){
    +        if (state.asyncRoomSummary()?.membership != Membership.JOIN) {
                 return@withState false
             }
             when (itemId) {
    @@ -629,7 +631,7 @@ class RoomDetailViewModel @AssistedInject constructor(
         }
     
         private fun handleJoinToAnotherRoomSlashCommand(command: ParsedCommand.JoinRoom) {
    -        session.joinRoom(command.roomAlias, command.reason, object : MatrixCallback {
    +        session.joinRoom(command.roomAlias, command.reason, emptyList(), object : MatrixCallback {
                 override fun onSuccess(data: Unit) {
                     session.getRoomSummary(command.roomAlias)
                             ?.roomId
    @@ -1147,6 +1149,19 @@ class RoomDetailViewModel @AssistedInject constructor(
             }
         }
     
    +    private fun observeMembershipChanges() {
    +        session.rx()
    +                .liveRoomChangeMembershipState()
    +                .map {
    +                    it[initialState.roomId] ?: ChangeMembershipState.Unknown
    +                }
    +                .distinctUntilChanged()
    +                .subscribe {
    +                    setState { copy(changeMembershipState = it) }
    +                }
    +                .disposeOnClear()
    +    }
    +
         private fun observeSummaryState() {
             asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
                 roomSummaryHolder.set(summary)
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
    index 224dd61b65..6800850c48 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
    @@ -20,6 +20,7 @@ import com.airbnb.mvrx.Async
     import com.airbnb.mvrx.MvRxState
     import com.airbnb.mvrx.Uninitialized
     import im.vector.matrix.android.api.session.events.model.Event
    +import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
     import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
     import im.vector.matrix.android.api.session.room.model.RoomSummary
     import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
    @@ -64,6 +65,7 @@ data class RoomDetailViewState(
             val highlightedEventId: String? = null,
             val unreadState: UnreadState = UnreadState.Unknown,
             val canShowJumpToReadMarker: Boolean = true,
    +        val changeMembershipState: ChangeMembershipState = ChangeMembershipState.Unknown,
             val canSendMessage: Boolean = true
     ) : MvRxState {
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomInvitationItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomInvitationItem.kt
    index 4e4e758aa2..7338a46d8a 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomInvitationItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomInvitationItem.kt
    @@ -19,9 +19,9 @@ package im.vector.riotx.features.home.room.list
     import android.view.ViewGroup
     import android.widget.ImageView
     import android.widget.TextView
    -import androidx.core.view.isVisible
     import com.airbnb.epoxy.EpoxyAttribute
     import com.airbnb.epoxy.EpoxyModelClass
    +import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
     import im.vector.matrix.android.api.util.MatrixItem
     import im.vector.riotx.R
     import im.vector.riotx.core.epoxy.VectorEpoxyHolder
    @@ -29,6 +29,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyModel
     import im.vector.riotx.core.extensions.setTextOrHide
     import im.vector.riotx.core.platform.ButtonStateView
     import im.vector.riotx.features.home.AvatarRenderer
    +import im.vector.riotx.features.invite.InviteButtonStateBinder
     
     @EpoxyModelClass(layout = R.layout.item_room_invitation)
     abstract class RoomInvitationItem : VectorEpoxyModel() {
    @@ -37,53 +38,36 @@ abstract class RoomInvitationItem : VectorEpoxyModel(
         @EpoxyAttribute lateinit var matrixItem: MatrixItem
         @EpoxyAttribute var secondLine: CharSequence? = null
         @EpoxyAttribute var listener: (() -> Unit)? = null
    -    @EpoxyAttribute var invitationAcceptInProgress: Boolean = false
    -    @EpoxyAttribute var invitationAcceptInError: Boolean = false
    -    @EpoxyAttribute var invitationRejectInProgress: Boolean = false
    -    @EpoxyAttribute var invitationRejectInError: Boolean = false
    +    @EpoxyAttribute lateinit var changeMembershipState: ChangeMembershipState
         @EpoxyAttribute var acceptListener: (() -> Unit)? = null
         @EpoxyAttribute var rejectListener: (() -> Unit)? = null
     
    +    private val acceptCallback = object : ButtonStateView.Callback {
    +        override fun onButtonClicked() {
    +            acceptListener?.invoke()
    +        }
    +
    +        override fun onRetryClicked() {
    +            acceptListener?.invoke()
    +        }
    +    }
    +
    +    private val rejectCallback = object : ButtonStateView.Callback {
    +        override fun onButtonClicked() {
    +            rejectListener?.invoke()
    +        }
    +
    +        override fun onRetryClicked() {
    +            rejectListener?.invoke()
    +        }
    +    }
    +
         override fun bind(holder: Holder) {
             super.bind(holder)
             holder.rootView.setOnClickListener { listener?.invoke() }
    -
    -        // When a request is in progress (accept or reject), we only use the accept State button
    -        val requestInProgress = invitationAcceptInProgress || invitationRejectInProgress
    -
    -        when {
    -            requestInProgress       -> holder.acceptView.render(ButtonStateView.State.Loading)
    -            invitationAcceptInError -> holder.acceptView.render(ButtonStateView.State.Error)
    -            else                    -> holder.acceptView.render(ButtonStateView.State.Button)
    -        }
    -        // ButtonStateView.State.Loaded not used because roomSummary will not be displayed as a room invitation anymore
    -
    -        holder.acceptView.callback = object : ButtonStateView.Callback {
    -            override fun onButtonClicked() {
    -                acceptListener?.invoke()
    -            }
    -
    -            override fun onRetryClicked() {
    -                acceptListener?.invoke()
    -            }
    -        }
    -
    -        holder.rejectView.isVisible = !requestInProgress
    -
    -        when {
    -            invitationRejectInError -> holder.rejectView.render(ButtonStateView.State.Error)
    -            else                    -> holder.rejectView.render(ButtonStateView.State.Button)
    -        }
    -
    -        holder.rejectView.callback = object : ButtonStateView.Callback {
    -            override fun onButtonClicked() {
    -                rejectListener?.invoke()
    -            }
    -
    -            override fun onRetryClicked() {
    -                rejectListener?.invoke()
    -            }
    -        }
    +        holder.acceptView.callback = acceptCallback
    +        holder.rejectView.callback = rejectCallback
    +        InviteButtonStateBinder.bind(holder.acceptView, holder.rejectView, changeMembershipState)
             holder.titleView.text = matrixItem.getBestName()
             holder.subtitleView.setTextOrHide(secondLine)
             avatarRenderer.render(matrixItem, holder.avatarImageView)
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt
    index a2de7c79a0..cfc76b61a8 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt
    @@ -21,10 +21,12 @@ import com.airbnb.mvrx.MvRxViewModelFactory
     import com.airbnb.mvrx.ViewModelContext
     import im.vector.matrix.android.api.MatrixCallback
     import im.vector.matrix.android.api.NoOpMatrixCallback
    +import im.vector.matrix.android.api.extensions.orFalse
     import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.room.model.Membership
     import im.vector.matrix.android.api.session.room.model.RoomSummary
     import im.vector.matrix.android.api.session.room.model.tag.RoomTag
    +import im.vector.matrix.rx.rx
     import im.vector.riotx.core.extensions.exhaustive
     import im.vector.riotx.core.platform.VectorViewModel
     import im.vector.riotx.core.utils.DataSource
    @@ -55,6 +57,7 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
     
         init {
             observeRoomSummaries()
    +        observeMembershipChanges()
         }
     
         override fun handle(action: RoomListAction) {
    @@ -102,37 +105,19 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
                     .observeOn(Schedulers.computation())
                     .map { buildRoomSummaries(it) }
                     .execute { async ->
    -                    val invitedRooms = async()?.get(RoomCategory.INVITE)?.map { it.roomId }.orEmpty()
    -                    val remainingJoining = joiningRoomsIds.intersect(invitedRooms)
    -                    val remainingJoinErrors = joiningErrorRoomsIds.intersect(invitedRooms)
    -                    val remainingRejecting = rejectingRoomsIds.intersect(invitedRooms)
    -                    val remainingRejectErrors = rejectingErrorRoomsIds.intersect(invitedRooms)
    -                    copy(
    -                            asyncFilteredRooms = async,
    -                            joiningRoomsIds = remainingJoining,
    -                            joiningErrorRoomsIds = remainingJoinErrors,
    -                            rejectingRoomsIds = remainingRejecting,
    -                            rejectingErrorRoomsIds = remainingRejectErrors
    -                    )
    +                    copy(asyncFilteredRooms = async)
                     }
         }
     
         private fun handleAcceptInvitation(action: RoomListAction.AcceptInvitation) = withState { state ->
             val roomId = action.roomSummary.roomId
    -
    -        if (state.joiningRoomsIds.contains(roomId) || state.rejectingRoomsIds.contains(roomId)) {
    +        val roomMembershipChange = state.roomMembershipChanges[roomId]
    +        if (roomMembershipChange?.isInProgress().orFalse()) {
                 // Request already sent, should not happen
                 Timber.w("Try to join an already joining room. Should not happen")
                 return@withState
             }
     
    -        setState {
    -            copy(
    -                    joiningRoomsIds = joiningRoomsIds + roomId,
    -                    rejectingErrorRoomsIds = rejectingErrorRoomsIds - roomId
    -            )
    -        }
    -
             session.getRoom(roomId)?.join(callback = object : MatrixCallback {
                 override fun onSuccess(data: Unit) {
                     // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
    @@ -142,32 +127,19 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
                 override fun onFailure(failure: Throwable) {
                     // Notify the user
                     _viewEvents.post(RoomListViewEvents.Failure(failure))
    -                setState {
    -                    copy(
    -                            joiningRoomsIds = joiningRoomsIds - roomId,
    -                            joiningErrorRoomsIds = joiningErrorRoomsIds + roomId
    -                    )
    -                }
                 }
             })
         }
     
         private fun handleRejectInvitation(action: RoomListAction.RejectInvitation) = withState { state ->
             val roomId = action.roomSummary.roomId
    -
    -        if (state.joiningRoomsIds.contains(roomId) || state.rejectingRoomsIds.contains(roomId)) {
    +        val roomMembershipChange = state.roomMembershipChanges[roomId]
    +        if (roomMembershipChange?.isInProgress().orFalse()) {
                 // Request already sent, should not happen
    -            Timber.w("Try to reject an already rejecting room. Should not happen")
    +            Timber.w("Try to left an already leaving or joining room. Should not happen")
                 return@withState
             }
     
    -        setState {
    -            copy(
    -                    rejectingRoomsIds = rejectingRoomsIds + roomId,
    -                    joiningErrorRoomsIds = joiningErrorRoomsIds - roomId
    -            )
    -        }
    -
             session.getRoom(roomId)?.leave(null, object : MatrixCallback {
                 override fun onSuccess(data: Unit) {
                     // We do not update the rejectingRoomsIds here, because, the room is not rejected yet regarding the sync data.
    @@ -179,12 +151,6 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
                 override fun onFailure(failure: Throwable) {
                     // Notify the user
                     _viewEvents.post(RoomListViewEvents.Failure(failure))
    -                setState {
    -                    copy(
    -                            rejectingRoomsIds = rejectingRoomsIds - roomId,
    -                            rejectingErrorRoomsIds = rejectingErrorRoomsIds + roomId
    -                    )
    -                }
                 }
             })
         }
    @@ -235,6 +201,16 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
             })
         }
     
    +    private fun observeMembershipChanges() {
    +        session.rx()
    +                .liveRoomChangeMembershipState()
    +                .subscribe {
    +                    Timber.v("ChangeMembership states: $it")
    +                    setState { copy(roomMembershipChanges = it) }
    +                }
    +                .disposeOnClear()
    +    }
    +
         private fun buildRoomSummaries(rooms: List): RoomSummaries {
             // Set up init size on directChats and groupRooms as they are the biggest ones
             val invites = ArrayList()
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt
    index b41b4b9eeb..63f0cf2a1a 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt
    @@ -20,6 +20,7 @@ import androidx.annotation.StringRes
     import com.airbnb.mvrx.Async
     import com.airbnb.mvrx.MvRxState
     import com.airbnb.mvrx.Uninitialized
    +import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
     import im.vector.matrix.android.api.session.room.model.Membership
     import im.vector.matrix.android.api.session.room.model.RoomSummary
     import im.vector.riotx.R
    @@ -30,14 +31,7 @@ data class RoomListViewState(
             val asyncRooms: Async> = Uninitialized,
             val roomFilter: String = "",
             val asyncFilteredRooms: Async = Uninitialized,
    -        // List of roomIds that the user wants to join
    -        val joiningRoomsIds: Set = emptySet(),
    -        // List of roomIds that the user wants to join, but an error occurred
    -        val joiningErrorRoomsIds: Set = emptySet(),
    -        // List of roomIds that the user wants to join
    -        val rejectingRoomsIds: Set = emptySet(),
    -        // List of roomIds that the user wants to reject, but an error occurred
    -        val rejectingErrorRoomsIds: Set = emptySet(),
    +        val roomMembershipChanges: Map = emptyMap(),
             val isInviteExpanded: Boolean = true,
             val isFavouriteRoomsExpanded: Boolean = true,
             val isDirectRoomsExpanded: Boolean = true,
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt
    index b06cb8a4bb..efa19d012b 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt
    @@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.list
     
     import androidx.annotation.StringRes
     import com.airbnb.epoxy.EpoxyController
    +import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
     import im.vector.matrix.android.api.session.room.model.Membership
     import im.vector.matrix.android.api.session.room.model.RoomSummary
     import im.vector.riotx.R
    @@ -72,10 +73,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
                     .filter { it.membership == Membership.JOIN && roomListNameFilter.test(it) }
     
             buildRoomModels(filteredSummaries,
    -                viewState.joiningRoomsIds,
    -                viewState.joiningErrorRoomsIds,
    -                viewState.rejectingRoomsIds,
    -                viewState.rejectingErrorRoomsIds,
    +                viewState.roomMembershipChanges,
                     emptySet())
     
             addFilterFooter(viewState)
    @@ -94,10 +92,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
                     }
                     if (isExpanded) {
                         buildRoomModels(summaries,
    -                            viewState.joiningRoomsIds,
    -                            viewState.joiningErrorRoomsIds,
    -                            viewState.rejectingRoomsIds,
    -                            viewState.rejectingErrorRoomsIds,
    +                            viewState.roomMembershipChanges,
                                 emptySet())
                         // Never set showHelp to true for invitation
                         if (category != RoomCategory.INVITE) {
    @@ -153,18 +148,12 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
         }
     
         private fun buildRoomModels(summaries: List,
    -                                joiningRoomsIds: Set,
    -                                joiningErrorRoomsIds: Set,
    -                                rejectingRoomsIds: Set,
    -                                rejectingErrorRoomsIds: Set,
    +                                roomChangedMembershipStates: Map,
                                     selectedRoomIds: Set) {
             summaries.forEach { roomSummary ->
                 roomSummaryItemFactory
                         .create(roomSummary,
    -                            joiningRoomsIds,
    -                            joiningErrorRoomsIds,
    -                            rejectingRoomsIds,
    -                            rejectingErrorRoomsIds,
    +                            roomChangedMembershipStates,
                                 selectedRoomIds,
                                 listener)
                         .addTo(this)
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt
    index 1830899d80..f33166504d 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt
    @@ -17,6 +17,7 @@
     package im.vector.riotx.features.home.room.list
     
     import android.view.View
    +import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
     import im.vector.matrix.android.api.session.room.model.Membership
     import im.vector.matrix.android.api.session.room.model.RoomSummary
     import im.vector.matrix.android.api.util.toMatrixItem
    @@ -39,23 +40,20 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
                                                      private val avatarRenderer: AvatarRenderer) {
     
         fun create(roomSummary: RoomSummary,
    -               joiningRoomsIds: Set,
    -               joiningErrorRoomsIds: Set,
    -               rejectingRoomsIds: Set,
    -               rejectingErrorRoomsIds: Set,
    +               roomChangeMembershipStates: Map,
                    selectedRoomIds: Set,
                    listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
             return when (roomSummary.membership) {
    -            Membership.INVITE -> createInvitationItem(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds, listener)
    +            Membership.INVITE -> {
    +                val changeMembershipState = roomChangeMembershipStates[roomSummary.roomId] ?: ChangeMembershipState.Unknown
    +                createInvitationItem(roomSummary, changeMembershipState, listener)
    +            }
                 else              -> createRoomItem(roomSummary, selectedRoomIds, listener?.let { it::onRoomClicked }, listener?.let { it::onRoomLongClicked })
             }
         }
     
    -    fun createInvitationItem(roomSummary: RoomSummary,
    -                             joiningRoomsIds: Set,
    -                             joiningErrorRoomsIds: Set,
    -                             rejectingRoomsIds: Set,
    -                             rejectingErrorRoomsIds: Set,
    +    private fun createInvitationItem(roomSummary: RoomSummary,
    +                             changeMembershipState: ChangeMembershipState,
                                  listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
             val secondLine = if (roomSummary.isDirect) {
                 roomSummary.inviterId
    @@ -70,10 +68,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
                     .avatarRenderer(avatarRenderer)
                     .matrixItem(roomSummary.toMatrixItem())
                     .secondLine(secondLine)
    -                .invitationAcceptInProgress(joiningRoomsIds.contains(roomSummary.roomId))
    -                .invitationAcceptInError(joiningErrorRoomsIds.contains(roomSummary.roomId))
    -                .invitationRejectInProgress(rejectingRoomsIds.contains(roomSummary.roomId))
    -                .invitationRejectInError(rejectingErrorRoomsIds.contains(roomSummary.roomId))
    +                .changeMembershipState(changeMembershipState)
                     .acceptListener { listener?.onAcceptRoomInvitation(roomSummary) }
                     .rejectListener { listener?.onRejectRoomInvitation(roomSummary) }
                     .listener { listener?.onRoomClicked(roomSummary) }
    diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteButtonStateBinder.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteButtonStateBinder.kt
    new file mode 100644
    index 0000000000..88abf28888
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteButtonStateBinder.kt
    @@ -0,0 +1,48 @@
    +/*
    + * Copyright (c) 2020 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.riotx.features.invite
    +
    +import androidx.core.view.isInvisible
    +import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
    +import im.vector.riotx.core.platform.ButtonStateView
    +
    +object InviteButtonStateBinder {
    +
    +    fun bind(
    +            acceptView: ButtonStateView,
    +            rejectView: ButtonStateView,
    +            changeMembershipState: ChangeMembershipState
    +    ) {
    +        // When a request is in progress (accept or reject), we only use the accept State button
    +        // We check for isSuccessful, otherwise we get a glitch the time room summaries get rebuilt
    +
    +        val requestInProgress = changeMembershipState.isInProgress() || changeMembershipState.isSuccessful()
    +        when {
    +            requestInProgress                                            -> acceptView.render(ButtonStateView.State.Loading)
    +            changeMembershipState is ChangeMembershipState.FailedJoining -> acceptView.render(ButtonStateView.State.Error)
    +            else                                                         -> acceptView.render(ButtonStateView.State.Button)
    +        }
    +        // ButtonStateView.State.Loaded not used because roomSummary will not be displayed as a room invitation anymore
    +
    +        rejectView.isInvisible = requestInProgress
    +
    +        when {
    +            changeMembershipState is ChangeMembershipState.FailedLeaving -> rejectView.render(ButtonStateView.State.Error)
    +            else                                                         -> rejectView.render(ButtonStateView.State.Button)
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/invite/VectorInviteView.kt b/vector/src/main/java/im/vector/riotx/features/invite/VectorInviteView.kt
    index b9bd9b0e1e..42f440fc30 100644
    --- a/vector/src/main/java/im/vector/riotx/features/invite/VectorInviteView.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/invite/VectorInviteView.kt
    @@ -21,10 +21,12 @@ import android.util.AttributeSet
     import android.view.View
     import androidx.constraintlayout.widget.ConstraintLayout
     import androidx.core.view.updateLayoutParams
    +import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
     import im.vector.matrix.android.api.session.user.model.User
     import im.vector.matrix.android.api.util.toMatrixItem
     import im.vector.riotx.R
     import im.vector.riotx.core.di.HasScreenInjector
    +import im.vector.riotx.core.platform.ButtonStateView
     import im.vector.riotx.features.home.AvatarRenderer
     import kotlinx.android.synthetic.main.vector_invite_view.view.*
     import javax.inject.Inject
    @@ -50,11 +52,28 @@ class VectorInviteView @JvmOverloads constructor(context: Context, attrs: Attrib
                 context.injector().inject(this)
             }
             View.inflate(context, R.layout.vector_invite_view, this)
    -        inviteRejectView.setOnClickListener { callback?.onRejectInvite() }
    -        inviteAcceptView.setOnClickListener { callback?.onAcceptInvite() }
    +        inviteAcceptView.callback = object : ButtonStateView.Callback {
    +            override fun onButtonClicked() {
    +                callback?.onAcceptInvite()
    +            }
    +
    +            override fun onRetryClicked() {
    +                callback?.onAcceptInvite()
    +            }
    +        }
    +
    +        inviteRejectView.callback = object : ButtonStateView.Callback {
    +            override fun onButtonClicked() {
    +                callback?.onRejectInvite()
    +            }
    +
    +            override fun onRetryClicked() {
    +                callback?.onRejectInvite()
    +            }
    +        }
         }
     
    -    fun render(sender: User, mode: Mode = Mode.LARGE) {
    +    fun render(sender: User, mode: Mode = Mode.LARGE, changeMembershipState: ChangeMembershipState) {
             if (mode == Mode.LARGE) {
                 updateLayoutParams { height = LayoutParams.MATCH_CONSTRAINT }
                 avatarRenderer.render(sender.toMatrixItem(), inviteAvatarView)
    @@ -68,5 +87,6 @@ class VectorInviteView @JvmOverloads constructor(context: Context, attrs: Attrib
                 inviteNameView.visibility = View.GONE
                 inviteLabelView.text = context.getString(R.string.invited_by, sender.userId)
             }
    +        InviteButtonStateBinder.bind(inviteAcceptView, inviteRejectView, changeMembershipState)
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
    index 0b89ab8ec4..7b3eedc71f 100644
    --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
    @@ -29,6 +29,7 @@ import androidx.core.view.ViewCompat
     import androidx.fragment.app.Fragment
     import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
     import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
    +import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
     import im.vector.matrix.android.api.session.terms.TermsService
     import im.vector.matrix.android.api.session.widgets.model.Widget
     import im.vector.matrix.android.api.util.MatrixItem
    @@ -159,8 +160,8 @@ class DefaultNavigator @Inject constructor(
             activity.finish()
         }
     
    -    override fun openRoomPreview(publicRoom: PublicRoom, context: Context) {
    -        val intent = RoomPreviewActivity.getIntent(context, publicRoom)
    +    override fun openRoomPreview(context: Context, publicRoom: PublicRoom, roomDirectoryData: RoomDirectoryData) {
    +        val intent = RoomPreviewActivity.getIntent(context, publicRoom, roomDirectoryData)
             context.startActivity(intent)
         }
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
    index ce4d5ef3ea..08fd63b93c 100644
    --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
    @@ -22,6 +22,7 @@ import android.view.View
     import androidx.core.util.Pair
     import androidx.fragment.app.Fragment
     import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
    +import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
     import im.vector.matrix.android.api.session.terms.TermsService
     import im.vector.matrix.android.api.util.MatrixItem
     import im.vector.matrix.android.api.session.widgets.model.Widget
    @@ -48,7 +49,7 @@ interface Navigator {
     
         fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String? = null, buildTask: Boolean = false)
     
    -    fun openRoomPreview(publicRoom: PublicRoom, context: Context)
    +    fun openRoomPreview(context: Context, publicRoom: PublicRoom, roomDirectoryData: RoomDirectoryData)
     
         fun openCreateRoom(context: Context, initialName: String = "")
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsController.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsController.kt
    index aaacb2a170..04ddccdd8a 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsController.kt
    @@ -21,6 +21,7 @@ import com.airbnb.epoxy.VisibilityState
     import com.airbnb.mvrx.Fail
     import com.airbnb.mvrx.Incomplete
     import com.airbnb.mvrx.Success
    +import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
     import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
     import im.vector.matrix.android.api.util.toMatrixItem
     import im.vector.riotx.R
    @@ -89,13 +90,14 @@ class PublicRoomsController @Inject constructor(private val stringProvider: Stri
                 roomTopic(publicRoom.topic)
                 nbOfMembers(publicRoom.numJoinedMembers)
     
    +            val roomChangeMembership = viewState.changeMembershipStates[publicRoom.roomId] ?: ChangeMembershipState.Unknown
    +            val isJoined = viewState.joinedRoomsIds.contains(publicRoom.roomId) || roomChangeMembership is ChangeMembershipState.Joined
                 val joinState = when {
    -                viewState.joinedRoomsIds.contains(publicRoom.roomId)       -> JoinState.JOINED
    -                viewState.joiningRoomsIds.contains(publicRoom.roomId)      -> JoinState.JOINING
    -                viewState.joiningErrorRoomsIds.contains(publicRoom.roomId) -> JoinState.JOINING_ERROR
    -                else                                                       -> JoinState.NOT_JOINED
    +                isJoined                                                    -> JoinState.JOINED
    +                roomChangeMembership is ChangeMembershipState.Joining       -> JoinState.JOINING
    +                roomChangeMembership is ChangeMembershipState.FailedJoining -> JoinState.JOINING_ERROR
    +                else                                                        -> JoinState.NOT_JOINED
                 }
    -
                 joinState(joinState)
     
                 joinListener {
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt
    index 869ee85337..dcccd33cf6 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt
    @@ -114,26 +114,22 @@ class PublicRoomsFragment @Inject constructor(
     
         override fun onPublicRoomClicked(publicRoom: PublicRoom, joinState: JoinState) {
             Timber.v("PublicRoomClicked: $publicRoom")
    -
    -        when (joinState) {
    -            JoinState.JOINED        -> {
    -                navigator.openRoom(requireActivity(), publicRoom.roomId)
    -            }
    -            JoinState.NOT_JOINED,
    -            JoinState.JOINING_ERROR -> {
    -                // ROOM PREVIEW
    -                navigator.openRoomPreview(publicRoom, requireActivity())
    -            }
    -            else                    -> {
    -                Snackbar.make(publicRoomsCoordinator, getString(R.string.please_wait), Snackbar.LENGTH_SHORT)
    -                        .show()
    +        withState(viewModel) { state ->
    +            when (joinState) {
    +                JoinState.JOINED -> {
    +                    navigator.openRoom(requireActivity(), publicRoom.roomId)
    +                }
    +                else             -> {
    +                    // ROOM PREVIEW
    +                    navigator.openRoomPreview(requireActivity(), publicRoom, state.roomDirectoryData)
    +                }
                 }
             }
         }
     
         override fun onPublicRoomJoin(publicRoom: PublicRoom) {
             Timber.v("PublicRoomJoinClicked: $publicRoom")
    -        viewModel.handle(RoomDirectoryAction.JoinRoom(publicRoom.getPrimaryAlias(), publicRoom.roomId))
    +        viewModel.handle(RoomDirectoryAction.JoinRoom(publicRoom.roomId))
         }
     
         override fun loadMore() {
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsViewState.kt
    index 665e37dcbd..67b17ea34e 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsViewState.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsViewState.kt
    @@ -19,7 +19,9 @@ package im.vector.riotx.features.roomdirectory
     import com.airbnb.mvrx.Async
     import com.airbnb.mvrx.MvRxState
     import com.airbnb.mvrx.Uninitialized
    +import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
     import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
    +import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
     
     data class PublicRoomsViewState(
             // The current filter
    @@ -30,11 +32,9 @@ data class PublicRoomsViewState(
             val asyncPublicRoomsRequest: Async> = Uninitialized,
             // True if more result are available server side
             val hasMore: Boolean = false,
    -        // Set of roomIds that the user wants to join
    -        val joiningRoomsIds: Set = emptySet(),
    -        // Set of roomIds that the user wants to join, but an error occurred
    -        val joiningErrorRoomsIds: Set = emptySet(),
             // Set of joined roomId,
             val joinedRoomsIds: Set = emptySet(),
    -        val roomDirectoryDisplayName: String? = null
    +        // keys are room alias or roomId
    +        val changeMembershipStates: Map = emptyMap(),
    +        val roomDirectoryData: RoomDirectoryData = RoomDirectoryData()
     ) : MvRxState
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryAction.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryAction.kt
    index 598f26fc3b..8b32726370 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryAction.kt
    @@ -23,5 +23,5 @@ sealed class RoomDirectoryAction : VectorViewModelAction {
         data class SetRoomDirectoryData(val roomDirectoryData: RoomDirectoryData) : RoomDirectoryAction()
         data class FilterWith(val filter: String) : RoomDirectoryAction()
         object LoadMore : RoomDirectoryAction()
    -    data class JoinRoom(val roomAlias: String?, val roomId: String) : RoomDirectoryAction()
    +    data class JoinRoom(val roomId: String) : RoomDirectoryAction()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt
    index 53661b075a..96de55a5b8 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt
    @@ -26,6 +26,7 @@ import com.airbnb.mvrx.appendAt
     import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.MatrixCallback
    +import im.vector.matrix.android.api.extensions.orFalse
     import im.vector.matrix.android.api.failure.Failure
     import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.room.model.Membership
    @@ -63,18 +64,10 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
     
         private var currentTask: Cancelable? = null
     
    -    // Default RoomDirectoryData
    -    private var roomDirectoryData = RoomDirectoryData()
    -
         init {
    -        setState {
    -            copy(
    -                    roomDirectoryDisplayName = roomDirectoryData.displayName
    -            )
    -        }
    -
             // Observe joined room (from the sync)
             observeJoinedRooms()
    +        observeMembershipChanges()
         }
     
         private fun observeJoinedRooms() {
    @@ -91,18 +84,21 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
                                 ?: emptySet()
     
                         setState {
    -                        copy(
    -                                joinedRoomsIds = joinedRoomIds,
    -                                // Remove (newly) joined room id from the joining room list
    -                                joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { removeAll(joinedRoomIds) },
    -                                // Remove (newly) joined room id from the joining room list in error
    -                                joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableSet().apply { removeAll(joinedRoomIds) }
    -                        )
    +                        copy(joinedRoomsIds = joinedRoomIds)
                         }
                     }
                     .disposeOnClear()
         }
     
    +    private fun observeMembershipChanges() {
    +        session.rx()
    +                .liveRoomChangeMembershipState()
    +                .subscribe {
    +                    setState { copy(changeMembershipStates = it) }
    +                }
    +                .disposeOnClear()
    +    }
    +
         override fun handle(action: RoomDirectoryAction) {
             when (action) {
                 is RoomDirectoryAction.SetRoomDirectoryData -> setRoomDirectoryData(action)
    @@ -112,15 +108,15 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
             }
         }
     
    -    private fun setRoomDirectoryData(action: RoomDirectoryAction.SetRoomDirectoryData) {
    -        if (this.roomDirectoryData == action.roomDirectoryData) {
    -            return
    +    private fun setRoomDirectoryData(action: RoomDirectoryAction.SetRoomDirectoryData) = withState {
    +        if (it.roomDirectoryData == action.roomDirectoryData) {
    +            return@withState
    +        }
    +        setState{
    +            copy(roomDirectoryData = action.roomDirectoryData)
             }
    -
    -        this.roomDirectoryData = action.roomDirectoryData
    -
             reset("")
    -        load("")
    +        load("", action.roomDirectoryData)
         }
     
         private fun filterWith(action: RoomDirectoryAction.FilterWith) = withState { state ->
    @@ -128,7 +124,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
                 currentTask?.cancel()
     
                 reset(action.filter)
    -            load(action.filter)
    +            load(action.filter, state.roomDirectoryData)
             }
         }
     
    @@ -141,7 +137,6 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
                         publicRooms = emptyList(),
                         asyncPublicRoomsRequest = Loading(),
                         hasMore = false,
    -                    roomDirectoryDisplayName = roomDirectoryData.displayName,
                         currentFilter = newFilter
                 )
             }
    @@ -154,12 +149,11 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
                             asyncPublicRoomsRequest = Loading()
                     )
                 }
    -
    -            load(state.currentFilter)
    +            load(state.currentFilter, state.roomDirectoryData)
             }
         }
     
    -    private fun load(filter: String) {
    +    private fun load(filter: String, roomDirectoryData: RoomDirectoryData) {
             currentTask = session.getPublicRooms(roomDirectoryData.homeServer,
                     PublicRoomsParams(
                             limit = PUBLIC_ROOMS_LIMIT,
    @@ -204,19 +198,16 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
         }
     
         private fun joinRoom(action: RoomDirectoryAction.JoinRoom) = withState { state ->
    -        if (state.joiningRoomsIds.contains(action.roomId)) {
    +        val roomMembershipChange = state.changeMembershipStates[action.roomId]
    +        if (roomMembershipChange?.isInProgress().orFalse()) {
                 // Request already sent, should not happen
                 Timber.w("Try to join an already joining room. Should not happen")
                 return@withState
             }
    -
    -        setState {
    -            copy(
    -                    joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { add(action.roomId) }
    -            )
    -        }
    -
    -        session.joinRoom(action.roomAlias ?: action.roomId, callback = object : MatrixCallback {
    +        val viaServers = state.roomDirectoryData.homeServer?.let {
    +            listOf(it)
    +        } ?: emptyList()
    +        session.joinRoom(action.roomId, viaServers = viaServers, callback = object : MatrixCallback {
                 override fun onSuccess(data: Unit) {
                     // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
                     // Instead, we wait for the room to be joined
    @@ -225,20 +216,12 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
                 override fun onFailure(failure: Throwable) {
                     // Notify the user
                     _viewEvents.post(RoomDirectoryViewEvents.Failure(failure))
    -
    -                setState {
    -                    copy(
    -                            joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { remove(action.roomId) },
    -                            joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableSet().apply { add(action.roomId) }
    -                    )
    -                }
                 }
             })
         }
     
         override fun onCleared() {
             super.onCleared()
    -
             currentTask?.cancel()
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewAction.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewAction.kt
    index 6b83ada90e..426078fa3d 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewAction.kt
    @@ -19,5 +19,5 @@ package im.vector.riotx.features.roomdirectory.roompreview
     import im.vector.riotx.core.platform.VectorViewModelAction
     
     sealed class RoomPreviewAction : VectorViewModelAction {
    -    data class Join(val roomAlias: String?) : RoomPreviewAction()
    +    object Join : RoomPreviewAction()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewActivity.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewActivity.kt
    index 3cb442127f..063cf3b8ff 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewActivity.kt
    @@ -21,6 +21,7 @@ import android.content.Intent
     import android.os.Parcelable
     import androidx.appcompat.widget.Toolbar
     import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
    +import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
     import im.vector.matrix.android.api.util.MatrixItem
     import im.vector.riotx.R
     import im.vector.riotx.core.extensions.addFragment
    @@ -35,7 +36,8 @@ data class RoomPreviewData(
             val roomAlias: String?,
             val topic: String?,
             val worldReadable: Boolean,
    -        val avatarUrl: String?
    +        val avatarUrl: String?,
    +        val homeServer: String?
     ) : Parcelable {
         val matrixItem: MatrixItem
             get() = MatrixItem.RoomItem(roomId, roomName ?: roomAlias, avatarUrl)
    @@ -46,7 +48,7 @@ class RoomPreviewActivity : VectorBaseActivity(), ToolbarConfigurable {
         companion object {
             private const val ARG = "ARG"
     
    -        fun getIntent(context: Context, publicRoom: PublicRoom): Intent {
    +        fun getIntent(context: Context, publicRoom: PublicRoom, roomDirectoryData: RoomDirectoryData): Intent {
                 return Intent(context, RoomPreviewActivity::class.java).apply {
                     putExtra(ARG, RoomPreviewData(
                             roomId = publicRoom.roomId,
    @@ -54,7 +56,8 @@ class RoomPreviewActivity : VectorBaseActivity(), ToolbarConfigurable {
                             roomAlias = publicRoom.getPrimaryAlias(),
                             topic = publicRoom.topic,
                             worldReadable = publicRoom.worldReadable,
    -                        avatarUrl = publicRoom.avatarUrl
    +                        avatarUrl = publicRoom.avatarUrl,
    +                        homeServer = roomDirectoryData.homeServer
                     ))
                 }
             }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt
    index 04ecdb2305..ee01e8f7fe 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt
    @@ -65,7 +65,7 @@ class RoomPreviewNoPreviewFragment @Inject constructor(
     
             roomPreviewNoPreviewJoin.callback = object : ButtonStateView.Callback {
                 override fun onButtonClicked() {
    -                roomPreviewViewModel.handle(RoomPreviewAction.Join(roomPreviewData.roomAlias))
    +                roomPreviewViewModel.handle(RoomPreviewAction.Join)
                 }
     
                 override fun onRetryClicked() {
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt
    index 3f8ae03029..c5e79832fc 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt
    @@ -22,7 +22,9 @@ import com.airbnb.mvrx.ViewModelContext
     import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.MatrixCallback
    +import im.vector.matrix.android.api.query.QueryStringValue
     import im.vector.matrix.android.api.session.Session
    +import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
     import im.vector.matrix.android.api.session.room.model.Membership
     import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
     import im.vector.matrix.rx.rx
    @@ -32,7 +34,7 @@ import im.vector.riotx.core.platform.VectorViewModel
     import im.vector.riotx.features.roomdirectory.JoinState
     import timber.log.Timber
     
    -class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: RoomPreviewViewState,
    +class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val initialState: RoomPreviewViewState,
                                                            private val session: Session)
         : VectorViewModel(initialState) {
     
    @@ -52,30 +54,41 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: R
     
         init {
             // Observe joined room (from the sync)
    -        observeJoinedRooms()
    +        observeRoomSummary()
    +        observeMembershipChanges()
         }
     
    -    private fun observeJoinedRooms() {
    +    private fun observeRoomSummary() {
             val queryParams = roomSummaryQueryParams {
    -            memberships = listOf(Membership.JOIN)
    +            roomId = QueryStringValue.Equals(initialState.roomId)
             }
             session
                     .rx()
                     .liveRoomSummaries(queryParams)
                     .subscribe { list ->
    -                    withState { state ->
    -                        val isRoomJoined = list
    -                                ?.map { it.roomId }
    -                                ?.toList()
    -                                ?.contains(state.roomId) == true
    +                    val isRoomJoined = list.any {
    +                        it.membership == Membership.JOIN
    +                    }
    +                    if (isRoomJoined) {
    +                        setState { copy(roomJoinState = JoinState.JOINED) }
    +                    }
    +                }
    +                .disposeOnClear()
    +    }
     
    -                        if (isRoomJoined) {
    -                            setState {
    -                                copy(
    -                                        roomJoinState = JoinState.JOINED
    -                                )
    -                            }
    -                        }
    +    private fun observeMembershipChanges() {
    +        session.rx()
    +                .liveRoomChangeMembershipState()
    +                .subscribe {
    +                    val changeMembership = it[initialState.roomId] ?: ChangeMembershipState.Unknown
    +                    val joinState = when (changeMembership) {
    +                        is ChangeMembershipState.Joining       -> JoinState.JOINING
    +                        is ChangeMembershipState.FailedJoining -> JoinState.JOINING_ERROR
    +                        // Other cases are handled by room summary
    +                        else                                   -> null
    +                    }
    +                    if (joinState != null) {
    +                        setState { copy(roomJoinState = joinState) }
                         }
                     }
                     .disposeOnClear()
    @@ -83,37 +96,27 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: R
     
         override fun handle(action: RoomPreviewAction) {
             when (action) {
    -            is RoomPreviewAction.Join -> handleJoinRoom(action)
    +            is RoomPreviewAction.Join -> handleJoinRoom()
             }.exhaustive
         }
     
    -    private fun handleJoinRoom(action: RoomPreviewAction.Join) = withState { state ->
    +    private fun handleJoinRoom() = withState { state ->
             if (state.roomJoinState == JoinState.JOINING) {
                 // Request already sent, should not happen
                 Timber.w("Try to join an already joining room. Should not happen")
                 return@withState
             }
    -
    -        setState {
    -            copy(
    -                    roomJoinState = JoinState.JOINING,
    -                    lastError = null
    -            )
    -        }
    -
    -        session.joinRoom(action.roomAlias ?: state.roomId, callback = object : MatrixCallback {
    +        val viaServers = state.homeServer?.let {
    +            listOf(it)
    +        } ?: emptyList()
    +        session.joinRoom(state.roomId, viaServers = viaServers, callback = object : MatrixCallback {
                 override fun onSuccess(data: Unit) {
                     // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
                     // Instead, we wait for the room to be joined
                 }
     
                 override fun onFailure(failure: Throwable) {
    -                setState {
    -                    copy(
    -                            roomJoinState = JoinState.JOINING_ERROR,
    -                            lastError = failure
    -                    )
    -                }
    +                setState { copy(lastError = failure) }
                 }
             })
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewState.kt
    index d3c75f95e0..04806ccf27 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewState.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewState.kt
    @@ -22,11 +22,21 @@ import im.vector.riotx.features.roomdirectory.JoinState
     data class RoomPreviewViewState(
             // The room id
             val roomId: String = "",
    +        val roomAlias: String? = null,
    +        /**
    +         * The server name (might be null)
    +         * Set null when the server is the current user's home server.
    +         */
    +        val homeServer: String? = null,
             // Current state of the room in preview
             val roomJoinState: JoinState = JoinState.NOT_JOINED,
             // Last error of join room request
             val lastError: Throwable? = null
     ) : MvRxState {
     
    -    constructor(args: RoomPreviewData) : this(roomId = args.roomId)
    +    constructor(args: RoomPreviewData) : this(
    +            roomId = args.roomId,
    +            roomAlias = args.roomAlias,
    +            homeServer = args.homeServer
    +    )
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt
    index aa414ec2a1..1ff5094517 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt
    @@ -112,7 +112,6 @@ class RoomProfileFragment @Inject constructor(
                 when (it) {
                     is RoomProfileViewEvents.Loading            -> showLoading(it.message)
                     is RoomProfileViewEvents.Failure            -> showFailure(it.throwable)
    -                is RoomProfileViewEvents.OnLeaveRoomSuccess -> onLeaveRoom()
                     is RoomProfileViewEvents.ShareRoomProfile   -> onShareRoomProfile(it.permalink)
                     RoomProfileViewEvents.OnChangeAvatarSuccess -> dismissLoadingDialog()
                 }.exhaustive
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewEvents.kt
    index 78df127f72..c0c1f2eb24 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewEvents.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewEvents.kt
    @@ -25,7 +25,6 @@ sealed class RoomProfileViewEvents : VectorViewEvents {
         data class Loading(val message: CharSequence? = null) : RoomProfileViewEvents()
         data class Failure(val throwable: Throwable) : RoomProfileViewEvents()
     
    -    object OnLeaveRoomSuccess : RoomProfileViewEvents()
         object OnChangeAvatarSuccess : RoomProfileViewEvents()
         data class ShareRoomProfile(val permalink: String) : RoomProfileViewEvents()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt
    index 925ab716a4..d673422d06 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt
    @@ -98,7 +98,7 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted private val ini
             _viewEvents.post(RoomProfileViewEvents.Loading(stringProvider.getString(R.string.room_profile_leaving_room)))
             room.leave(null, object : MatrixCallback {
                 override fun onSuccess(data: Unit) {
    -                _viewEvents.post(RoomProfileViewEvents.OnLeaveRoomSuccess)
    +                // Do nothing, we will be closing the room automatically when it will get back from sync
                 }
     
                 override fun onFailure(failure: Throwable) {
    diff --git a/vector/src/main/res/layout/vector_invite_view.xml b/vector/src/main/res/layout/vector_invite_view.xml
    index 5e557895c2..7356fcf64b 100644
    --- a/vector/src/main/res/layout/vector_invite_view.xml
    +++ b/vector/src/main/res/layout/vector_invite_view.xml
    @@ -57,34 +57,35 @@
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toBottomOf="@id/inviteIdentifierView" />
     
    -    
    -
    -    
     
    +    
    +
         
    
    From e07a584d668e967d382f9bf1cffc0b734aaba7c5 Mon Sep 17 00:00:00 2001
    From: ganfra 
    Date: Fri, 10 Jul 2020 20:09:30 +0200
    Subject: [PATCH 067/122] Revert fixing users as it's not the good catch
    
    ---
     .../internal/session/room/membership/RoomMemberEventHandler.kt  | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt
    index b225895532..d7d578b635 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt
    @@ -34,7 +34,7 @@ internal class RoomMemberEventHandler @Inject constructor() {
             val userId = event.stateKey ?: return false
             val roomMemberEntity = RoomMemberEntityFactory.create(roomId, userId, roomMember)
             realm.insertOrUpdate(roomMemberEntity)
    -        if (roomMember.membership.isActive() && event.unsignedData?.replacesState.isNullOrEmpty()) {
    +        if (roomMember.membership.isActive()) {
                 val userEntity = UserEntityFactory.create(userId, roomMember)
                 realm.insertOrUpdate(userEntity)
             }
    
    From 253582219c6e4e17def3f7beb08c94d5c42e9f66 Mon Sep 17 00:00:00 2001
    From: ganfra 
    Date: Fri, 10 Jul 2020 22:35:51 +0200
    Subject: [PATCH 068/122] Remove EllipsizingTextView as it provokes more issue
     than it solves
    
    ---
     .../core/platform/EllipsizingTextView.kt      | 421 ------------------
     ...onstraint_set_composer_layout_expanded.xml |   2 +-
     .../layout/fragment_create_direct_room.xml    |   2 +-
     ...ent_create_direct_room_directory_users.xml |   2 +-
     .../main/res/layout/fragment_create_room.xml  |   2 +-
     .../main/res/layout/fragment_home_detail.xml  |   2 +-
     .../main/res/layout/fragment_known_users.xml  |   2 +-
     .../res/layout/fragment_matrix_profile.xml    |   2 +-
     .../main/res/layout/fragment_room_detail.xml  |   4 +-
     .../fragment_room_preview_no_preview.xml      |   2 +-
     .../layout/fragment_room_setting_generic.xml  |   2 +-
     .../main/res/layout/fragment_room_uploads.xml |   2 +-
     .../res/layout/fragment_user_directory.xml    |   2 +-
     .../res/layout/item_autocomplete_emoji.xml    |   2 +-
     .../res/layout/item_bottom_sheet_action.xml   |   2 +-
     .../item_bottom_sheet_message_preview.xml     |   4 +-
     .../layout/item_bottom_sheet_room_preview.xml |   2 +-
     .../layout/item_create_direct_room_user.xml   |   4 +-
     vector/src/main/res/layout/item_device.xml    |   2 +-
     .../src/main/res/layout/item_form_switch.xml  |   2 +-
     vector/src/main/res/layout/item_group.xml     |   2 +-
     .../src/main/res/layout/item_known_user.xml   |   4 +-
     .../main/res/layout/item_profile_action.xml   |   4 +-
     .../res/layout/item_profile_matrix_item.xml   |   4 +-
     .../src/main/res/layout/item_public_room.xml  |   2 +-
     vector/src/main/res/layout/item_room.xml      |   6 +-
     .../main/res/layout/item_room_category.xml    |   2 +-
     .../main/res/layout/item_room_directory.xml   |   4 +-
     .../main/res/layout/item_room_invitation.xml  |   4 +-
     .../res/layout/item_timeline_event_base.xml   |   2 +-
     .../src/main/res/layout/item_uploads_file.xml |   4 +-
     vector/src/main/res/layout/item_user.xml      |   4 +-
     .../main/res/layout/merge_composer_layout.xml |   2 +-
     33 files changed, 44 insertions(+), 465 deletions(-)
     delete mode 100644 vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt
    deleted file mode 100644
    index f54776fc40..0000000000
    --- a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt
    +++ /dev/null
    @@ -1,421 +0,0 @@
    -/*
    - * Copyright (C) 2011 Micah Hainline
    - * Copyright (C) 2012 Triposo
    - * Copyright (C) 2013 Paul Imhoff
    - * Copyright (C) 2014 Shahin Yousefi
    - * Copyright 2020 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.riotx.core.platform
    -
    -import android.content.Context
    -import android.graphics.Canvas
    -import android.graphics.Color
    -import android.text.Layout
    -import android.text.Spannable
    -import android.text.SpannableString
    -import android.text.SpannableStringBuilder
    -import android.text.Spanned
    -import android.text.StaticLayout
    -import android.text.TextUtils.TruncateAt
    -import android.text.TextUtils.concat
    -import android.text.TextUtils.copySpansFrom
    -import android.text.TextUtils.indexOf
    -import android.text.TextUtils.lastIndexOf
    -import android.text.TextUtils.substring
    -import android.text.style.ForegroundColorSpan
    -import android.util.AttributeSet
    -import androidx.appcompat.widget.AppCompatTextView
    -import androidx.core.content.withStyledAttributes
    -import timber.log.Timber
    -import java.util.ArrayList
    -import java.util.regex.Pattern
    -
    -/*
    - * Imported from https://gist.github.com/hateum/d2095575b441007d62b8
    - *
    - * Use it in your layout to avoid this issue: https://issuetracker.google.com/issues/121092510
    - */
    -
    -/**
    - * A [android.widget.TextView] that ellipsizes more intelligently.
    - * This class supports ellipsizing multiline text through setting `android:ellipsize`
    - * and `android:maxLines`.
    - *
    - *
    - * Note: [TruncateAt.MARQUEE] ellipsizing type is not supported.
    - * This as to be used to get rid of the StaticLayout issue with maxLines and ellipsize causing some performance issues.
    - */
    -class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = android.R.attr.textViewStyle)
    -    : AppCompatTextView(context, attrs, defStyle) {
    -
    -    private val ELLIPSIS = SpannableString("\u2026")
    -    private val ellipsizeListeners: MutableList = ArrayList()
    -    private var ellipsizeStrategy: EllipsizeStrategy? = null
    -    var isEllipsized = false
    -        private set
    -    private var isStale = false
    -    private var programmaticChange = false
    -    private var fullText: CharSequence? = null
    -    private var maxLines = 0
    -    private var lineSpacingMult = 1.0f
    -    private var lineAddVertPad = 0.0f
    -
    -    /**
    -     * The end punctuation which will be removed when appending [.ELLIPSIS].
    -     */
    -    private var mEndPunctPattern: Pattern? = null
    -
    -    fun setEndPunctuationPattern(pattern: Pattern?) {
    -        mEndPunctPattern = pattern
    -    }
    -
    -    fun addEllipsizeListener(listener: EllipsizeListener) {
    -        ellipsizeListeners.add(listener)
    -    }
    -
    -    fun removeEllipsizeListener(listener: EllipsizeListener) {
    -        ellipsizeListeners.remove(listener)
    -    }
    -
    -    /**
    -     * @return The maximum number of lines displayed in this [android.widget.TextView].
    -     */
    -    override fun getMaxLines(): Int {
    -        return maxLines
    -    }
    -
    -    override fun setMaxLines(maxLines: Int) {
    -        super.setMaxLines(maxLines)
    -        this.maxLines = maxLines
    -        isStale = true
    -    }
    -
    -    /**
    -     * Determines if the last fully visible line is being ellipsized.
    -     *
    -     * @return `true` if the last fully visible line is being ellipsized;
    -     * otherwise, returns `false`.
    -     */
    -    fun ellipsizingLastFullyVisibleLine(): Boolean {
    -        return maxLines == Int.MAX_VALUE
    -    }
    -
    -    override fun setLineSpacing(add: Float, mult: Float) {
    -        lineAddVertPad = add
    -        lineSpacingMult = mult
    -        super.setLineSpacing(add, mult)
    -    }
    -
    -    override fun setText(text: CharSequence?, type: BufferType) {
    -        if (!programmaticChange) {
    -            fullText = if (text is Spanned) text else text
    -            isStale = true
    -        }
    -        super.setText(text, type)
    -    }
    -
    -    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    -        super.onSizeChanged(w, h, oldw, oldh)
    -        if (ellipsizingLastFullyVisibleLine()) {
    -            isStale = true
    -        }
    -    }
    -
    -    override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
    -        super.setPadding(left, top, right, bottom)
    -        if (ellipsizingLastFullyVisibleLine()) {
    -            isStale = true
    -        }
    -    }
    -
    -    override fun onDraw(canvas: Canvas) {
    -        if (isStale) {
    -            resetText()
    -        }
    -        super.onDraw(canvas)
    -    }
    -
    -    /**
    -     * Sets the ellipsized text if appropriate.
    -     */
    -    private fun resetText() {
    -        val maxLines = maxLines
    -        var workingText = fullText
    -        var ellipsized = false
    -        if (maxLines != -1) {
    -            if (ellipsizeStrategy == null) setEllipsize(null)
    -            workingText = ellipsizeStrategy!!.processText(fullText)
    -            ellipsized = !ellipsizeStrategy!!.isInLayout(fullText)
    -        }
    -        if (workingText != text) {
    -            programmaticChange = true
    -            text = try {
    -                workingText
    -            } finally {
    -                programmaticChange = false
    -            }
    -        }
    -        isStale = false
    -        if (ellipsized != isEllipsized) {
    -            isEllipsized = ellipsized
    -            for (listener in ellipsizeListeners) {
    -                listener.ellipsizeStateChanged(ellipsized)
    -            }
    -        }
    -    }
    -
    -    /**
    -     * Causes words in the text that are longer than the view is wide to be ellipsized
    -     * instead of broken in the middle. Use `null` to turn off ellipsizing.
    -     *
    -     *
    -     * Note: Method does nothing for [TruncateAt.MARQUEE]
    -     * ellipsizing type.
    -     *
    -     * @param where part of text to ellipsize
    -     */
    -    override fun setEllipsize(where: TruncateAt?) {
    -        if (where == null) {
    -            ellipsizeStrategy = EllipsizeNoneStrategy()
    -            return
    -        }
    -        ellipsizeStrategy = when (where) {
    -            TruncateAt.END     -> EllipsizeEndStrategy()
    -            TruncateAt.START   -> EllipsizeStartStrategy()
    -            TruncateAt.MIDDLE  -> EllipsizeMiddleStrategy()
    -            TruncateAt.MARQUEE -> EllipsizeNoneStrategy()
    -            else               -> EllipsizeNoneStrategy()
    -        }
    -    }
    -
    -    /**
    -     * A listener that notifies when the ellipsize state has changed.
    -     */
    -    interface EllipsizeListener {
    -        fun ellipsizeStateChanged(ellipsized: Boolean)
    -    }
    -
    -    /**
    -     * A base class for an ellipsize strategy.
    -     */
    -    private abstract inner class EllipsizeStrategy {
    -        /**
    -         * Returns ellipsized text if the text does not fit inside of the layout;
    -         * otherwise, returns the full text.
    -         *
    -         * @param text text to process
    -         * @return Ellipsized text if the text does not fit inside of the layout;
    -         * otherwise, returns the full text.
    -         */
    -        fun processText(text: CharSequence?): CharSequence? {
    -            return if (!isInLayout(text)) createEllipsizedText(text) else text
    -        }
    -
    -        /**
    -         * Determines if the text fits inside of the layout.
    -         *
    -         * @param text text to fit
    -         * @return `true` if the text fits inside of the layout;
    -         * otherwise, returns `false`.
    -         */
    -        fun isInLayout(text: CharSequence?): Boolean {
    -            val layout = createWorkingLayout(text)
    -            return layout.lineCount <= linesCount
    -        }
    -
    -        /**
    -         * Creates a working layout with the given text.
    -         *
    -         * @param workingText text to create layout with
    -         * @return [android.text.Layout] with the given text.
    -         */
    -        @Suppress("DEPRECATION")
    -        protected fun createWorkingLayout(workingText: CharSequence?): Layout {
    -            return StaticLayout(
    -                    workingText ?: "",
    -                    paint,
    -                    width - compoundPaddingLeft - compoundPaddingRight,
    -                    Layout.Alignment.ALIGN_NORMAL,
    -                    lineSpacingMult,
    -                    lineAddVertPad,
    -                    false
    -            )
    -        }
    -
    -        /**
    -         * Get how many lines of text we are allowed to display.
    -         */
    -        protected val linesCount: Int
    -            get() = if (ellipsizingLastFullyVisibleLine()) {
    -                val fullyVisibleLinesCount = fullyVisibleLinesCount
    -                if (fullyVisibleLinesCount == -1) 1 else fullyVisibleLinesCount
    -            } else {
    -                maxLines
    -            }
    -
    -        /**
    -         * Get how many lines of text we can display so their full height is visible.
    -         */
    -        protected val fullyVisibleLinesCount: Int
    -            get() {
    -                val layout = createWorkingLayout("")
    -                val height = height - compoundPaddingTop - compoundPaddingBottom
    -                val lineHeight = layout.getLineBottom(0)
    -                return height / lineHeight
    -            }
    -
    -        /**
    -         * Creates ellipsized text from the given text.
    -         *
    -         * @param fullText text to ellipsize
    -         * @return Ellipsized text
    -         */
    -        protected abstract fun createEllipsizedText(fullText: CharSequence?): CharSequence?
    -    }
    -
    -    /**
    -     * An [EllipsizingTextView.EllipsizeStrategy] that
    -     * does not ellipsize text.
    -     */
    -    private inner class EllipsizeNoneStrategy : EllipsizeStrategy() {
    -        override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
    -            return fullText
    -        }
    -    }
    -
    -    /**
    -     * An [EllipsizingTextView.EllipsizeStrategy] that
    -     * ellipsizes text at the end.
    -     */
    -    private inner class EllipsizeEndStrategy : EllipsizeStrategy() {
    -        override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
    -            val layout = createWorkingLayout(fullText)
    -            val cutOffIndex = try {
    -                layout.getLineEnd(maxLines - 1)
    -            } catch (exception: IndexOutOfBoundsException) {
    -                // Not sure to understand why this is happening
    -                Timber.e(exception, "IndexOutOfBoundsException, maxLine: $maxLines")
    -                0
    -            }
    -            val textLength = fullText!!.length
    -            var cutOffLength = textLength - cutOffIndex
    -            if (cutOffLength < ELLIPSIS.length) cutOffLength = ELLIPSIS.length
    -            var workingText: CharSequence = substring(fullText, 0, textLength - cutOffLength).trim()
    -            while (!isInLayout(concat(stripEndPunctuation(workingText), ELLIPSIS))) {
    -                val lastSpace = lastIndexOf(workingText, ' ')
    -                if (lastSpace == -1) {
    -                    break
    -                }
    -                workingText = substring(workingText, 0, lastSpace).trim()
    -            }
    -            workingText = concat(stripEndPunctuation(workingText), ELLIPSIS)
    -            val dest = SpannableStringBuilder(workingText)
    -            if (fullText is Spanned) {
    -                copySpansFrom(fullText as Spanned?, 0, workingText.length, null, dest, 0)
    -            }
    -            return dest
    -        }
    -
    -        /**
    -         * Strips the end punctuation from a given text according to [.mEndPunctPattern].
    -         *
    -         * @param workingText text to strip end punctuation from
    -         * @return Text without end punctuation.
    -         */
    -        fun stripEndPunctuation(workingText: CharSequence): String {
    -            return mEndPunctPattern!!.matcher(workingText).replaceFirst("")
    -        }
    -    }
    -
    -    /**
    -     * An [EllipsizingTextView.EllipsizeStrategy] that
    -     * ellipsizes text at the start.
    -     */
    -    private inner class EllipsizeStartStrategy : EllipsizeStrategy() {
    -        override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
    -            val layout = createWorkingLayout(fullText)
    -            val cutOffIndex = layout.getLineEnd(maxLines - 1)
    -            val textLength = fullText!!.length
    -            var cutOffLength = textLength - cutOffIndex
    -            if (cutOffLength < ELLIPSIS.length) cutOffLength = ELLIPSIS.length
    -            var workingText: CharSequence = substring(fullText, cutOffLength, textLength).trim()
    -            while (!isInLayout(concat(ELLIPSIS, workingText))) {
    -                val firstSpace = indexOf(workingText, ' ')
    -                if (firstSpace == -1) {
    -                    break
    -                }
    -                workingText = substring(workingText, firstSpace, workingText.length).trim()
    -            }
    -            workingText = concat(ELLIPSIS, workingText)
    -            val dest = SpannableStringBuilder(workingText)
    -            if (fullText is Spanned) {
    -                copySpansFrom(fullText as Spanned?, textLength - workingText.length,
    -                        textLength, null, dest, 0)
    -            }
    -            return dest
    -        }
    -    }
    -
    -    /**
    -     * An [EllipsizingTextView.EllipsizeStrategy] that
    -     * ellipsizes text in the middle.
    -     */
    -    private inner class EllipsizeMiddleStrategy : EllipsizeStrategy() {
    -        override fun createEllipsizedText(fullText: CharSequence?): CharSequence? {
    -            val layout = createWorkingLayout(fullText)
    -            val cutOffIndex = layout.getLineEnd(maxLines - 1)
    -            val textLength = fullText!!.length
    -            var cutOffLength = textLength - cutOffIndex
    -            if (cutOffLength < ELLIPSIS.length) cutOffLength = ELLIPSIS.length
    -            cutOffLength += cutOffIndex % 2 // Make it even.
    -            var firstPart = substring(
    -                    fullText, 0, textLength / 2 - cutOffLength / 2).trim()
    -            var secondPart = substring(
    -                    fullText, textLength / 2 + cutOffLength / 2, textLength).trim()
    -            while (!isInLayout(concat(firstPart, ELLIPSIS, secondPart))) {
    -                val lastSpaceFirstPart = firstPart.lastIndexOf(' ')
    -                val firstSpaceSecondPart = secondPart.indexOf(' ')
    -                if (lastSpaceFirstPart == -1 || firstSpaceSecondPart == -1) break
    -                firstPart = firstPart.substring(0, lastSpaceFirstPart).trim()
    -                secondPart = secondPart.substring(firstSpaceSecondPart, secondPart.length).trim()
    -            }
    -            val firstDest = SpannableStringBuilder(firstPart)
    -            val secondDest = SpannableStringBuilder(secondPart)
    -            if (fullText is Spanned) {
    -                copySpansFrom(fullText as Spanned?, 0, firstPart.length,
    -                        null, firstDest, 0)
    -                copySpansFrom(fullText as Spanned?, textLength - secondPart.length,
    -                        textLength, null, secondDest, 0)
    -            }
    -            return concat(firstDest, ELLIPSIS, secondDest)
    -        }
    -    }
    -
    -    companion object {
    -        const val ELLIPSIZE_ALPHA = 0x88
    -        private val DEFAULT_END_PUNCTUATION = Pattern.compile("[.!?,;:\u2026]*$", Pattern.DOTALL)
    -    }
    -
    -    init {
    -        context.withStyledAttributes(attrs, intArrayOf(android.R.attr.maxLines, android.R.attr.ellipsize), defStyle) {
    -            maxLines = getInt(0, Int.MAX_VALUE)
    -        }
    -        setEndPunctuationPattern(DEFAULT_END_PUNCTUATION)
    -        val currentTextColor = currentTextColor
    -        val ellipsizeColor = Color.argb(ELLIPSIZE_ALPHA, Color.red(currentTextColor), Color.green(currentTextColor), Color.blue(currentTextColor))
    -        ELLIPSIS.setSpan(ForegroundColorSpan(ellipsizeColor), 0, ELLIPSIS.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
    -    }
    -}
    diff --git a/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml b/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml
    index 17b350542a..198f4ca83b 100644
    --- a/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml
    +++ b/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml
    @@ -60,7 +60,7 @@
             app:layout_constraintTop_toTopOf="parent"
             tools:text="@tools:sample/first_names" />
     
    -    
     
    -                
     
    -                
     
    -                
     
    -            
     
    -                
     
    -                    
     
    -            
     
    -            
     
    -                
     
    -            
     
    -                
     
    -                
     
    -        
     
    -    
     
    -    
     
    -    
     
    -    
         
     
    -    
     
    -    
     
    -        
     
    -    
     
    -    
         
     
    -    
     
    -    
     
    -    
     
    -    
     
    -    
     
    -    
     
    -    
     
    -    
     
    -    
     
    -    
     
    -    
     
    -    
     
    -    
     
    -    
     
    -    
     
    -    
     
    -    
     
    -    
     
    -    
     
    -    
     
    -    
    Date: Sat, 11 Jul 2020 10:48:45 +0200
    Subject: [PATCH 069/122] KeybackupBanner: remove unnecessary animation
    
    ---
     .../main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt | 2 --
     1 file changed, 2 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt
    index d0cea6194b..d457099087 100755
    --- a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt
    @@ -80,8 +80,6 @@ class KeysBackupBanner @JvmOverloads constructor(
             state = newState
     
             hideAll()
    -        val parent = parent as ViewGroup
    -        TransitionManager.beginDelayedTransition(parent)
             when (newState) {
                 State.Initial    -> renderInitial()
                 State.Hidden     -> renderHidden()
    
    From 4387fd332704fe1a6f105462dcb96820db872f91 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Sat, 11 Jul 2020 12:25:31 +0200
    Subject: [PATCH 070/122] We do not need write storage permission to create a
     Txt file with the intent Intent.ACTION_CREATE_DOCUMENT
    
    ---
     CHANGES.md                                    |  2 +-
     .../vector/riotx/core/extensions/Fragment.kt  | 47 ++++++-------
     .../core/utils/ExternalApplicationsUtil.kt    | 27 ++++++++
     .../riotx/core/utils/PermissionsTools.kt      |  1 -
     .../setup/KeysBackupSetupActivity.kt          | 40 +----------
     .../setup/KeysBackupSetupSharedViewModel.kt   |  3 +
     .../setup/KeysBackupSetupStep3Fragment.kt     | 67 ++++++++++---------
     .../VectorSettingsSecurityPrivacyFragment.kt  | 10 ---
     vector/src/main/res/values/strings.xml        |  2 +
     9 files changed, 94 insertions(+), 105 deletions(-)
    
    diff --git a/CHANGES.md b/CHANGES.md
    index 60d66014ef..81a0e11525 100644
    --- a/CHANGES.md
    +++ b/CHANGES.md
    @@ -25,7 +25,7 @@ Build 🧱:
      - Revert to build-tools 3.5.3
     
     Other changes:
    - -
    + - Use Intent.ACTION_CREATE_DOCUMENT to save megolm key or recovery key in a txt file
     
     Changes in Riot.imX 0.91.4 (2020-07-06)
     ===================================================
    diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt
    index 7c1cae3644..7c66cb61dd 100644
    --- a/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt
    @@ -16,13 +16,12 @@
     
     package im.vector.riotx.core.extensions
     
    -import android.content.ActivityNotFoundException
    -import android.content.Intent
    +import android.app.Activity
     import android.os.Parcelable
     import androidx.fragment.app.Fragment
     import im.vector.riotx.R
     import im.vector.riotx.core.platform.VectorBaseFragment
    -import im.vector.riotx.core.utils.toast
    +import im.vector.riotx.core.utils.selectTxtFileToWrite
     import java.text.SimpleDateFormat
     import java.util.Date
     import java.util.Locale
    @@ -98,27 +97,25 @@ fun Fragment.getAllChildFragments(): List {
     const val POP_BACK_STACK_EXCLUSIVE = 0
     
     fun Fragment.queryExportKeys(userId: String, requestCode: Int) {
    -    // We need WRITE_EXTERNAL permission
    -//    if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES,
    -//                    this,
    -//                    PERMISSION_REQUEST_CODE_EXPORT_KEYS,
    -//                    R.string.permissions_rationale_msg_keys_backup_export)) {
    -    // WRITE permissions are not needed
    -    val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).let {
    -        it.format(Date())
    -    }
    -    val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
    -    intent.addCategory(Intent.CATEGORY_OPENABLE)
    -    intent.type = "text/plain"
    -    intent.putExtra(
    -            Intent.EXTRA_TITLE,
    -            "riot-megolm-export-$userId-$timestamp.txt"
    -    )
    +    val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
     
    -    try {
    -        startActivityForResult(Intent.createChooser(intent, getString(R.string.keys_backup_setup_step1_manual_export)), requestCode)
    -    } catch (activityNotFoundException: ActivityNotFoundException) {
    -        activity?.toast(R.string.error_no_external_application_found)
    -    }
    -//    }
    +    selectTxtFileToWrite(
    +            activity = requireActivity(),
    +            fragment = this,
    +            defaultFileName = "riot-megolm-export-$userId-$timestamp.txt",
    +            chooserHint = getString(R.string.keys_backup_setup_step1_manual_export),
    +            requestCode = requestCode
    +    )
    +}
    +
    +fun Activity.queryExportKeys(userId: String, requestCode: Int) {
    +    val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
    +
    +    selectTxtFileToWrite(
    +            activity = this,
    +            fragment = null,
    +            defaultFileName = "riot-megolm-export-$userId-$timestamp.txt",
    +            chooserHint = getString(R.string.keys_backup_setup_step1_manual_export),
    +            requestCode = requestCode
    +    )
     }
    diff --git a/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt
    index 2520f44f50..9c2d12514a 100644
    --- a/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt
    @@ -424,6 +424,33 @@ fun openPlayStore(activity: Activity, appId: String = BuildConfig.APPLICATION_ID
         }
     }
     
    +/**
    + * Ask the user to select a location and a file name to write in
    + */
    +fun selectTxtFileToWrite(
    +        activity: Activity,
    +        fragment: Fragment?,
    +        defaultFileName: String,
    +        chooserHint: String,
    +        requestCode: Int
    +) {
    +    val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
    +    intent.addCategory(Intent.CATEGORY_OPENABLE)
    +    intent.type = "text/plain"
    +    intent.putExtra(Intent.EXTRA_TITLE, defaultFileName)
    +
    +    try {
    +        val chooserIntent = Intent.createChooser(intent, chooserHint)
    +        if (fragment != null) {
    +            fragment.startActivityForResult(chooserIntent, requestCode)
    +        } else {
    +            activity.startActivityForResult(chooserIntent, requestCode)
    +        }
    +    } catch (activityNotFoundException: ActivityNotFoundException) {
    +        activity.toast(R.string.error_no_external_application_found)
    +    }
    +}
    +
     // ==============================================================================================================
     // Media utils
     // ==============================================================================================================
    diff --git a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt
    index 4790b26ad0..360a5efccc 100644
    --- a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt
    @@ -63,7 +63,6 @@ const val PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA = 569
     const val PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA = 570
     const val PERMISSION_REQUEST_CODE_AUDIO_CALL = 571
     const val PERMISSION_REQUEST_CODE_VIDEO_CALL = 572
    -const val PERMISSION_REQUEST_CODE_EXPORT_KEYS = 573
     const val PERMISSION_REQUEST_CODE_CHANGE_AVATAR = 574
     const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575
     const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt
    index b99c0e4330..f42fee0030 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt
    @@ -16,7 +16,6 @@
     package im.vector.riotx.features.crypto.keysbackup.setup
     
     import android.app.Activity
    -import android.content.ActivityNotFoundException
     import android.content.Context
     import android.content.Intent
     import androidx.appcompat.app.AlertDialog
    @@ -27,12 +26,9 @@ import im.vector.matrix.android.api.MatrixCallback
     import im.vector.riotx.R
     import im.vector.riotx.core.dialogs.ExportKeysDialog
     import im.vector.riotx.core.extensions.observeEvent
    +import im.vector.riotx.core.extensions.queryExportKeys
     import im.vector.riotx.core.extensions.replaceFragment
     import im.vector.riotx.core.platform.SimpleFragmentActivity
    -import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
    -import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_EXPORT_KEYS
    -import im.vector.riotx.core.utils.allGranted
    -import im.vector.riotx.core.utils.checkPermissions
     import im.vector.riotx.core.utils.toast
     import im.vector.riotx.features.crypto.keys.KeysExporter
     
    @@ -97,7 +93,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
                                 .show()
                     }
                     KeysBackupSetupSharedViewModel.NAVIGATE_MANUAL_EXPORT  -> {
    -                    exportKeysManually()
    +                    queryExportKeys(session.myUserId, REQUEST_CODE_SAVE_MEGOLM_EXPORT)
                     }
                 }
             }
    @@ -129,38 +125,6 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
             })
         }
     
    -    private fun exportKeysManually() {
    -        if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES,
    -                        this,
    -                        PERMISSION_REQUEST_CODE_EXPORT_KEYS,
    -                        R.string.permissions_rationale_msg_keys_backup_export)) {
    -            try {
    -                val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
    -                intent.addCategory(Intent.CATEGORY_OPENABLE)
    -                intent.type = "text/plain"
    -                intent.putExtra(Intent.EXTRA_TITLE, "riot-megolm-export-${session.myUserId}-${System.currentTimeMillis()}.txt")
    -
    -                startActivityForResult(
    -                        Intent.createChooser(
    -                                intent,
    -                                getString(R.string.keys_backup_setup_step1_manual_export)
    -                        ),
    -                        REQUEST_CODE_SAVE_MEGOLM_EXPORT
    -                )
    -            } catch (activityNotFoundException: ActivityNotFoundException) {
    -                toast(R.string.error_no_external_application_found)
    -            }
    -        }
    -    }
    -
    -    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
    -        if (allGranted(grantResults)) {
    -            if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) {
    -                exportKeysManually()
    -            }
    -        }
    -    }
    -
         override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
             if (requestCode == REQUEST_CODE_SAVE_MEGOLM_EXPORT) {
                 val uri = data?.data
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt
    index d9a90eb457..6381786e57 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt
    @@ -48,6 +48,9 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
     
         lateinit var session: Session
     
    +    val userId: String
    +        get() = session.myUserId
    +
         var showManualExport: MutableLiveData = MutableLiveData()
     
         var navigateEvent: MutableLiveData> = MutableLiveData()
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt
    index 21a25f1684..de9c0add30 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt
    @@ -15,8 +15,10 @@
      */
     package im.vector.riotx.features.crypto.keysbackup.setup
     
    +import android.app.Activity
    +import android.content.Intent
    +import android.net.Uri
     import android.os.Bundle
    -import android.os.Environment
     import android.view.View
     import android.widget.Button
     import android.widget.TextView
    @@ -29,25 +31,27 @@ import butterknife.BindView
     import butterknife.OnClick
     import com.google.android.material.bottomsheet.BottomSheetDialog
     import im.vector.riotx.R
    -import im.vector.riotx.core.files.addEntryToDownloadManager
    -import im.vector.riotx.core.files.writeToFile
     import im.vector.riotx.core.platform.VectorBaseFragment
     import im.vector.riotx.core.utils.LiveEvent
    -import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
    -import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_EXPORT_KEYS
    -import im.vector.riotx.core.utils.allGranted
    -import im.vector.riotx.core.utils.checkPermissions
     import im.vector.riotx.core.utils.copyToClipboard
    +import im.vector.riotx.core.utils.selectTxtFileToWrite
     import im.vector.riotx.core.utils.startSharePlainTextIntent
     import kotlinx.coroutines.Dispatchers
     import kotlinx.coroutines.GlobalScope
     import kotlinx.coroutines.launch
     import kotlinx.coroutines.withContext
    -import java.io.File
    +import java.io.IOException
    +import java.text.SimpleDateFormat
    +import java.util.Date
    +import java.util.Locale
     import javax.inject.Inject
     
     class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment() {
     
    +    companion object {
    +        private const val SAVE_RECOVERY_KEY_REQUEST_CODE = 2754
    +    }
    +
         override fun getLayoutResId() = R.layout.fragment_keys_backup_setup_step3
     
         @BindView(R.id.keys_backup_setup_step3_button)
    @@ -130,15 +134,15 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment()
             }
     
             dialog.findViewById(R.id.keys_backup_setup_save)?.setOnClickListener {
    -            val permissionsChecked = checkPermissions(
    -                    PERMISSIONS_FOR_WRITING_FILES,
    -                    this,
    -                    PERMISSION_REQUEST_CODE_EXPORT_KEYS,
    -                    R.string.permissions_rationale_msg_keys_backup_export
    +            val userId = viewModel.userId
    +            val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
    +            selectTxtFileToWrite(
    +                    activity = requireActivity(),
    +                    fragment = this,
    +                    defaultFileName = "recovery-key-$userId-$timestamp.txt",
    +                    chooserHint = getString(R.string.save_recovery_key_chooser_hint),
    +                    requestCode = SAVE_RECOVERY_KEY_REQUEST_CODE
                 )
    -            if (permissionsChecked) {
    -                exportRecoveryKeyToFile(recoveryKey)
    -            }
                 dialog.dismiss()
             }
     
    @@ -163,19 +167,19 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment()
             }
         }
     
    -    private fun exportRecoveryKeyToFile(data: String) {
    +    private fun exportRecoveryKeyToFile(uri: Uri, data: String) {
             GlobalScope.launch(Dispatchers.Main) {
                 Try {
                     withContext(Dispatchers.IO) {
    -                    val parentDir = context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
    -                    val file = File(parentDir, "recovery-key-" + System.currentTimeMillis() + ".txt")
    -
    -                    writeToFile(data, file)
    -
    -                    addEntryToDownloadManager(requireContext(), file, "text/plain")
    -
    -                    file.absolutePath
    +                    requireContext().contentResolver.openOutputStream(uri)
    +                            ?.use { os ->
    +                                os.write(data.toByteArray())
    +                                os.flush()
    +                            }
    +                }?.let {
    +                    uri.toString()
                     }
    +                        ?: throw IOException()
                 }
                         .fold(
                                 { throwable ->
    @@ -200,11 +204,14 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment()
             }
         }
     
    -    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
    -        if (allGranted(grantResults)) {
    -            if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) {
    -                viewModel.recoveryKey.value?.let {
    -                    exportRecoveryKeyToFile(it)
    +    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    +        when (requestCode) {
    +            SAVE_RECOVERY_KEY_REQUEST_CODE -> {
    +                val uri = data?.data
    +                if (resultCode == Activity.RESULT_OK && uri != null) {
    +                    viewModel.recoveryKey.value?.let {
    +                        exportRecoveryKeyToFile(uri, it)
    +                    }
                     }
                 }
             }
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    index 3c2acb1693..73c167fa74 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    @@ -42,8 +42,6 @@ import im.vector.riotx.core.intent.analyseIntent
     import im.vector.riotx.core.intent.getFilenameFromUri
     import im.vector.riotx.core.platform.SimpleTextWatcher
     import im.vector.riotx.core.preference.VectorPreference
    -import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_EXPORT_KEYS
    -import im.vector.riotx.core.utils.allGranted
     import im.vector.riotx.core.utils.openFileSelection
     import im.vector.riotx.core.utils.toast
     import im.vector.riotx.features.crypto.keys.KeysExporter
    @@ -142,14 +140,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
             mCrossSigningStatePreference.isVisible = true
         }
     
    -    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
    -        if (allGranted(grantResults)) {
    -            if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) {
    -                queryExportKeys(activeSessionHolder.getSafeActiveSession()?.myUserId ?: "", REQUEST_CODE_SAVE_MEGOLM_EXPORT)
    -            }
    -        }
    -    }
    -
         override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
             super.onActivityResult(requestCode, resultCode, data)
             if (requestCode == REQUEST_CODE_SAVE_MEGOLM_EXPORT) {
    diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
    index 7cb839eba6..a6868a1214 100644
    --- a/vector/src/main/res/values/strings.xml
    +++ b/vector/src/main/res/values/strings.xml
    @@ -2526,4 +2526,6 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
         You cannot access this message because your session is not trusted by the sender
         You cannot access this message because the sender purposely did not send the keys
         Waiting for encryption history
    +
    +    Save recovery key in
     
    
    From 2c5d2ea179fc94df81c94a5d0fa5c228379d94d0 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 10 Jul 2020 17:34:09 +0200
    Subject: [PATCH 071/122] Improve wording
    
    ---
     .../setup/KeysBackupSetupStep3Fragment.kt       | 17 ++++++++---------
     vector/src/main/res/values/strings.xml          |  2 ++
     2 files changed, 10 insertions(+), 9 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt
    index de9c0add30..124bef2e28 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt
    @@ -176,25 +176,24 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment()
                                     os.write(data.toByteArray())
                                     os.flush()
                                 }
    -                }?.let {
    -                    uri.toString()
                     }
    -                        ?: throw IOException()
    +                        ?: throw IOException("Unable to write the file")
                 }
                         .fold(
                                 { throwable ->
    -                                context?.let {
    +                                activity?.let {
                                         AlertDialog.Builder(it)
                                                 .setTitle(R.string.dialog_title_error)
    -                                            .setMessage(throwable.localizedMessage)
    +                                            .setMessage(errorFormatter.toHumanReadable(throwable))
    +
                                     }
                                 },
    -                            { path ->
    +                            {
                                     viewModel.copyHasBeenMade = true
    -
    -                                context?.let {
    +                                activity?.let {
                                         AlertDialog.Builder(it)
    -                                            .setMessage(getString(R.string.recovery_key_export_saved_as_warning, path))
    +                                            .setTitle(R.string.dialog_title_success)
    +                                            .setMessage(R.string.recovery_key_export_saved)
                                     }
                                 }
                         )
    diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
    index a6868a1214..1bfff06005 100644
    --- a/vector/src/main/res/values/strings.xml
    +++ b/vector/src/main/res/values/strings.xml
    @@ -126,6 +126,7 @@
         Confirmation
         Warning
         Error
    +    Success
     
         
         Home
    @@ -1413,6 +1414,7 @@ Why choose Riot.im?
         Share
         Save as File
         The recovery key has been saved to \'%s\'.\n\nWarning: this file may be deleted if the application is uninstalled.
    +    The recovery key has been saved.
     
         A backup already exist on your HomeServer
         It looks like you already have setup key backup from another session. Do you want to replace it with the one you’re creating?
    
    From 5a8008a4dc480aa7e0b99be5bac8eea18c6efd8a Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Sat, 11 Jul 2020 12:26:04 +0200
    Subject: [PATCH 072/122] Fix bug when restoring key backup with recovery key
    
    ---
     CHANGES.md                                                      | 1 +
     .../keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt     | 2 +-
     2 files changed, 2 insertions(+), 1 deletion(-)
    
    diff --git a/CHANGES.md b/CHANGES.md
    index 81a0e11525..27794ed032 100644
    --- a/CHANGES.md
    +++ b/CHANGES.md
    @@ -13,6 +13,7 @@ Improvements 🙌:
     Bugfix 🐛:
      - Regression |  Share action menu do not work (#1647)
      - verification issues on transition (#1555)
    + - Fix issue when restoring keys backup using recovery key
     
     Translations 🗣:
      -
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt
    index faada7ba3e..2faff3d112 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt
    @@ -49,7 +49,7 @@ class KeysBackupRestoreFromKeyViewModel @Inject constructor(
             viewModelScope.launch(Dispatchers.IO) {
                 val recoveryKey = recoveryCode.value!!
                 try {
    -                sharedViewModel.recoverUsingBackupPass(recoveryKey)
    +                sharedViewModel.recoverUsingBackupRecoveryKey(recoveryKey)
                 } catch (failure: Throwable) {
                     recoveryCodeErrorText.postValue(stringProvider.getString(R.string.keys_backup_recovery_code_error_decrypt))
                 }
    
    From c57d41863f08724f72079abb6a5e186d27df0ae1 Mon Sep 17 00:00:00 2001
    From: Valere 
    Date: Fri, 10 Jul 2020 20:09:15 +0200
    Subject: [PATCH 073/122] 4S settings screen
    
    ---
     .../java/im/vector/matrix/rx/RxSession.kt     |  50 ++++++
     .../SharedSecretStorageService.kt             |   8 +
     .../crypto/OutgoingGossipingRequestManager.kt |   4 +-
     .../crypto/recover/BootstrapBottomSheet.kt    |  12 +-
     .../recover/BootstrapCrossSigningTask.kt      |  24 +++
     .../BootstrapSetupRecoveryKeyFragment.kt      |   7 +
     .../recover/BootstrapSharedViewModel.kt       |   8 +-
     .../features/crypto/recover/BootstrapStep.kt  |   2 +-
     .../request/VerificationRequestController.kt  |   2 +-
     .../features/navigation/DefaultNavigator.kt   |   4 +-
     .../features/settings/VectorPreferences.kt    |   3 +
     .../VectorSettingsSecurityPrivacyFragment.kt  | 152 ++++++++++++++++--
     .../SignOutBottomSheetDialogFragment.kt       |   2 +-
     .../fragment_bootstrap_setup_recovery.xml     |  14 ++
     vector/src/main/res/values/strings.xml        |   9 ++
     .../xml/vector_settings_security_privacy.xml  |  17 ++
     16 files changed, 294 insertions(+), 24 deletions(-)
    
    diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt
    index e8fef1361d..cd4481efea 100644
    --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt
    +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt
    @@ -19,7 +19,11 @@ package im.vector.matrix.rx
     import androidx.paging.PagedList
     import im.vector.matrix.android.api.query.QueryStringValue
     import im.vector.matrix.android.api.session.Session
    +import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
    +import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
     import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
    +import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
    +import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
     import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams
     import im.vector.matrix.android.api.session.group.model.GroupSummary
     import im.vector.matrix.android.api.session.identity.ThreePid
    @@ -36,9 +40,11 @@ import im.vector.matrix.android.api.util.toOptional
     import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
     import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
     import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
    +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
     import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
     import io.reactivex.Observable
     import io.reactivex.Single
    +import io.reactivex.functions.Function3
     
     class RxSession(private val session: Session) {
     
    @@ -165,6 +171,50 @@ class RxSession(private val session: Session) {
                         session.widgetService().getRoomWidgets(roomId, widgetId, widgetTypes, excludedTypes)
                     }
         }
    +
    +    data class SecretsSynchronisationInfo(
    +            val isBackupSetup: Boolean = false,
    +            val isCrossSigningEnabled: Boolean = false,
    +            val isCrossSigningTrusted: Boolean = false,
    +            val allPrivateKeysKnown: Boolean = false,
    +            val megolmBackupAvailable: Boolean = false,
    +            val megolmSecretKnown: Boolean = false,
    +            val isMegolmKeyIn4S: Boolean = false
    +    )
    +
    +    fun liveSecretSynchronisationInfo(): Observable {
    +        return Observable.combineLatest, Optional, Optional, SecretsSynchronisationInfo>(
    +                liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME)),
    +                liveCrossSigningInfo(session.myUserId),
    +                liveCrossSigningPrivateKeys(),
    +                Function3 { _, crossSigningInfo, pInfo ->
    +                    // first check if 4S is already setup
    +                    val is4SSetup = session.sharedSecretStorageService.isRecoverySetup()
    +                    val isCrossSigningEnabled = crossSigningInfo.getOrNull() != null
    +                    val isCrossSigningTrusted = crossSigningInfo.getOrNull()?.isTrusted() == true
    +                    val allPrivateKeysKnown = pInfo.getOrNull()?.master != null
    +                            && pInfo.getOrNull()?.selfSigned != null
    +                            && pInfo.getOrNull()?.user != null
    +
    +                    val keysBackupService = session.cryptoService().keysBackupService()
    +                    val currentBackupVersion = keysBackupService.currentBackupVersion
    +                    val megolmBackupAvailable = currentBackupVersion != null
    +                    val savedBackupKey = keysBackupService.getKeyBackupRecoveryKeyInfo()
    +
    +                    val megolmKeyKnown = savedBackupKey?.version == currentBackupVersion
    +                    SecretsSynchronisationInfo(
    +                            isBackupSetup = is4SSetup,
    +                            isCrossSigningEnabled = isCrossSigningEnabled,
    +                            isCrossSigningTrusted = isCrossSigningTrusted,
    +                            allPrivateKeysKnown = allPrivateKeysKnown,
    +                            megolmBackupAvailable = megolmBackupAvailable,
    +                            megolmSecretKnown = megolmKeyKnown,
    +                            isMegolmKeyIn4S = session.sharedSecretStorageService.isMegolmKeyInBackup()
    +                    )
    +                }
    +        )
    +                .distinctUntilChanged()
    +    }
     }
     
     fun Session.rx(): RxSession {
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt
    index 6644972aca..22fbcf2d26 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt
    @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.securestorage
     
     import im.vector.matrix.android.api.MatrixCallback
     import im.vector.matrix.android.api.listeners.ProgressListener
    +import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
     import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
     import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
     import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
    @@ -124,6 +125,13 @@ interface SharedSecretStorageService {
             ) is IntegrityResult.Success
         }
     
    +    fun isMegolmKeyInBackup(): Boolean {
    +        return checkShouldBeAbleToAccessSecrets(
    +                secretNames = listOf(KEYBACKUP_SECRET_SSSS_NAME),
    +                keyId = null
    +        ) is IntegrityResult.Success
    +    }
    +
         fun checkShouldBeAbleToAccessSecrets(secretNames: List, keyId: String?): IntegrityResult
     
         fun requestSecret(name: String, myOtherDeviceId: String)
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequestManager.kt
    index eb1c07cb92..5ad1013f49 100755
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequestManager.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequestManager.kt
    @@ -71,8 +71,8 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
                 delay(1500)
                 cryptoStore.getOrAddOutgoingSecretShareRequest(secretName, recipients)?.let {
                     // TODO check if there is already one that is being sent?
    -                if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
    -                    Timber.v("## CRYPTO - GOSSIP sendSecretShareRequest() : we already request for that session: $it")
    +                if (it.state == OutgoingGossipingRequestState.SENDING /**|| it.state == OutgoingGossipingRequestState.SENT*/) {
    +                    Timber.v("## CRYPTO - GOSSIP sendSecretShareRequest() : we are already sending for that session: $it")
                         return@launch
                     }
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt
    index f14d27b3d9..945a8c2866 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt
    @@ -45,7 +45,8 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
     
         @Parcelize
         data class Args(
    -            val initCrossSigningOnly: Boolean
    +            val initCrossSigningOnly: Boolean,
    +            val forceReset4S: Boolean
         ) : Parcelable
     
         override val showExpanded = true
    @@ -180,10 +181,15 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
     
             const val EXTRA_ARGS = "EXTRA_ARGS"
     
    -        fun show(fragmentManager: FragmentManager, initCrossSigningOnly: Boolean) {
    +        fun show(fragmentManager: FragmentManager, initCrossSigningOnly: Boolean, forceReset4S: Boolean) {
                 BootstrapBottomSheet().apply {
                     isCancelable = false
    -                arguments = Bundle().apply { this.putParcelable(EXTRA_ARGS, Args(initCrossSigningOnly)) }
    +                arguments = Bundle().apply {
    +                    this.putParcelable(EXTRA_ARGS, Args(
    +                            initCrossSigningOnly,
    +                            forceReset4S
    +                    ))
    +                }
                 }.show(fragmentManager, "BootstrapBottomSheet")
             }
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt
    index 9f68e09444..8781cbe570 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt
    @@ -258,6 +258,30 @@ class BootstrapCrossSigningTask @Inject constructor(
                             )
                         }
                     }
    +            } else {
    +                Timber.d("## BootstrapCrossSigningTask: Creating 4S - Existing megolm backup found")
    +                // ensure we store existing backup secret if we have it!
    +                val knownSecret = session.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()
    +                if (knownSecret != null && knownSecret.version == serverVersion.version) {
    +                    // check it matches
    +                    val isValid = awaitCallback {
    +                        session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(knownSecret.recoveryKey, it)
    +                    }
    +                    if (isValid) {
    +                        Timber.d("## BootstrapCrossSigningTask: Creating 4S - Megolm key valid and known")
    +                        awaitCallback {
    +                            extractCurveKeyFromRecoveryKey(knownSecret.recoveryKey)?.toBase64NoPadding()?.let { secret ->
    +                                ssssService.storeSecret(
    +                                        KEYBACKUP_SECRET_SSSS_NAME,
    +                                        secret,
    +                                        listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it
    +                                )
    +                            }
    +                        }
    +                    } else {
    +                        Timber.d("## BootstrapCrossSigningTask: Creating 4S - Megolm key is unknown by this session")
    +                    }
    +                }
                 }
             } catch (failure: Throwable) {
                 Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup")
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt
    index 156acf845f..ea558145c0 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt
    @@ -58,6 +58,13 @@ class BootstrapSetupRecoveryKeyFragment @Inject constructor() : VectorBaseFragme
                     bootstrapSetupSecureUseSecurityPassphrase.isVisible = false
                     bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = false
                 } else {
    +                if (state.step.reset) {
    +                    bootstrapSetupSecureText.text = getString(R.string.reset_secure_backup_title)
    +                    bootstrapSetupWarningTextView.isVisible = true
    +                } else {
    +                    bootstrapSetupSecureText.text = getString(R.string.bottom_sheet_setup_secure_backup_subtitle)
    +                    bootstrapSetupWarningTextView.isVisible = false
    +                }
                     // Choose between create a passphrase or use a recovery key
                     bootstrapSetupSecureSubmit.isVisible = false
                     bootstrapSetupSecureUseSecurityKey.isVisible = true
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt
    index 22dcab217e..8b247bd975 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt
    @@ -69,7 +69,11 @@ class BootstrapSharedViewModel @AssistedInject constructor(
     
         init {
     
    -        if (args.initCrossSigningOnly) {
    +        if (args.forceReset4S) {
    +            setState {
    +                copy(step = BootstrapStep.FirstForm(keyBackUpExist = false, reset = true))
    +            }
    +        } else if (args.initCrossSigningOnly) {
                 // Go straight to account password
                 setState {
                     copy(step = BootstrapStep.AccountPassword(false))
    @@ -554,7 +558,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
             override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? {
                 val fragment: BootstrapBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
                 val args: BootstrapBottomSheet.Args = fragment.arguments?.getParcelable(BootstrapBottomSheet.EXTRA_ARGS)
    -                    ?: BootstrapBottomSheet.Args(initCrossSigningOnly = true)
    +                    ?: BootstrapBottomSheet.Args(initCrossSigningOnly = true, forceReset4S = false)
                 return fragment.bootstrapViewModelFactory.create(state, args)
             }
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt
    index c7639068d1..71b00016ab 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt
    @@ -89,7 +89,7 @@ sealed class BootstrapStep {
         object CheckingMigration : BootstrapStep()
     
         // Use will be asked to choose between passphrase or recovery key, or to start process if a key backup exists
    -    data class FirstForm(val keyBackUpExist: Boolean) : BootstrapStep()
    +    data class FirstForm(val keyBackUpExist: Boolean, val reset: Boolean = false) : BootstrapStep()
     
         data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
         data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt
    index 88f6607a41..8ac2ce72cb 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt
    @@ -146,7 +146,7 @@ class VerificationRequestController @Inject constructor(
                 }
             }
     
    -        if (state.isMe && state.currentDeviceCanCrossSign) {
    +        if (state.isMe && state.currentDeviceCanCrossSign && !state.selfVerificationMode) {
                 dividerItem {
                     id("sep_notMe")
                 }
    diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
    index 3c3e85d3f6..26908182b9 100644
    --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
    @@ -145,7 +145,7 @@ class DefaultNavigator @Inject constructor(
     
         override fun upgradeSessionSecurity(context: Context, initCrossSigningOnly: Boolean) {
             if (context is VectorBaseActivity) {
    -            BootstrapBottomSheet.show(context.supportFragmentManager, initCrossSigningOnly)
    +            BootstrapBottomSheet.show(context.supportFragmentManager, initCrossSigningOnly, false)
             }
         }
     
    @@ -221,7 +221,7 @@ class DefaultNavigator @Inject constructor(
             // if cross signing is enabled we should propose full 4S
             sessionHolder.getSafeActiveSession()?.let { session ->
                 if (session.cryptoService().crossSigningService().canCrossSign() && context is VectorBaseActivity) {
    -                BootstrapBottomSheet.show(context.supportFragmentManager, false)
    +                BootstrapBottomSheet.show(context.supportFragmentManager, initCrossSigningOnly = false, forceReset4S = false)
                 } else {
                     context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport))
                 }
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
    index e4a0eb3eb6..b485e337a9 100755
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
    @@ -72,6 +72,9 @@ class VectorPreferences @Inject constructor(private val context: Context) {
             const val SETTINGS_ALLOW_INTEGRATIONS_KEY = "SETTINGS_ALLOW_INTEGRATIONS_KEY"
             const val SETTINGS_INTEGRATION_MANAGER_UI_URL_KEY = "SETTINGS_INTEGRATION_MANAGER_UI_URL_KEY"
             const val SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY = "SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY"
    +        const val SETTINGS_CRYPTOGRAPHY_MANAGE_4S_CATEGORY_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_4S_CATEGORY_KEY"
    +        const val SETTINGS_SECURE_BACKUP_RECOVERY_PREFERENCE_KEY = "SETTINGS_SECURE_BACKUP_RECOVERY_PREFERENCE_KEY"
    +//        const val SETTINGS_SECURE_BACKUP_RESET_PREFERENCE_KEY = "SETTINGS_SECURE_BACKUP_RESET_PREFERENCE_KEY"
     
             // user
             const val SETTINGS_PROFILE_PICTURE_PREFERENCE_KEY = "SETTINGS_PROFILE_PICTURE_PREFERENCE_KEY"
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    index 73c167fa74..afb9463aee 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    @@ -33,6 +33,8 @@ import im.vector.matrix.android.internal.crypto.crosssigning.isVerified
     import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
     import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
     import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
    +import im.vector.matrix.rx.RxSession
    +import im.vector.matrix.rx.rx
     import im.vector.riotx.R
     import im.vector.riotx.core.di.ActiveSessionHolder
     import im.vector.riotx.core.dialogs.ExportKeysDialog
    @@ -41,12 +43,17 @@ import im.vector.riotx.core.intent.ExternalIntentData
     import im.vector.riotx.core.intent.analyseIntent
     import im.vector.riotx.core.intent.getFilenameFromUri
     import im.vector.riotx.core.platform.SimpleTextWatcher
    +import im.vector.riotx.core.platform.VectorBaseActivity
     import im.vector.riotx.core.preference.VectorPreference
    +import im.vector.riotx.core.preference.VectorPreferenceCategory
     import im.vector.riotx.core.utils.openFileSelection
     import im.vector.riotx.core.utils.toast
     import im.vector.riotx.features.crypto.keys.KeysExporter
     import im.vector.riotx.features.crypto.keys.KeysImporter
     import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
    +import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet
    +import io.reactivex.android.schedulers.AndroidSchedulers
    +import io.reactivex.disposables.Disposable
     import javax.inject.Inject
     
     class VectorSettingsSecurityPrivacyFragment @Inject constructor(
    @@ -56,6 +63,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
     
         override var titleRes = R.string.settings_security_and_privacy
         override val preferenceXmlRes = R.xml.vector_settings_security_privacy
    +    private var disposables = emptyList().toMutableList()
     
         // cryptography
         private val mCryptographyCategory by lazy {
    @@ -92,6 +100,109 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
             // My device name may have been updated
             refreshMyDevice()
             refreshXSigningStatus()
    +        session.rx().liveSecretSynchronisationInfo()
    +                .observeOn(AndroidSchedulers.mainThread())
    +                .subscribe {
    +                    refresh4SSection(it)
    +                    refreshXSigningStatus()
    +                }.also {
    +                    disposables.add(it)
    +                }
    +    }
    +
    +    private val secureBackupCategory by lazy {
    +        findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_MANAGE_4S_CATEGORY_KEY)
    +    }
    +    private val secureBackupPreference by lazy {
    +        findPreference(VectorPreferences.SETTINGS_SECURE_BACKUP_RECOVERY_PREFERENCE_KEY)
    +    }
    +//    private val secureBackupResetPreference by lazy {
    +//        findPreference(VectorPreferences.SETTINGS_SECURE_BACKUP_RESET_PREFERENCE_KEY)
    +//    }
    +
    +    override fun onPause() {
    +        super.onPause()
    +        disposables.forEach {
    +            it.dispose()
    +        }
    +        disposables.clear()
    +    }
    +
    +    private fun refresh4SSection(state: RxSession.SecretsSynchronisationInfo) {
    +        secureBackupCategory?.isVisible = false
    +
    +        // it's a lot of if / else if / else
    +        // But it's not yet clear how to manage all cases
    +        if (!state.isCrossSigningEnabled) {
    +            // There is not cross signing, so we can remove the section
    +        } else {
    +            secureBackupCategory?.isVisible = true
    +
    +            if (!state.isBackupSetup) {
    +                if (state.isCrossSigningEnabled && state.allPrivateKeysKnown) {
    +                    // You can setup recovery!
    +                    secureBackupCategory?.isVisible = true
    +                    secureBackupPreference?.isVisible = true
    +                    secureBackupPreference?.title = getString(R.string.settings_secure_backup_setup)
    +                    secureBackupPreference?.isEnabled = true
    +                    secureBackupPreference?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
    +                        BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = false)
    +                        true
    +                    }
    +                } else {
    +                    // just hide all, you can't setup from here
    +                    // you should synchronize to get gossips
    +                    secureBackupCategory?.isVisible = false
    +                }
    +                return
    +            }
    +
    +            // so here we know that 4S is setup
    +            if (state.isCrossSigningTrusted && state.allPrivateKeysKnown) {
    +                // Looks like we have all cross signing secrets and session is trusted
    +                // Let's see if there is a megolm backup
    +                if (!state.megolmBackupAvailable || state.megolmSecretKnown) {
    +                    // Only option here is to create a new backup if you want?
    +                    // aka reset
    +                    secureBackupCategory?.isVisible = true
    +                    secureBackupPreference?.isVisible = true
    +                    secureBackupPreference?.title = getString(R.string.settings_secure_backup_reset)
    +                    secureBackupPreference?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
    +                        BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = true)
    +                        true
    +                    }
    +                } else if (!state.megolmSecretKnown) {
    +                    // megolm backup is available but we don't have key
    +                    // you could try to synchronize to get missing megolm key ?
    +                    secureBackupCategory?.isVisible = true
    +                    secureBackupPreference?.isVisible = true
    +                    secureBackupPreference?.title = getString(R.string.settings_secure_backup_enter_to_setup)
    +                    secureBackupPreference?.isEnabled = true
    +                    secureBackupPreference?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
    +                        (requireActivity() as? VectorBaseActivity)?.let {
    +                            it.navigator.requestSelfSessionVerification(it)
    +                        }
    +                        true
    +                    }
    +                } else {
    +                    secureBackupCategory?.isVisible = false
    +                }
    +                return
    +            } else {
    +                // there is a backup, but this session is not trusted, or is missing some secrets
    +                // you should enter passphrase to get them or verify against another session
    +                secureBackupCategory?.isVisible = true
    +                secureBackupPreference?.isVisible = true
    +                secureBackupPreference?.title = getString(R.string.settings_secure_backup_enter_to_setup)
    +                secureBackupPreference?.isEnabled = true
    +                secureBackupPreference?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
    +                    (requireActivity() as? VectorBaseActivity)?.let {
    +                        it.navigator.requestSelfSessionVerification(it)
    +                    }
    +                    true
    +                }
    +            }
    +        }
         }
     
         override fun bindPref() {
    @@ -115,26 +226,43 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
             }
     
             refreshXSigningStatus()
    +
    +//        secureBackupResetPreference?.let { pref ->
    +//            val destructiveColor = ContextCompat.getColor(requireContext(), R.color.riotx_destructive_accent)
    +//            pref.title = span {
    +//                text = getString(R.string.keys_backup_restore_setup_recovery_key)
    +//                textColor = destructiveColor
    +//            }
    +//            pref.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_delete)?.let {
    +//                ThemeUtils.tintDrawableWithColor(it, destructiveColor)
    +//            }
    +//        }
         }
     
    +    // Todo this should be refactored and use same state as 4S section
         private fun refreshXSigningStatus() {
             val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
             val xSigningIsEnableInAccount = crossSigningKeys != null
             val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified()
             val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign()
     
    -        if (xSigningKeyCanSign) {
    -            mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted)
    -            mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete)
    -        } else if (xSigningKeysAreTrusted) {
    -            mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom)
    -            mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted)
    -        } else if (xSigningIsEnableInAccount) {
    -            mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black)
    -            mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted)
    -        } else {
    -            mCrossSigningStatePreference.setIcon(android.R.color.transparent)
    -            mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_disabled)
    +        when {
    +            xSigningKeyCanSign        -> {
    +                mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted)
    +                mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete)
    +            }
    +            xSigningKeysAreTrusted    -> {
    +                mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom)
    +                mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted)
    +            }
    +            xSigningIsEnableInAccount -> {
    +                mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black)
    +                mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted)
    +            }
    +            else                      -> {
    +                mCrossSigningStatePreference.setIcon(android.R.color.transparent)
    +                mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_disabled)
    +            }
             }
     
             mCrossSigningStatePreference.isVisible = true
    diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt
    index 16be661f06..2ebf086796 100644
    --- a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt
    @@ -121,7 +121,7 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(),
             super.onActivityCreated(savedInstanceState)
     
             setupRecoveryButton.action = {
    -            BootstrapBottomSheet.show(parentFragmentManager, false)
    +            BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = false)
             }
     
             exitAnywayButton.action = {
    diff --git a/vector/src/main/res/layout/fragment_bootstrap_setup_recovery.xml b/vector/src/main/res/layout/fragment_bootstrap_setup_recovery.xml
    index 82344824d0..a7405cb73a 100644
    --- a/vector/src/main/res/layout/fragment_bootstrap_setup_recovery.xml
    +++ b/vector/src/main/res/layout/fragment_bootstrap_setup_recovery.xml
    @@ -71,4 +71,18 @@
             android:layout_height="1dp"
             android:background="?attr/vctr_list_divider_color" />
     
    +    
    +
     
    diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
    index 1bfff06005..37a849b3fd 100644
    --- a/vector/src/main/res/values/strings.xml
    +++ b/vector/src/main/res/values/strings.xml
    @@ -843,6 +843,15 @@
         Send message with enter
         Enter button of the soft keyboard will send message instead of adding a line break
     
    +    Secure Backup
    +    Manage
    +    Set up Secure Backup
    +    Reset Secure Backup
    +    Set up on this device
    +    Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.
    +    Generate a new Security Key or set a new Security Phrase for your existing backup.
    +    This will replace your current Key or Phrase.
    +
         Deactivate account
         Deactivate my account
         Discovery
    diff --git a/vector/src/main/res/xml/vector_settings_security_privacy.xml b/vector/src/main/res/xml/vector_settings_security_privacy.xml
    index 8b4823eac9..9bfe5e944b 100644
    --- a/vector/src/main/res/xml/vector_settings_security_privacy.xml
    +++ b/vector/src/main/res/xml/vector_settings_security_privacy.xml
    @@ -48,6 +48,23 @@
     
         
     
    +    
    +
    +        
    +
    +        
    +    
    +
    +
         
    
    From 2f0645a94e85dd1131478bf9fad93745dccfda61 Mon Sep 17 00:00:00 2001
    From: ganfra 
    Date: Sat, 11 Jul 2020 12:39:41 +0200
    Subject: [PATCH 074/122] Fix left user has no name in db
    
    ---
     .../database/helper/ChunkEntityHelper.kt      |  9 ++++----
     .../database/model/RoomMemberSummaryEntity.kt |  2 +-
     .../room/membership/RoomMemberEventHandler.kt |  9 +++++++-
     .../internal/session/sync/RoomSyncHandler.kt  | 23 ++++++++++++++++---
     4 files changed, 34 insertions(+), 9 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt
    index bc681e4eb8..a2965df27b 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt
    @@ -123,17 +123,18 @@ private fun computeIsUnique(
             realm: Realm,
             roomId: String,
             isLastForward: Boolean,
    -        myRoomMemberContent: RoomMemberContent,
    +        senderRoomMemberContent: RoomMemberContent,
             roomMemberContentsByUser: Map
     ): Boolean {
         val isHistoricalUnique = roomMemberContentsByUser.values.find {
    -        it != myRoomMemberContent && it?.displayName == myRoomMemberContent.displayName
    +        it != senderRoomMemberContent && it?.displayName == senderRoomMemberContent.displayName
         } == null
         return if (isLastForward) {
             val isLiveUnique = RoomMemberSummaryEntity
                     .where(realm, roomId)
    -                .equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, myRoomMemberContent.displayName)
    -                .findAll().none {
    +                .equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, senderRoomMemberContent.displayName)
    +                .findAll()
    +                .none {
                         !roomMemberContentsByUser.containsKey(it.userId)
                     }
             isHistoricalUnique && isLiveUnique
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberSummaryEntity.kt
    index 45bf1b3a22..e2a9af649e 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberSummaryEntity.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberSummaryEntity.kt
    @@ -24,7 +24,7 @@ import io.realm.annotations.PrimaryKey
     internal open class RoomMemberSummaryEntity(@PrimaryKey var primaryKey: String = "",
                                                 @Index var userId: String = "",
                                                 @Index var roomId: String = "",
    -                                            var displayName: String? = null,
    +                                            @Index var displayName: String? = null,
                                                 var avatarUrl: String? = null,
                                                 var reason: String? = null,
                                                 var isDirect: Boolean = false
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt
    index d7d578b635..b340766c1b 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt
    @@ -30,8 +30,15 @@ internal class RoomMemberEventHandler @Inject constructor() {
             if (event.type != EventType.STATE_ROOM_MEMBER) {
                 return false
             }
    -        val roomMember = event.content.toModel() ?: return false
             val userId = event.stateKey ?: return false
    +        val roomMember = event.content.toModel()
    +        return handle(realm, roomId, userId, roomMember)
    +    }
    +
    +    fun handle(realm: Realm, roomId: String, userId: String, roomMember: RoomMemberContent?): Boolean {
    +        if (roomMember == null) {
    +            return false
    +        }
             val roomMemberEntity = RoomMemberEntityFactory.create(roomId, userId, roomMember)
             realm.insertOrUpdate(roomMemberEntity)
             if (roomMember.membership.isActive()) {
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
    index de2c3cda57..6f27da2c5c 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
    @@ -32,6 +32,7 @@ import im.vector.matrix.android.internal.database.helper.addOrUpdate
     import im.vector.matrix.android.internal.database.helper.addTimelineEvent
     import im.vector.matrix.android.internal.database.helper.deleteOnCascade
     import im.vector.matrix.android.internal.database.mapper.ContentMapper
    +import im.vector.matrix.android.internal.database.mapper.asDomain
     import im.vector.matrix.android.internal.database.mapper.toEntity
     import im.vector.matrix.android.internal.database.model.ChunkEntity
     import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
    @@ -313,14 +314,15 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                         root = eventEntity
                     }
                     if (event.type == EventType.STATE_ROOM_MEMBER) {
    -                    roomMemberContentsByUser[event.stateKey] = event.content.toModel()
    -                    roomMemberEventHandler.handle(realm, roomEntity.roomId, event)
    +                    val fixedContent = event.getFixedRoomMemberContent()
    +                    roomMemberContentsByUser[event.stateKey] = fixedContent
    +                    roomMemberEventHandler.handle(realm, roomEntity.roomId, event.stateKey, fixedContent)
                     }
                 }
                 roomMemberContentsByUser.getOrPut(event.senderId) {
                     // If we don't have any new state on this user, get it from db
                     val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root
    -                ContentMapper.map(rootStateEvent?.content).toModel()
    +                rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
                 }
     
                 chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
    @@ -411,4 +413,19 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                 }
             }
         }
    +
    +    private fun Event.getFixedRoomMemberContent(): RoomMemberContent?{
    +        val content = content.toModel()
    +        // if user is leaving, we should grab his last name and avatar from prevContent
    +        return if (content?.membership?.isLeft() == true) {
    +            val prevContent = resolvedPrevContent().toModel()
    +            content.copy(
    +                    displayName = prevContent?.displayName,
    +                    avatarUrl = prevContent?.avatarUrl
    +            )
    +        } else {
    +            content
    +        }
    +    }
    +
     }
    
    From 31984a57d6a35eb22c4d9e2aa6c4533dbfbabae6 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Sat, 11 Jul 2020 12:46:10 +0200
    Subject: [PATCH 075/122] Subscribe to view model is already disposed
    
    ---
     .../riotx/features/home/HomeDetailFragment.kt   | 17 +++++++++--------
     1 file changed, 9 insertions(+), 8 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
    index f0fdc207f9..770b03d345 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
    @@ -193,14 +193,15 @@ class HomeDetailFragment @Inject constructor(
         }
     
         private fun setupKeysBackupBanner() {
    -        serverBackupStatusViewModel.subscribe(this) {
    -            when (val banState = it.bannerState.invoke()) {
    -                is BannerState.Setup  -> homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false)
    -                BannerState.BackingUp ->  homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false)
    -                null,
    -                BannerState.Hidden    -> homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false)
    -            }
    -        }.disposeOnDestroyView()
    +        serverBackupStatusViewModel
    +                .subscribe(this) {
    +                    when (val banState = it.bannerState.invoke()) {
    +                        is BannerState.Setup  -> homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false)
    +                        BannerState.BackingUp -> homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false)
    +                        null,
    +                        BannerState.Hidden    -> homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false)
    +                    }
    +                }
             homeKeysBackupBanner.delegate = this
         }
     
    
    From 9e1c30ec5d534eb26d5ec088d433022dc3f1f6c8 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Sat, 11 Jul 2020 12:46:37 +0200
    Subject: [PATCH 076/122] No need to return the disposable, it is never used
    
    ---
     .../java/im/vector/riotx/core/platform/VectorBaseActivity.kt   | 3 +--
     .../java/im/vector/riotx/core/platform/VectorBaseFragment.kt   | 3 +--
     2 files changed, 2 insertions(+), 4 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
    index bdd873d0cd..59bf7a8aeb 100644
    --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
    @@ -162,9 +162,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
             return this
         }
     
    -    protected fun Disposable.disposeOnDestroy(): Disposable {
    +    protected fun Disposable.disposeOnDestroy() {
             uiDisposables.add(this)
    -        return this
         }
     
         override fun onCreate(savedInstanceState: Bundle?) {
    diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt
    index c0b1b54c09..f4343a3e58 100644
    --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt
    @@ -234,9 +234,8 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
     
         private val uiDisposables = CompositeDisposable()
     
    -    protected fun Disposable.disposeOnDestroyView(): Disposable {
    +    protected fun Disposable.disposeOnDestroyView() {
             uiDisposables.add(this)
    -        return this
         }
     
         /* ==========================================================================================
    
    From aa1843774a1a01bc11d731ef4c81490d3b0e1289 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Sat, 11 Jul 2020 12:50:16 +0200
    Subject: [PATCH 077/122] Cleanup
    
    ---
     .../features/settings/VectorSettingsSecurityPrivacyFragment.kt  | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    index afb9463aee..0c4a0404d7 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    @@ -63,7 +63,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
     
         override var titleRes = R.string.settings_security_and_privacy
         override val preferenceXmlRes = R.xml.vector_settings_security_privacy
    -    private var disposables = emptyList().toMutableList()
    +    private var disposables = mutableListOf()
     
         // cryptography
         private val mCryptographyCategory by lazy {
    
    From de5f182f29600d8adf6db874924c13c17ae08e43 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Sat, 11 Jul 2020 12:57:20 +0200
    Subject: [PATCH 078/122] Move SecretsSynchronisationInfo to its file And do
     some other cleanup
    
    ---
     .../java/im/vector/matrix/rx/RxSession.kt     | 17 +++---------
     .../matrix/rx/SecretsSynchronisationInfo.kt   | 27 +++++++++++++++++++
     .../VectorSettingsSecurityPrivacyFragment.kt  |  4 +--
     3 files changed, 33 insertions(+), 15 deletions(-)
     create mode 100644 matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/SecretsSynchronisationInfo.kt
    
    diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt
    index cd4481efea..98d055bee7 100644
    --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt
    +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt
    @@ -17,6 +17,7 @@
     package im.vector.matrix.rx
     
     import androidx.paging.PagedList
    +import im.vector.matrix.android.api.extensions.orFalse
     import im.vector.matrix.android.api.query.QueryStringValue
     import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
    @@ -172,16 +173,6 @@ class RxSession(private val session: Session) {
                     }
         }
     
    -    data class SecretsSynchronisationInfo(
    -            val isBackupSetup: Boolean = false,
    -            val isCrossSigningEnabled: Boolean = false,
    -            val isCrossSigningTrusted: Boolean = false,
    -            val allPrivateKeysKnown: Boolean = false,
    -            val megolmBackupAvailable: Boolean = false,
    -            val megolmSecretKnown: Boolean = false,
    -            val isMegolmKeyIn4S: Boolean = false
    -    )
    -
         fun liveSecretSynchronisationInfo(): Observable {
             return Observable.combineLatest, Optional, Optional, SecretsSynchronisationInfo>(
                     liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME)),
    @@ -192,9 +183,9 @@ class RxSession(private val session: Session) {
                         val is4SSetup = session.sharedSecretStorageService.isRecoverySetup()
                         val isCrossSigningEnabled = crossSigningInfo.getOrNull() != null
                         val isCrossSigningTrusted = crossSigningInfo.getOrNull()?.isTrusted() == true
    -                    val allPrivateKeysKnown = pInfo.getOrNull()?.master != null
    -                            && pInfo.getOrNull()?.selfSigned != null
    -                            && pInfo.getOrNull()?.user != null
    +                    val allPrivateKeysKnown = pInfo.getOrNull()
    +                            ?.let { it.master != null && it.selfSigned != null && it.user != null }
    +                            .orFalse()
     
                         val keysBackupService = session.cryptoService().keysBackupService()
                         val currentBackupVersion = keysBackupService.currentBackupVersion
    diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/SecretsSynchronisationInfo.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/SecretsSynchronisationInfo.kt
    new file mode 100644
    index 0000000000..616783706b
    --- /dev/null
    +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/SecretsSynchronisationInfo.kt
    @@ -0,0 +1,27 @@
    +/*
    + * Copyright (c) 2020 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.rx
    +
    +data class SecretsSynchronisationInfo(
    +        val isBackupSetup: Boolean,
    +        val isCrossSigningEnabled: Boolean,
    +        val isCrossSigningTrusted: Boolean,
    +        val allPrivateKeysKnown: Boolean,
    +        val megolmBackupAvailable: Boolean,
    +        val megolmSecretKnown: Boolean,
    +        val isMegolmKeyIn4S: Boolean
    +)
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    index 0c4a0404d7..858b5eee76 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    @@ -33,7 +33,7 @@ import im.vector.matrix.android.internal.crypto.crosssigning.isVerified
     import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
     import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
     import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
    -import im.vector.matrix.rx.RxSession
    +import im.vector.matrix.rx.SecretsSynchronisationInfo
     import im.vector.matrix.rx.rx
     import im.vector.riotx.R
     import im.vector.riotx.core.di.ActiveSessionHolder
    @@ -128,7 +128,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
             disposables.clear()
         }
     
    -    private fun refresh4SSection(state: RxSession.SecretsSynchronisationInfo) {
    +    private fun refresh4SSection(state: SecretsSynchronisationInfo) {
             secureBackupCategory?.isVisible = false
     
             // it's a lot of if / else if / else
    
    From 1c17bd9f5a957560d740986c75b231ce16577434 Mon Sep 17 00:00:00 2001
    From: ganfra 
    Date: Sat, 11 Jul 2020 12:57:49 +0200
    Subject: [PATCH 079/122] Clean code
    
    ---
     .../matrix/android/api/session/room/RoomService.kt   |  2 +-
     .../session/room/members/ChangeMembershipState.kt    |  1 -
     .../room/timeline/TokenChunkEventPersistor.kt        |  3 ++-
     .../android/internal/session/sync/RoomSyncHandler.kt |  4 +---
     .../vector/riotx/core/ui/views/KeysBackupBanner.kt   |  2 --
     .../vector/riotx/features/home/HomeDetailFragment.kt |  1 -
     .../room/detail/JumpToBottomViewVisibilityManager.kt |  2 --
     .../features/roomdirectory/RoomDirectoryViewModel.kt |  2 +-
     .../features/roomprofile/RoomProfileViewModel.kt     |  2 +-
     .../roomprofile/settings/RoomSettingsViewModel.kt    | 12 +++++++-----
     10 files changed, 13 insertions(+), 18 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt
    index 3093fb8ec5..813d7d98b2 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt
    @@ -111,7 +111,7 @@ interface RoomService {
          * It won't know anything about change being done in other client.
          * Keys are roomId or roomAlias, depending of what you used as parameter for the join/leave action
          */
    -    fun getChangeMembershipsLive(): LiveData>
    +    fun getChangeMembershipsLive(): LiveData>
     
         fun getExistingDirectRoomWithUser(otherUserId: String) : Room?
     }
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/ChangeMembershipState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/ChangeMembershipState.kt
    index 8f42e310bf..1094f9cb21 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/ChangeMembershipState.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/ChangeMembershipState.kt
    @@ -30,5 +30,4 @@ sealed class ChangeMembershipState() {
         fun isSuccessful() = this is Joined || this is Left
     
         fun isFailed() = this is FailedJoining || this is FailedLeaving
    -
     }
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt
    index 87bff38587..b0c697ee6c 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt
    @@ -245,7 +245,8 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
                 it.deleteOnCascade()
             }
             val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
    -        val shouldUpdateSummary = roomSummaryEntity.latestPreviewableEvent == null || (chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS)
    +        val shouldUpdateSummary = roomSummaryEntity.latestPreviewableEvent == null
    +                || (chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS)
             if (shouldUpdateSummary) {
                 val latestPreviewableEvent = TimelineEventEntity.latestEvent(
                         realm,
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
    index 6f27da2c5c..df4f52bcc9 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
    @@ -31,7 +31,6 @@ import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResu
     import im.vector.matrix.android.internal.database.helper.addOrUpdate
     import im.vector.matrix.android.internal.database.helper.addTimelineEvent
     import im.vector.matrix.android.internal.database.helper.deleteOnCascade
    -import im.vector.matrix.android.internal.database.mapper.ContentMapper
     import im.vector.matrix.android.internal.database.mapper.asDomain
     import im.vector.matrix.android.internal.database.mapper.toEntity
     import im.vector.matrix.android.internal.database.model.ChunkEntity
    @@ -414,7 +413,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
             }
         }
     
    -    private fun Event.getFixedRoomMemberContent(): RoomMemberContent?{
    +    private fun Event.getFixedRoomMemberContent(): RoomMemberContent? {
             val content = content.toModel()
             // if user is leaving, we should grab his last name and avatar from prevContent
             return if (content?.membership?.isLeft() == true) {
    @@ -427,5 +426,4 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                 content
             }
         }
    -
     }
    diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt
    index d457099087..0152f7c2a8 100755
    --- a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt
    @@ -19,13 +19,11 @@ package im.vector.riotx.core.ui.views
     import android.content.Context
     import android.util.AttributeSet
     import android.view.View
    -import android.view.ViewGroup
     import android.widget.TextView
     import androidx.constraintlayout.widget.ConstraintLayout
     import androidx.core.content.edit
     import androidx.core.view.isVisible
     import androidx.preference.PreferenceManager
    -import androidx.transition.TransitionManager
     import butterknife.BindView
     import butterknife.ButterKnife
     import butterknife.OnClick
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
    index e7ee8ca577..5da5049045 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
    @@ -32,7 +32,6 @@ import im.vector.matrix.android.api.util.toMatrixItem
     import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
     import im.vector.riotx.R
     import im.vector.riotx.core.extensions.commitTransaction
    -import im.vector.riotx.core.extensions.commitTransactionNow
     import im.vector.riotx.core.glide.GlideApp
     import im.vector.riotx.core.platform.ToolbarConfigurable
     import im.vector.riotx.core.platform.VectorBaseActivity
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt
    index 4be5502678..50a28b8a8b 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt
    @@ -20,7 +20,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
     import androidx.recyclerview.widget.RecyclerView
     import com.google.android.material.floatingactionbutton.FloatingActionButton
     import im.vector.riotx.core.utils.Debouncer
    -import timber.log.Timber
     
     /**
      * Show or hide the jumpToBottomView, depending on the scrolling and if the timeline is displaying the more recent event
    @@ -67,7 +66,6 @@ class JumpToBottomViewVisibilityManager(
         }
     
         private fun maybeShowJumpToBottomViewVisibility() {
    -        Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}")
             if (layoutManager.findFirstVisibleItemPosition() != 0) {
                 jumpToBottomView.show()
             } else {
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt
    index 96de55a5b8..1b51ab1822 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt
    @@ -112,7 +112,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
             if (it.roomDirectoryData == action.roomDirectoryData) {
                 return@withState
             }
    -        setState{
    +        setState {
                 copy(roomDirectoryData = action.roomDirectoryData)
             }
             reset("")
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt
    index d673422d06..bab0331ccb 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt
    @@ -73,7 +73,7 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted private val ini
                     .subscribe {
                         val powerLevelsHelper = PowerLevelsHelper(it)
                         setState {
    -                        copy(canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, isState = true, eventType = EventType.STATE_ROOM_AVATAR))
    +                        copy(canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true,  EventType.STATE_ROOM_AVATAR))
                         }
                     }
                     .disposeOnClear()
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt
    index 4aa4a437ac..652c5cf4c5 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/settings/RoomSettingsViewModel.kt
    @@ -101,11 +101,13 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
                     .subscribe {
                         val powerLevelsHelper = PowerLevelsHelper(it)
                         val permissions = RoomSettingsViewState.ActionPermissions(
    -                            canChangeName = powerLevelsHelper.isUserAllowedToSend(session.myUserId, isState = true, eventType = EventType.STATE_ROOM_NAME),
    -                            canChangeTopic =  powerLevelsHelper.isUserAllowedToSend(session.myUserId, isState = true, eventType = EventType.STATE_ROOM_TOPIC),
    -                            canChangeCanonicalAlias = powerLevelsHelper.isUserAllowedToSend(session.myUserId, isState = true, eventType = EventType.STATE_ROOM_CANONICAL_ALIAS),
    -                            canChangeHistoryReadability = powerLevelsHelper.isUserAllowedToSend(session.myUserId, isState = true, eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY),
    -                            canEnableEncryption =  powerLevelsHelper.isUserAllowedToSend(session.myUserId, isState = true, eventType = EventType.STATE_ROOM_ENCRYPTION)
    +                            canChangeName = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME),
    +                            canChangeTopic =  powerLevelsHelper.isUserAllowedToSend(session.myUserId,  true, EventType.STATE_ROOM_TOPIC),
    +                            canChangeCanonicalAlias = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true,
    +                                    EventType.STATE_ROOM_CANONICAL_ALIAS),
    +                            canChangeHistoryReadability = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true,
    +                                    EventType.STATE_ROOM_HISTORY_VISIBILITY),
    +                            canEnableEncryption =  powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION)
                         )
                         setState { copy(actionPermissions = permissions) }
                     }
    
    From 6f4ea83fa9a6aa59ed69b9ff614f7a37a7c9a407 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Sat, 11 Jul 2020 13:03:53 +0200
    Subject: [PATCH 080/122] Create allKnown() fun
    
    ---
     .../src/main/java/im/vector/matrix/rx/RxSession.kt           | 4 +---
     .../crypto/crosssigning/DefaultCrossSigningService.kt        | 5 ++---
     .../matrix/android/internal/crypto/store/PrivateKeysInfo.kt  | 4 +++-
     .../features/workers/signout/ServerBackupStatusViewModel.kt  | 5 ++---
     4 files changed, 8 insertions(+), 10 deletions(-)
    
    diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt
    index 98d055bee7..ad2245eef3 100644
    --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt
    +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt
    @@ -183,9 +183,7 @@ class RxSession(private val session: Session) {
                         val is4SSetup = session.sharedSecretStorageService.isRecoverySetup()
                         val isCrossSigningEnabled = crossSigningInfo.getOrNull() != null
                         val isCrossSigningTrusted = crossSigningInfo.getOrNull()?.isTrusted() == true
    -                    val allPrivateKeysKnown = pInfo.getOrNull()
    -                            ?.let { it.master != null && it.selfSigned != null && it.user != null }
    -                            .orFalse()
    +                    val allPrivateKeysKnown = pInfo.getOrNull()?.allKnown().orFalse()
     
                         val keysBackupService = session.cryptoService().keysBackupService()
                         val currentBackupVersion = keysBackupService.currentBackupVersion
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt
    index fdecfe202e..5a7c07fb53 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt
    @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.crosssigning
     
     import androidx.lifecycle.LiveData
     import im.vector.matrix.android.api.MatrixCallback
    +import im.vector.matrix.android.api.extensions.orFalse
     import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
     import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
     import im.vector.matrix.android.api.util.Optional
    @@ -509,9 +510,7 @@ internal class DefaultCrossSigningService @Inject constructor(
     
         override fun allPrivateKeysKnown(): Boolean {
             return checkSelfTrust().isVerified()
    -                && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null
    -                && cryptoStore.getCrossSigningPrivateKeys()?.user != null
    -                && cryptoStore.getCrossSigningPrivateKeys()?.master != null
    +                && cryptoStore.getCrossSigningPrivateKeys()?.allKnown().orFalse()
         }
     
         override fun trustUser(otherUserId: String, callback: MatrixCallback) {
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/PrivateKeysInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/PrivateKeysInfo.kt
    index a10b6d2645..d1591e35d8 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/PrivateKeysInfo.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/PrivateKeysInfo.kt
    @@ -20,4 +20,6 @@ data class PrivateKeysInfo(
             val master: String? = null,
             val selfSigned: String? = null,
             val user: String? = null
    -)
    +) {
    +    fun allKnown() = master != null && selfSigned != null && user != null
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt
    index dca98c16b2..bfeb959534 100644
    --- a/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt
    @@ -26,6 +26,7 @@ import com.airbnb.mvrx.Uninitialized
     import com.airbnb.mvrx.ViewModelContext
     import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
    +import im.vector.matrix.android.api.extensions.orFalse
     import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
     import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
    @@ -114,9 +115,7 @@ class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialS
                         // So recovery is not setup
                         // Check if cross signing is enabled and local secrets known
                         if (crossSigningInfo.getOrNull()?.isTrusted() == true
    -                            && pInfo.getOrNull()?.master != null
    -                            && pInfo.getOrNull()?.selfSigned != null
    -                            && pInfo.getOrNull()?.user != null
    +                            && pInfo.getOrNull()?.allKnown().orFalse()
                         ) {
                             // So 4S is not setup and we have local secrets,
                             return@Function4 BannerState.Setup(numberOfKeys = getNumberOfKeysToBackup())
    
    From 1afabb69c11469f5b5e798024e686a3bb49f8fe0 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Sat, 11 Jul 2020 13:13:08 +0200
    Subject: [PATCH 081/122] Cleanup pref
    
    ---
     .../features/settings/VectorPreferences.kt    |  2 -
     .../VectorSettingsSecurityPrivacyFragment.kt  | 50 +++++++++----------
     2 files changed, 25 insertions(+), 27 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
    index b485e337a9..9d08a9f62f 100755
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
    @@ -72,8 +72,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
             const val SETTINGS_ALLOW_INTEGRATIONS_KEY = "SETTINGS_ALLOW_INTEGRATIONS_KEY"
             const val SETTINGS_INTEGRATION_MANAGER_UI_URL_KEY = "SETTINGS_INTEGRATION_MANAGER_UI_URL_KEY"
             const val SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY = "SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY"
    -        const val SETTINGS_CRYPTOGRAPHY_MANAGE_4S_CATEGORY_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_4S_CATEGORY_KEY"
    -        const val SETTINGS_SECURE_BACKUP_RECOVERY_PREFERENCE_KEY = "SETTINGS_SECURE_BACKUP_RECOVERY_PREFERENCE_KEY"
     //        const val SETTINGS_SECURE_BACKUP_RESET_PREFERENCE_KEY = "SETTINGS_SECURE_BACKUP_RESET_PREFERENCE_KEY"
     
             // user
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    index 858b5eee76..7d524b76de 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    @@ -111,10 +111,10 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
         }
     
         private val secureBackupCategory by lazy {
    -        findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_MANAGE_4S_CATEGORY_KEY)
    +        findPreference("SETTINGS_CRYPTOGRAPHY_MANAGE_4S_CATEGORY_KEY")!!
         }
         private val secureBackupPreference by lazy {
    -        findPreference(VectorPreferences.SETTINGS_SECURE_BACKUP_RECOVERY_PREFERENCE_KEY)
    +        findPreference("SETTINGS_SECURE_BACKUP_RECOVERY_PREFERENCE_KEY")!!
         }
     //    private val secureBackupResetPreference by lazy {
     //        findPreference(VectorPreferences.SETTINGS_SECURE_BACKUP_RESET_PREFERENCE_KEY)
    @@ -129,30 +129,30 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
         }
     
         private fun refresh4SSection(state: SecretsSynchronisationInfo) {
    -        secureBackupCategory?.isVisible = false
    +        secureBackupCategory.isVisible = false
     
             // it's a lot of if / else if / else
             // But it's not yet clear how to manage all cases
             if (!state.isCrossSigningEnabled) {
                 // There is not cross signing, so we can remove the section
             } else {
    -            secureBackupCategory?.isVisible = true
    +            secureBackupCategory.isVisible = true
     
                 if (!state.isBackupSetup) {
                     if (state.isCrossSigningEnabled && state.allPrivateKeysKnown) {
                         // You can setup recovery!
    -                    secureBackupCategory?.isVisible = true
    -                    secureBackupPreference?.isVisible = true
    -                    secureBackupPreference?.title = getString(R.string.settings_secure_backup_setup)
    -                    secureBackupPreference?.isEnabled = true
    -                    secureBackupPreference?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
    +                    secureBackupCategory.isVisible = true
    +                    secureBackupPreference.isVisible = true
    +                    secureBackupPreference.title = getString(R.string.settings_secure_backup_setup)
    +                    secureBackupPreference.isEnabled = true
    +                    secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
                             BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = false)
                             true
                         }
                     } else {
                         // just hide all, you can't setup from here
                         // you should synchronize to get gossips
    -                    secureBackupCategory?.isVisible = false
    +                    secureBackupCategory.isVisible = false
                     }
                     return
                 }
    @@ -164,38 +164,38 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
                     if (!state.megolmBackupAvailable || state.megolmSecretKnown) {
                         // Only option here is to create a new backup if you want?
                         // aka reset
    -                    secureBackupCategory?.isVisible = true
    -                    secureBackupPreference?.isVisible = true
    -                    secureBackupPreference?.title = getString(R.string.settings_secure_backup_reset)
    -                    secureBackupPreference?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
    +                    secureBackupCategory.isVisible = true
    +                    secureBackupPreference.isVisible = true
    +                    secureBackupPreference.title = getString(R.string.settings_secure_backup_reset)
    +                    secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
                             BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = true)
                             true
                         }
                     } else if (!state.megolmSecretKnown) {
                         // megolm backup is available but we don't have key
                         // you could try to synchronize to get missing megolm key ?
    -                    secureBackupCategory?.isVisible = true
    -                    secureBackupPreference?.isVisible = true
    -                    secureBackupPreference?.title = getString(R.string.settings_secure_backup_enter_to_setup)
    -                    secureBackupPreference?.isEnabled = true
    -                    secureBackupPreference?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
    +                    secureBackupCategory.isVisible = true
    +                    secureBackupPreference.isVisible = true
    +                    secureBackupPreference.title = getString(R.string.settings_secure_backup_enter_to_setup)
    +                    secureBackupPreference.isEnabled = true
    +                    secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
                             (requireActivity() as? VectorBaseActivity)?.let {
                                 it.navigator.requestSelfSessionVerification(it)
                             }
                             true
                         }
                     } else {
    -                    secureBackupCategory?.isVisible = false
    +                    secureBackupCategory.isVisible = false
                     }
                     return
                 } else {
                     // there is a backup, but this session is not trusted, or is missing some secrets
                     // you should enter passphrase to get them or verify against another session
    -                secureBackupCategory?.isVisible = true
    -                secureBackupPreference?.isVisible = true
    -                secureBackupPreference?.title = getString(R.string.settings_secure_backup_enter_to_setup)
    -                secureBackupPreference?.isEnabled = true
    -                secureBackupPreference?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
    +                secureBackupCategory.isVisible = true
    +                secureBackupPreference.isVisible = true
    +                secureBackupPreference.title = getString(R.string.settings_secure_backup_enter_to_setup)
    +                secureBackupPreference.isEnabled = true
    +                secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
                         (requireActivity() as? VectorBaseActivity)?.let {
                             it.navigator.requestSelfSessionVerification(it)
                         }
    
    From 352662d19ac44baba0504a0294b48f09d82876ac Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Sat, 11 Jul 2020 13:13:44 +0200
    Subject: [PATCH 082/122] Rename param
    
    ---
     .../VectorSettingsSecurityPrivacyFragment.kt       | 14 +++++++-------
     1 file changed, 7 insertions(+), 7 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    index 7d524b76de..271265942e 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    @@ -128,18 +128,18 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
             disposables.clear()
         }
     
    -    private fun refresh4SSection(state: SecretsSynchronisationInfo) {
    +    private fun refresh4SSection(info: SecretsSynchronisationInfo) {
             secureBackupCategory.isVisible = false
     
             // it's a lot of if / else if / else
             // But it's not yet clear how to manage all cases
    -        if (!state.isCrossSigningEnabled) {
    +        if (!info.isCrossSigningEnabled) {
                 // There is not cross signing, so we can remove the section
             } else {
                 secureBackupCategory.isVisible = true
     
    -            if (!state.isBackupSetup) {
    -                if (state.isCrossSigningEnabled && state.allPrivateKeysKnown) {
    +            if (!info.isBackupSetup) {
    +                if (info.isCrossSigningEnabled && info.allPrivateKeysKnown) {
                         // You can setup recovery!
                         secureBackupCategory.isVisible = true
                         secureBackupPreference.isVisible = true
    @@ -158,10 +158,10 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
                 }
     
                 // so here we know that 4S is setup
    -            if (state.isCrossSigningTrusted && state.allPrivateKeysKnown) {
    +            if (info.isCrossSigningTrusted && info.allPrivateKeysKnown) {
                     // Looks like we have all cross signing secrets and session is trusted
                     // Let's see if there is a megolm backup
    -                if (!state.megolmBackupAvailable || state.megolmSecretKnown) {
    +                if (!info.megolmBackupAvailable || info.megolmSecretKnown) {
                         // Only option here is to create a new backup if you want?
                         // aka reset
                         secureBackupCategory.isVisible = true
    @@ -171,7 +171,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
                             BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = true)
                             true
                         }
    -                } else if (!state.megolmSecretKnown) {
    +                } else if (!info.megolmSecretKnown) {
                         // megolm backup is available but we don't have key
                         // you could try to synchronize to get missing megolm key ?
                         secureBackupCategory.isVisible = true
    
    From 7193db834425a816b2ecd7cd9b13c87941b3fe9a Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Sat, 11 Jul 2020 13:19:43 +0200
    Subject: [PATCH 083/122] Try to improve readability
    
    ---
     .../VectorSettingsSecurityPrivacyFragment.kt  | 73 +++++++++----------
     1 file changed, 34 insertions(+), 39 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    index 271265942e..1dc314be08 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    @@ -129,15 +129,12 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
         }
     
         private fun refresh4SSection(info: SecretsSynchronisationInfo) {
    -        secureBackupCategory.isVisible = false
    -
             // it's a lot of if / else if / else
             // But it's not yet clear how to manage all cases
             if (!info.isCrossSigningEnabled) {
                 // There is not cross signing, so we can remove the section
    +            secureBackupCategory.isVisible = false
             } else {
    -            secureBackupCategory.isVisible = true
    -
                 if (!info.isBackupSetup) {
                     if (info.isCrossSigningEnabled && info.allPrivateKeysKnown) {
                         // You can setup recovery!
    @@ -154,26 +151,40 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
                         // you should synchronize to get gossips
                         secureBackupCategory.isVisible = false
                     }
    -                return
    -            }
    -
    -            // so here we know that 4S is setup
    -            if (info.isCrossSigningTrusted && info.allPrivateKeysKnown) {
    -                // Looks like we have all cross signing secrets and session is trusted
    -                // Let's see if there is a megolm backup
    -                if (!info.megolmBackupAvailable || info.megolmSecretKnown) {
    -                    // Only option here is to create a new backup if you want?
    -                    // aka reset
    -                    secureBackupCategory.isVisible = true
    -                    secureBackupPreference.isVisible = true
    -                    secureBackupPreference.title = getString(R.string.settings_secure_backup_reset)
    -                    secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
    -                        BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = true)
    -                        true
    +            } else {
    +                // so here we know that 4S is setup
    +                if (info.isCrossSigningTrusted && info.allPrivateKeysKnown) {
    +                    // Looks like we have all cross signing secrets and session is trusted
    +                    // Let's see if there is a megolm backup
    +                    if (!info.megolmBackupAvailable || info.megolmSecretKnown) {
    +                        // Only option here is to create a new backup if you want?
    +                        // aka reset
    +                        secureBackupCategory.isVisible = true
    +                        secureBackupPreference.isVisible = true
    +                        secureBackupPreference.title = getString(R.string.settings_secure_backup_reset)
    +                        secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
    +                            BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = true)
    +                            true
    +                        }
    +                    } else if (!info.megolmSecretKnown) {
    +                        // megolm backup is available but we don't have key
    +                        // you could try to synchronize to get missing megolm key ?
    +                        secureBackupCategory.isVisible = true
    +                        secureBackupPreference.isVisible = true
    +                        secureBackupPreference.title = getString(R.string.settings_secure_backup_enter_to_setup)
    +                        secureBackupPreference.isEnabled = true
    +                        secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
    +                            (requireActivity() as? VectorBaseActivity)?.let {
    +                                it.navigator.requestSelfSessionVerification(it)
    +                            }
    +                            true
    +                        }
    +                    } else {
    +                        secureBackupCategory.isVisible = false
                         }
    -                } else if (!info.megolmSecretKnown) {
    -                    // megolm backup is available but we don't have key
    -                    // you could try to synchronize to get missing megolm key ?
    +                } else {
    +                    // there is a backup, but this session is not trusted, or is missing some secrets
    +                    // you should enter passphrase to get them or verify against another session
                         secureBackupCategory.isVisible = true
                         secureBackupPreference.isVisible = true
                         secureBackupPreference.title = getString(R.string.settings_secure_backup_enter_to_setup)
    @@ -184,22 +195,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
                             }
                             true
                         }
    -                } else {
    -                    secureBackupCategory.isVisible = false
    -                }
    -                return
    -            } else {
    -                // there is a backup, but this session is not trusted, or is missing some secrets
    -                // you should enter passphrase to get them or verify against another session
    -                secureBackupCategory.isVisible = true
    -                secureBackupPreference.isVisible = true
    -                secureBackupPreference.title = getString(R.string.settings_secure_backup_enter_to_setup)
    -                secureBackupPreference.isEnabled = true
    -                secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
    -                    (requireActivity() as? VectorBaseActivity)?.let {
    -                        it.navigator.requestSelfSessionVerification(it)
    -                    }
    -                    true
                     }
                 }
             }
    
    From 22959cddb2507c98277d9b57bab30a1400be61fd Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Sat, 11 Jul 2020 13:24:10 +0200
    Subject: [PATCH 084/122] Pref is always visible and enabled
    
    ---
     .../VectorSettingsSecurityPrivacyFragment.kt         | 12 ++----------
     1 file changed, 2 insertions(+), 10 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    index 1dc314be08..d8f8546e2d 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    @@ -43,7 +43,6 @@ import im.vector.riotx.core.intent.ExternalIntentData
     import im.vector.riotx.core.intent.analyseIntent
     import im.vector.riotx.core.intent.getFilenameFromUri
     import im.vector.riotx.core.platform.SimpleTextWatcher
    -import im.vector.riotx.core.platform.VectorBaseActivity
     import im.vector.riotx.core.preference.VectorPreference
     import im.vector.riotx.core.preference.VectorPreferenceCategory
     import im.vector.riotx.core.utils.openFileSelection
    @@ -139,9 +138,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
                     if (info.isCrossSigningEnabled && info.allPrivateKeysKnown) {
                         // You can setup recovery!
                         secureBackupCategory.isVisible = true
    -                    secureBackupPreference.isVisible = true
                         secureBackupPreference.title = getString(R.string.settings_secure_backup_setup)
    -                    secureBackupPreference.isEnabled = true
                         secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
                             BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = false)
                             true
    @@ -160,7 +157,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
                             // Only option here is to create a new backup if you want?
                             // aka reset
                             secureBackupCategory.isVisible = true
    -                        secureBackupPreference.isVisible = true
                             secureBackupPreference.title = getString(R.string.settings_secure_backup_reset)
                             secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
                                 BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = true)
    @@ -170,11 +166,9 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
                             // megolm backup is available but we don't have key
                             // you could try to synchronize to get missing megolm key ?
                             secureBackupCategory.isVisible = true
    -                        secureBackupPreference.isVisible = true
                             secureBackupPreference.title = getString(R.string.settings_secure_backup_enter_to_setup)
    -                        secureBackupPreference.isEnabled = true
                             secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
    -                            (requireActivity() as? VectorBaseActivity)?.let {
    +                            vectorActivity.let {
                                     it.navigator.requestSelfSessionVerification(it)
                                 }
                                 true
    @@ -186,11 +180,9 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
                         // there is a backup, but this session is not trusted, or is missing some secrets
                         // you should enter passphrase to get them or verify against another session
                         secureBackupCategory.isVisible = true
    -                    secureBackupPreference.isVisible = true
                         secureBackupPreference.title = getString(R.string.settings_secure_backup_enter_to_setup)
    -                    secureBackupPreference.isEnabled = true
                         secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
    -                        (requireActivity() as? VectorBaseActivity)?.let {
    +                        vectorActivity.let {
                                 it.navigator.requestSelfSessionVerification(it)
                             }
                             true
    
    From 0412fabbd2e2ab94f3346c467aaf13e1f1bde497 Mon Sep 17 00:00:00 2001
    From: ganfra 
    Date: Sat, 11 Jul 2020 13:24:32 +0200
    Subject: [PATCH 085/122] Clean comment on EventInsertLiveProcessor
    
    ---
     .../android/internal/database/EventInsertLiveObserver.kt      | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/EventInsertLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/EventInsertLiveObserver.kt
    index f0884918c0..2f7ed60bc7 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/EventInsertLiveObserver.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/EventInsertLiveObserver.kt
    @@ -88,8 +88,8 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
                             forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
                     )
                 } catch (e: MXCryptoError) {
    -                Timber.v("Call service: Failed to decrypt event")
    -                // TODO -> we should keep track of this and retry, or aggregation will be broken
    +                Timber.v("Failed to decrypt event")
    +                // TODO -> we should keep track of this and retry, or some processing will never be handled
                 }
             }
         }
    
    From e7804af2f71fa9b450977ee75b983159200d39a7 Mon Sep 17 00:00:00 2001
    From: ganfra 
    Date: Sat, 11 Jul 2020 13:27:49 +0200
    Subject: [PATCH 086/122] EventInsertLiveObserver: change of delete method
     (should be faster)
    
    ---
     .../matrix/android/internal/database/EventInsertLiveObserver.kt | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/EventInsertLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/EventInsertLiveObserver.kt
    index 2f7ed60bc7..98d8806288 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/EventInsertLiveObserver.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/EventInsertLiveObserver.kt
    @@ -72,7 +72,7 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
                             it.process(realm, domainEvent)
                         }
                     }
    -                realm.where(EventInsertEntity::class.java).findAll().deleteAllFromRealm()
    +                realm.delete(EventInsertEntity::class.java)
                 }
             }
         }
    
    From c2cccd8b11cd51bbdcf110b1b31373c5af713aff Mon Sep 17 00:00:00 2001
    From: ganfra 
    Date: Sat, 11 Jul 2020 15:26:54 +0200
    Subject: [PATCH 087/122] Some changes after benoit's review
    
    ---
     .../room/DefaultRoomDirectoryService.kt       |  2 --
     .../home/room/detail/RoomDetailFragment.kt    | 20 +++++++++----------
     2 files changed, 10 insertions(+), 12 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt
    index e89fae647f..288ee603b6 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt
    @@ -24,13 +24,11 @@ import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProt
     import im.vector.matrix.android.api.util.Cancelable
     import im.vector.matrix.android.internal.session.room.directory.GetPublicRoomTask
     import im.vector.matrix.android.internal.session.room.directory.GetThirdPartyProtocolsTask
    -import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
     import im.vector.matrix.android.internal.task.TaskExecutor
     import im.vector.matrix.android.internal.task.configureWith
     import javax.inject.Inject
     
     internal class DefaultRoomDirectoryService @Inject constructor(private val getPublicRoomTask: GetPublicRoomTask,
    -                                                               private val joinRoomTask: JoinRoomTask,
                                                                    private val getThirdPartyProtocolsTask: GetThirdPartyProtocolsTask,
                                                                    private val taskExecutor: TaskExecutor) : RoomDirectoryService {
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    index ba7e356545..62078c3053 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    @@ -69,6 +69,7 @@ import im.vector.matrix.android.api.session.events.model.Event
     import im.vector.matrix.android.api.session.events.model.toModel
     import im.vector.matrix.android.api.session.file.FileService
     import im.vector.matrix.android.api.session.room.model.Membership
    +import im.vector.matrix.android.api.session.room.model.RoomSummary
     import im.vector.matrix.android.api.session.room.model.message.MessageContent
     import im.vector.matrix.android.api.session.room.model.message.MessageFormat
     import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent
    @@ -293,7 +294,6 @@ class RoomDetailFragment @Inject constructor(
             setupJumpToBottomView()
             setupWidgetsBannerView()
     
    -        roomToolbarContentView.isClickable = false
             roomToolbarContentView.debouncedClicks {
                 navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId)
             }
    @@ -854,13 +854,14 @@ class RoomDetailFragment @Inject constructor(
         }
     
         override fun invalidate() = withState(roomDetailViewModel) { state ->
    -        renderRoomSummary(state)
             invalidateOptionsMenu()
             val summary = state.asyncRoomSummary()
    +        renderToolbar(summary, state.typingMessage)
             val inviter = state.asyncInviter()
             if (summary?.membership == Membership.JOIN) {
    -            roomToolbarContentView.isClickable = true
                 roomWidgetsBannerView.render(state.activeRoomWidgets())
    +            jumpToBottomView.count = summary.notificationCount
    +            jumpToBottomView.drawBadge = summary.hasUnreadMessages
                 scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline
                 timelineEventController.update(state)
                 inviteView.visibility = View.GONE
    @@ -881,7 +882,6 @@ class RoomDetailFragment @Inject constructor(
                     notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent))
                 }
             } else if (summary?.membership == Membership.INVITE && inviter != null) {
    -            roomToolbarContentView.isClickable = false
                 inviteView.visibility = View.VISIBLE
                 inviteView.render(inviter, VectorInviteView.Mode.LARGE, state.changeMembershipState)
                 // Intercept click event
    @@ -891,15 +891,15 @@ class RoomDetailFragment @Inject constructor(
             }
         }
     
    -    private fun renderRoomSummary(state: RoomDetailViewState) {
    -        state.asyncRoomSummary()?.let { roomSummary ->
    +    private fun renderToolbar(roomSummary: RoomSummary?, typingMessage: String?) {
    +        if (roomSummary == null) {
    +            roomToolbarContentView.isClickable = false
    +        } else {
    +            roomToolbarContentView.isClickable = roomSummary.membership == Membership.JOIN
                 roomToolbarTitleView.text = roomSummary.displayName
                 avatarRenderer.render(roomSummary.toMatrixItem(), roomToolbarAvatarImageView)
     
    -            renderSubTitle(state.typingMessage, roomSummary.topic)
    -            jumpToBottomView.count = roomSummary.notificationCount
    -            jumpToBottomView.drawBadge = roomSummary.hasUnreadMessages
    -
    +            renderSubTitle(typingMessage, roomSummary.topic)
                 roomToolbarDecorationImageView.let {
                     it.setImageResource(roomSummary.roomEncryptionTrustLevel.toImageRes())
                     it.isVisible = roomSummary.roomEncryptionTrustLevel != null
    
    From c6a5d05ffb33a5bba9a2b8352fdbd6fd2869d3db Mon Sep 17 00:00:00 2001
    From: Valere 
    Date: Sat, 11 Jul 2020 17:29:22 +0200
    Subject: [PATCH 088/122] update change log
    
    ---
     CHANGES.md | 2 ++
     1 file changed, 2 insertions(+)
    
    diff --git a/CHANGES.md b/CHANGES.md
    index 9d124dff66..8a20924d42 100644
    --- a/CHANGES.md
    +++ b/CHANGES.md
    @@ -9,6 +9,8 @@ Improvements 🙌:
      - Creating and listening to EventInsertEntity. (#1634)
      - Handling (almost) properly the groups fetching (#1634)
      - Improve fullscreen media display (#327)
    + - Setup server recovery banner (#1648)
    + - Set up SSSS from security settings (#1567)
     
     Bugfix 🐛:
      - Regression |  Share action menu do not work (#1647)
    
    From 7acbd42a45ef5e2b4c9f7d1aa60616f50f919718 Mon Sep 17 00:00:00 2001
    From: Valere 
    Date: Sat, 11 Jul 2020 18:38:03 +0200
    Subject: [PATCH 089/122] Propose unread tab as a lab setting
    
    ---
     .../riotx/features/home/HomeDetailFragment.kt | 79 +++++++++++++------
     .../features/home/RoomListDisplayMode.kt      |  2 +-
     .../room/list/RoomListDisplayModeFilter.kt    |  4 +-
     .../home/room/list/RoomListFragment.kt        | 16 ++--
     .../features/settings/VectorPreferences.kt    |  5 ++
     .../settings/VectorSettingsLabsFragment.kt    |  4 +
     .../ui/SharedPreferencesUiStateRepository.kt  | 12 ++-
     .../main/res/menu/home_bottom_navigation.xml  | 14 ++--
     vector/src/main/res/values/strings.xml        |  2 +
     .../src/main/res/xml/vector_settings_labs.xml |  6 ++
     10 files changed, 100 insertions(+), 44 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
    index 770b03d345..73b870fd3a 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
    @@ -17,16 +17,13 @@
     package im.vector.riotx.features.home
     
     import android.os.Bundle
    -import android.view.LayoutInflater
     import android.view.View
     import androidx.core.content.ContextCompat
    -import androidx.core.view.forEachIndexed
     import androidx.lifecycle.Observer
     import com.airbnb.mvrx.activityViewModel
     import com.airbnb.mvrx.fragmentViewModel
     import com.airbnb.mvrx.withState
    -import com.google.android.material.bottomnavigation.BottomNavigationItemView
    -import com.google.android.material.bottomnavigation.BottomNavigationMenuView
    +import com.google.android.material.badge.BadgeDrawable
     import im.vector.matrix.android.api.session.group.model.GroupSummary
     import im.vector.matrix.android.api.util.toMatrixItem
     import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
    @@ -44,10 +41,11 @@ import im.vector.riotx.features.call.VectorCallActivity
     import im.vector.riotx.features.call.WebRtcPeerConnectionManager
     import im.vector.riotx.features.home.room.list.RoomListFragment
     import im.vector.riotx.features.home.room.list.RoomListParams
    -import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView
     import im.vector.riotx.features.popup.PopupAlertManager
     import im.vector.riotx.features.popup.VerificationVectorAlert
    +import im.vector.riotx.features.settings.VectorPreferences
     import im.vector.riotx.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
    +import im.vector.riotx.features.themes.ThemeUtils
     import im.vector.riotx.features.workers.signout.BannerState
     import im.vector.riotx.features.workers.signout.ServerBackupStatusViewModel
     import im.vector.riotx.features.workers.signout.ServerBackupStatusViewState
    @@ -55,20 +53,19 @@ import kotlinx.android.synthetic.main.fragment_home_detail.*
     import timber.log.Timber
     import javax.inject.Inject
     
    -private const val INDEX_CATCHUP = 0
    -private const val INDEX_PEOPLE = 1
    -private const val INDEX_ROOMS = 2
    +private const val INDEX_PEOPLE = 0
    +private const val INDEX_ROOMS = 1
    +private const val INDEX_CATCHUP = 2
     
     class HomeDetailFragment @Inject constructor(
             val homeDetailViewModelFactory: HomeDetailViewModel.Factory,
             private val serverBackupStatusViewModelFactory: ServerBackupStatusViewModel.Factory,
             private val avatarRenderer: AvatarRenderer,
             private val alertManager: PopupAlertManager,
    -        private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager
    +        private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
    +        private val vectorPreferences: VectorPreferences
     ) : VectorBaseFragment(), KeysBackupBanner.Delegate, ActiveCallView.Callback, ServerBackupStatusViewModel.Factory {
     
    -    private val unreadCounterBadgeViews = arrayListOf()
    -
         private val viewModel: HomeDetailViewModel by fragmentViewModel()
         private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel()
         private val serverBackupStatusViewModel: ServerBackupStatusViewModel by activityViewModel()
    @@ -128,6 +125,25 @@ class HomeDetailFragment @Inject constructor(
                     })
         }
     
    +    override fun onResume() {
    +        super.onResume()
    +        // update notification tab if needed
    +        checkNotificationTabStatus()
    +    }
    +
    +    private fun checkNotificationTabStatus() {
    +        val wasVisible = bottomNavigationView.menu.findItem(R.id.bottom_action_notification).isVisible
    +        bottomNavigationView.menu.findItem(R.id.bottom_action_notification).isVisible = vectorPreferences.labAddNotificationTab()
    +        if (wasVisible && !vectorPreferences.labAddNotificationTab()) {
    +            // As we hide it check if it's not the current item!
    +            withState(viewModel) {
    +                if (it.displayMode.toMenuId() == R.id.bottom_action_notification) {
    +                    viewModel.handle(HomeDetailAction.SwitchDisplayMode(RoomListDisplayMode.PEOPLE))
    +                }
    +            }
    +        }
    +    }
    +
         private fun promptForNewUnknownDevices(uid: String, state: UnknownDevicesState, newest: DeviceInfo) {
             val user = state.myMatrixItem
             alertManager.postVectorAlert(
    @@ -226,24 +242,27 @@ class HomeDetailFragment @Inject constructor(
         }
     
         private fun setupBottomNavigationView() {
    +        bottomNavigationView.menu.findItem(R.id.bottom_action_notification).isVisible = vectorPreferences.labAddNotificationTab()
             bottomNavigationView.setOnNavigationItemSelectedListener {
                 val displayMode = when (it.itemId) {
                     R.id.bottom_action_people -> RoomListDisplayMode.PEOPLE
                     R.id.bottom_action_rooms  -> RoomListDisplayMode.ROOMS
    -                else                      -> RoomListDisplayMode.HOME
    +                else                      -> RoomListDisplayMode.NOTIFICATIONS
                 }
                 viewModel.handle(HomeDetailAction.SwitchDisplayMode(displayMode))
                 true
             }
     
    -        val menuView = bottomNavigationView.getChildAt(0) as BottomNavigationMenuView
    -        menuView.forEachIndexed { index, view ->
    -            val itemView = view as BottomNavigationItemView
    -            val badgeLayout = LayoutInflater.from(requireContext()).inflate(R.layout.vector_home_badge_unread_layout, menuView, false)
    -            val unreadCounterBadgeView: UnreadCounterBadgeView = badgeLayout.findViewById(R.id.actionUnreadCounterBadgeView)
    -            itemView.addView(badgeLayout)
    -            unreadCounterBadgeViews.add(index, unreadCounterBadgeView)
    -        }
    +//        val menuView = bottomNavigationView.getChildAt(0) as BottomNavigationMenuView
    +
    +//        bottomNavigationView.getOrCreateBadge()
    +//        menuView.forEachIndexed { index, view ->
    +//            val itemView = view as BottomNavigationItemView
    +//            val badgeLayout = LayoutInflater.from(requireContext()).inflate(R.layout.vector_home_badge_unread_layout, menuView, false)
    +//            val unreadCounterBadgeView: UnreadCounterBadgeView = badgeLayout.findViewById(R.id.actionUnreadCounterBadgeView)
    +//            itemView.addView(badgeLayout)
    +//            unreadCounterBadgeViews.add(index, unreadCounterBadgeView)
    +//        }
         }
     
         private fun switchDisplayMode(displayMode: RoomListDisplayMode) {
    @@ -283,16 +302,28 @@ class HomeDetailFragment @Inject constructor(
     
         override fun invalidate() = withState(viewModel) {
             Timber.v(it.toString())
    -        unreadCounterBadgeViews[INDEX_CATCHUP].render(UnreadCounterBadgeView.State(it.notificationCountCatchup, it.notificationHighlightCatchup))
    -        unreadCounterBadgeViews[INDEX_PEOPLE].render(UnreadCounterBadgeView.State(it.notificationCountPeople, it.notificationHighlightPeople))
    -        unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms))
    +        bottomNavigationView.getOrCreateBadge(R.id.bottom_action_people).render(it.notificationCountPeople, it.notificationHighlightPeople)
    +        bottomNavigationView.getOrCreateBadge(R.id.bottom_action_rooms).render(it.notificationCountRooms, it.notificationHighlightRooms)
    +        bottomNavigationView.getOrCreateBadge(R.id.bottom_action_notification).render(it.notificationCountCatchup, it.notificationHighlightCatchup)
             syncStateView.render(it.syncState)
         }
     
    +    private fun BadgeDrawable.render(count: Int, highlight: Boolean) {
    +        isVisible = count > 0
    +        number = count
    +        maxCharacterCount = 3
    +        badgeTextColor = ContextCompat.getColor(requireContext(), R.color.white)
    +        backgroundColor = if (highlight) {
    +            ContextCompat.getColor(requireContext(), R.color.riotx_notice)
    +        } else {
    +            ThemeUtils.getColor(requireContext(), R.attr.riotx_unread_room_badge)
    +        }
    +    }
    +
         private fun RoomListDisplayMode.toMenuId() = when (this) {
             RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people
             RoomListDisplayMode.ROOMS  -> R.id.bottom_action_rooms
    -        else                       -> R.id.bottom_action_home
    +        else                       -> R.id.bottom_action_notification
         }
     
         override fun onTapToReturnToCall() {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/RoomListDisplayMode.kt b/vector/src/main/java/im/vector/riotx/features/home/RoomListDisplayMode.kt
    index 6d7f49750d..365eda74a8 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/RoomListDisplayMode.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/RoomListDisplayMode.kt
    @@ -20,7 +20,7 @@ import androidx.annotation.StringRes
     import im.vector.riotx.R
     
     enum class RoomListDisplayMode(@StringRes val titleRes: Int) {
    -        HOME(R.string.bottom_action_home),
    +        NOTIFICATIONS(R.string.bottom_action_notification),
             PEOPLE(R.string.bottom_action_people_x),
             ROOMS(R.string.bottom_action_rooms),
             FILTERED(/* Not used */ 0)
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt
    index 3045987d01..dd75deb8ee 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt
    @@ -28,9 +28,9 @@ class RoomListDisplayModeFilter(private val displayMode: RoomListDisplayMode) :
                 return false
             }
             return when (displayMode) {
    -            RoomListDisplayMode.HOME     ->
    +            RoomListDisplayMode.NOTIFICATIONS ->
                     roomSummary.notificationCount > 0 || roomSummary.membership == Membership.INVITE || roomSummary.userDrafts.isNotEmpty()
    -            RoomListDisplayMode.PEOPLE   -> roomSummary.isDirect && roomSummary.membership.isActive()
    +            RoomListDisplayMode.PEOPLE        -> roomSummary.isDirect && roomSummary.membership.isActive()
                 RoomListDisplayMode.ROOMS    -> !roomSummary.isDirect && roomSummary.membership.isActive()
                 RoomListDisplayMode.FILTERED -> roomSummary.membership == Membership.JOIN
             }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt
    index b31117f18f..2858097e24 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt
    @@ -138,8 +138,8 @@ class RoomListFragment @Inject constructor(
     
         private fun setupCreateRoomButton() {
             when (roomListParams.displayMode) {
    -            RoomListDisplayMode.HOME   -> createChatFabMenu.isVisible = true
    -            RoomListDisplayMode.PEOPLE -> createChatRoomButton.isVisible = true
    +            RoomListDisplayMode.NOTIFICATIONS -> createChatFabMenu.isVisible = true
    +            RoomListDisplayMode.PEOPLE        -> createChatRoomButton.isVisible = true
                 RoomListDisplayMode.ROOMS  -> createGroupRoomButton.isVisible = true
                 else                       -> Unit // No button in this mode
             }
    @@ -164,8 +164,8 @@ class RoomListFragment @Inject constructor(
                                 RecyclerView.SCROLL_STATE_DRAGGING,
                                 RecyclerView.SCROLL_STATE_SETTLING -> {
                                     when (roomListParams.displayMode) {
    -                                    RoomListDisplayMode.HOME   -> createChatFabMenu.hide()
    -                                    RoomListDisplayMode.PEOPLE -> createChatRoomButton.hide()
    +                                    RoomListDisplayMode.NOTIFICATIONS -> createChatFabMenu.hide()
    +                                    RoomListDisplayMode.PEOPLE        -> createChatRoomButton.hide()
                                         RoomListDisplayMode.ROOMS  -> createGroupRoomButton.hide()
                                         else                       -> Unit
                                     }
    @@ -207,8 +207,8 @@ class RoomListFragment @Inject constructor(
         private val showFabRunnable = Runnable {
             if (isAdded) {
                 when (roomListParams.displayMode) {
    -                RoomListDisplayMode.HOME   -> createChatFabMenu.show()
    -                RoomListDisplayMode.PEOPLE -> createChatRoomButton.show()
    +                RoomListDisplayMode.NOTIFICATIONS -> createChatFabMenu.show()
    +                RoomListDisplayMode.PEOPLE        -> createChatRoomButton.show()
                     RoomListDisplayMode.ROOMS  -> createGroupRoomButton.show()
                     else                       -> Unit
                 }
    @@ -258,7 +258,7 @@ class RoomListFragment @Inject constructor(
             roomController.update(state)
             // Mark all as read menu
             when (roomListParams.displayMode) {
    -            RoomListDisplayMode.HOME,
    +            RoomListDisplayMode.NOTIFICATIONS,
                 RoomListDisplayMode.PEOPLE,
                 RoomListDisplayMode.ROOMS -> {
                     val newValue = state.hasUnread
    @@ -288,7 +288,7 @@ class RoomListFragment @Inject constructor(
                     }
                     .isNullOrEmpty()
             val emptyState = when (roomListParams.displayMode) {
    -            RoomListDisplayMode.HOME   -> {
    +            RoomListDisplayMode.NOTIFICATIONS -> {
                     if (hasNoRoom) {
                         StateView.State.Empty(
                                 getString(R.string.room_list_catchup_welcome_title),
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
    index 9d08a9f62f..50f4d516bf 100755
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
    @@ -147,6 +147,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
             private const val SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY"
             // SETTINGS_LABS_HIDE_TECHNICAL_E2E_ERRORS
             private const val SETTINGS_LABS_MERGE_E2E_ERRORS = "SETTINGS_LABS_MERGE_E2E_ERRORS"
    +        const val SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB = "SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB"
     
             // analytics
             const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY"
    @@ -276,6 +277,10 @@ class VectorPreferences @Inject constructor(private val context: Context) {
             return developerMode() && defaultPrefs.getBoolean(SETTINGS_LABS_ALLOW_EXTENDED_LOGS, false)
         }
     
    +    fun labAddNotificationTab(): Boolean {
    +        return defaultPrefs.getBoolean(SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB, false)
    +    }
    +
         fun failFast(): Boolean {
             return BuildConfig.DEBUG || (developerMode() && defaultPrefs.getBoolean(SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY, false))
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsLabsFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsLabsFragment.kt
    index eeda0167a3..1ffd80a591 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsLabsFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsLabsFragment.kt
    @@ -34,6 +34,10 @@ class VectorSettingsLabsFragment @Inject constructor(
                 it.isChecked = vectorPreferences.labAllowedExtendedLogging()
             }
     
    +        findPreference(VectorPreferences.SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB)?.let {
    +            it.isChecked = vectorPreferences.labAddNotificationTab()
    +        }
    +
     //        val useCryptoPref = findPreference(VectorPreferences.SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY) as SwitchPreference
     //        val cryptoIsEnabledPref = findPreference(VectorPreferences.SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY)
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/ui/SharedPreferencesUiStateRepository.kt b/vector/src/main/java/im/vector/riotx/features/ui/SharedPreferencesUiStateRepository.kt
    index d1a4315cc9..ec1f8e5131 100644
    --- a/vector/src/main/java/im/vector/riotx/features/ui/SharedPreferencesUiStateRepository.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/ui/SharedPreferencesUiStateRepository.kt
    @@ -19,12 +19,16 @@ package im.vector.riotx.features.ui
     import android.content.SharedPreferences
     import androidx.core.content.edit
     import im.vector.riotx.features.home.RoomListDisplayMode
    +import im.vector.riotx.features.settings.VectorPreferences
     import javax.inject.Inject
     
     /**
      * This class is used to persist UI state across application restart
      */
    -class SharedPreferencesUiStateRepository @Inject constructor(private val sharedPreferences: SharedPreferences) : UiStateRepository {
    +class SharedPreferencesUiStateRepository @Inject constructor(
    +        private val sharedPreferences: SharedPreferences,
    +        private val vectorPreferences: VectorPreferences
    +) : UiStateRepository {
     
         override fun reset() {
             sharedPreferences.edit {
    @@ -36,7 +40,11 @@ class SharedPreferencesUiStateRepository @Inject constructor(private val sharedP
             return when (sharedPreferences.getInt(KEY_DISPLAY_MODE, VALUE_DISPLAY_MODE_CATCHUP)) {
                 VALUE_DISPLAY_MODE_PEOPLE -> RoomListDisplayMode.PEOPLE
                 VALUE_DISPLAY_MODE_ROOMS  -> RoomListDisplayMode.ROOMS
    -            else                      -> RoomListDisplayMode.PEOPLE // RoomListDisplayMode.HOME
    +            else                      -> if (vectorPreferences.labAddNotificationTab()) {
    +                RoomListDisplayMode.NOTIFICATIONS
    +            } else {
    +                RoomListDisplayMode.PEOPLE
    +            }
             }
         }
     
    diff --git a/vector/src/main/res/menu/home_bottom_navigation.xml b/vector/src/main/res/menu/home_bottom_navigation.xml
    index aaf3203fe9..805e93744a 100644
    --- a/vector/src/main/res/menu/home_bottom_navigation.xml
    +++ b/vector/src/main/res/menu/home_bottom_navigation.xml
    @@ -1,13 +1,6 @@
     
     
     
    -    
    -
         
     
    +    
    +
     
    diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
    index 37a849b3fd..32de32e094 100644
    --- a/vector/src/main/res/values/strings.xml
    +++ b/vector/src/main/res/values/strings.xml
    @@ -130,6 +130,7 @@
     
         
         Home
    +    Notifications
         Favourites
         People
         Rooms
    @@ -1760,6 +1761,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
     
         Enable swipe to reply in timeline
         Merge failed to decrypt message in timeline
    +    Add a dedicated tab for unread notifications on main screen.
     
         Link copied to clipboard
     
    diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml
    index 9917bb0feb..2c52b2198e 100644
    --- a/vector/src/main/res/xml/vector_settings_labs.xml
    +++ b/vector/src/main/res/xml/vector_settings_labs.xml
    @@ -44,6 +44,12 @@
             android:defaultValue="false"
             android:key="SETTINGS_LABS_MERGE_E2E_ERRORS"
             android:title="@string/labs_merge_e2e_in_timeline" />
    +
    +
    +    
         
     
     
    \ No newline at end of file
    
    From 246f6bb0d086cf431ff091084abd6903957d5895 Mon Sep 17 00:00:00 2001
    From: Valere 
    Date: Sat, 11 Jul 2020 18:41:08 +0200
    Subject: [PATCH 090/122] update change log
    
    ---
     CHANGES.md | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/CHANGES.md b/CHANGES.md
    index 8a20924d42..72464ccf64 100644
    --- a/CHANGES.md
    +++ b/CHANGES.md
    @@ -11,6 +11,7 @@ Improvements 🙌:
      - Improve fullscreen media display (#327)
      - Setup server recovery banner (#1648)
      - Set up SSSS from security settings (#1567)
    + - New lab setting to add 'unread notifications' tab to main screen
     
     Bugfix 🐛:
      - Regression |  Share action menu do not work (#1647)
    
    From 37378ca5a604879d46f6674f5610574c4c39b3c0 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Sat, 11 Jul 2020 21:57:45 +0200
    Subject: [PATCH 091/122] typo
    
    ---
     .../im/vector/matrix/android/api/session/room/RoomService.kt    | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt
    index 813d7d98b2..3319cecfef 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt
    @@ -106,7 +106,7 @@ interface RoomService {
                              callback: MatrixCallback>): Cancelable
     
         /**
    -     * Return a live data of all local changes membership who happened since the session has been opened.
    +     * Return a live data of all local changes membership that happened since the session has been opened.
          * It allows you to track this in your client to known what is currently being processed by the SDK.
          * It won't know anything about change being done in other client.
          * Keys are roomId or roomAlias, depending of what you used as parameter for the join/leave action
    
    From 70e90d854279d8f3be3ecaad29ed1693891f7c2d Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Tue, 7 Jul 2020 17:10:12 +0200
    Subject: [PATCH 092/122] Render third party invite event (#548)
    
    ---
     CHANGES.md                                    |  1 +
     .../room/model/RoomThirdPartyInviteContent.kt | 67 +++++++++++++++++++
     .../timeline/factory/TimelineItemFactory.kt   |  4 +-
     .../timeline/format/NoticeEventFormatter.kt   | 28 ++++++++
     4 files changed, 98 insertions(+), 2 deletions(-)
     create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomThirdPartyInviteContent.kt
    
    diff --git a/CHANGES.md b/CHANGES.md
    index 94af9346eb..32ab57fda8 100644
    --- a/CHANGES.md
    +++ b/CHANGES.md
    @@ -12,6 +12,7 @@ Improvements 🙌:
      - Setup server recovery banner (#1648)
      - Set up SSSS from security settings (#1567)
      - New lab setting to add 'unread notifications' tab to main screen
    + - Render third party invite event (#548)
     
     Bugfix 🐛:
      - Integration Manager: Wrong URL to review terms if URL in config contains path (#1606)
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomThirdPartyInviteContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomThirdPartyInviteContent.kt
    new file mode 100644
    index 0000000000..2b8daa0c5b
    --- /dev/null
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomThirdPartyInviteContent.kt
    @@ -0,0 +1,67 @@
    +/*
    + * Copyright 2019 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.api.session.room.model
    +
    +import com.squareup.moshi.Json
    +import com.squareup.moshi.JsonClass
    +
    +/**
    + * Class representing the EventType.STATE_ROOM_THIRD_PARTY_INVITE state event content
    + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#m-room-third-party-invite
    + */
    +@JsonClass(generateAdapter = true)
    +data class RoomThirdPartyInviteContent(
    +        /**
    +         * Required. A user-readable string which represents the user who has been invited.
    +         * This should not contain the user's third party ID, as otherwise when the invite
    +         * is accepted it would leak the association between the matrix ID and the third party ID.
    +         */
    +        @Json(name = "display_name") val displayName: String,
    +
    +        /**
    +         * Required. A URL which can be fetched, with querystring public_key=public_key, to validate
    +         * whether the key has been revoked. The URL must return a JSON object containing a boolean property named 'valid'.
    +         */
    +        @Json(name = "key_validity_url") val keyValidityUrl: String,
    +
    +        /**
    +         * Required. A base64-encoded ed25519 key with which token must be signed (though a signature from any entry in
    +         * public_keys is also sufficient). This exists for backwards compatibility.
    +         */
    +        @Json(name = "public_key") val publicKey: String,
    +
    +        /**
    +         * Keys with which the token may be signed.
    +         */
    +        @Json(name = "public_keys") val publicKeys: List = emptyList()
    +)
    +
    +@JsonClass(generateAdapter = true)
    +data class PublicKeys(
    +        /**
    +         * An optional URL which can be fetched, with querystring public_key=public_key, to validate whether the key
    +         * has been revoked. The URL must return a JSON object containing a boolean property named 'valid'. If this URL
    +         * is absent, the key must be considered valid indefinitely.
    +         */
    +        @Json(name = "key_validity_url") val keyValidityUrl: String? = null,
    +
    +        /**
    +         * Required. A base-64 encoded ed25519 key with which token may be signed.
    +         */
    +        @Json(name = "public_key") val publicKey: String
    +)
    +
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
    index 22fd4eb5ec..72da87415c 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
    @@ -50,6 +50,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
                     EventType.STATE_ROOM_TOPIC,
                     EventType.STATE_ROOM_AVATAR,
                     EventType.STATE_ROOM_MEMBER,
    +                EventType.STATE_ROOM_THIRD_PARTY_INVITE,
                     EventType.STATE_ROOM_ALIASES,
                     EventType.STATE_ROOM_CANONICAL_ALIAS,
                     EventType.STATE_ROOM_JOIN_RULES,
    @@ -96,8 +97,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
                         verificationConclusionItemFactory.create(event, highlight, callback)
                     }
     
    -                // Unhandled event types (yet)
    -                EventType.STATE_ROOM_THIRD_PARTY_INVITE -> defaultItemFactory.create(event, highlight, callback)
    +                // Unhandled event types
                     else                                    -> {
                         // Should only happen when shouldShowHiddenEvents() settings is ON
                         Timber.v("Type ${event.root.getClearType()} not handled")
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
    index c2f683d5a5..032ad4fb62 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
    @@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.room.model.RoomJoinRules
     import im.vector.matrix.android.api.session.room.model.RoomJoinRulesContent
     import im.vector.matrix.android.api.session.room.model.RoomMemberContent
     import im.vector.matrix.android.api.session.room.model.RoomNameContent
    +import im.vector.matrix.android.api.session.room.model.RoomThirdPartyInviteContent
     import im.vector.matrix.android.api.session.room.model.RoomTopicContent
     import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
     import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent
    @@ -63,6 +64,7 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
                 EventType.STATE_ROOM_TOPIC              -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
                 EventType.STATE_ROOM_AVATAR             -> formatRoomAvatarEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
                 EventType.STATE_ROOM_MEMBER             -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
    +            EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
                 EventType.STATE_ROOM_ALIASES            -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
                 EventType.STATE_ROOM_CANONICAL_ALIAS    -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
                 EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
    @@ -156,6 +158,7 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
                 EventType.STATE_ROOM_TOPIC              -> formatRoomTopicEvent(event, senderName)
                 EventType.STATE_ROOM_AVATAR             -> formatRoomAvatarEvent(event, senderName)
                 EventType.STATE_ROOM_MEMBER             -> formatRoomMemberEvent(event, senderName)
    +            EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(event, senderName)
                 EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName)
                 EventType.CALL_INVITE,
                 EventType.CALL_HANGUP,
    @@ -254,6 +257,31 @@ class NoticeEventFormatter @Inject constructor(private val activeSessionDataSour
             }
         }
     
    +    private fun formatRoomThirdPartyInvite(event: Event, senderName: String?): CharSequence? {
    +        val content = event.getClearContent().toModel()
    +        val prevContent = event.resolvedPrevContent()?.toModel()
    +
    +        return when {
    +            prevContent != null -> {
    +                // Revoke case
    +                if (event.isSentByCurrentUser()) {
    +                    sp.getString(R.string.notice_room_third_party_revoked_invite_by_you, prevContent.displayName)
    +                } else {
    +                    sp.getString(R.string.notice_room_third_party_revoked_invite, senderName, prevContent.displayName)
    +                }
    +            }
    +            content != null     -> {
    +                // Invitation case
    +                if (event.isSentByCurrentUser()) {
    +                    sp.getString(R.string.notice_room_third_party_invite_by_you, content.displayName)
    +                } else {
    +                    sp.getString(R.string.notice_room_third_party_invite, senderName, content.displayName)
    +                }
    +            }
    +            else                -> null
    +        }
    +    }
    +
         private fun formatCallEvent(type: String, event: Event, senderName: String?): CharSequence? {
             return when (type) {
                 EventType.CALL_INVITE     -> {
    
    From ab1d652f1725d6c5d116c51de48b13e9e008094e Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Tue, 7 Jul 2020 19:52:04 +0200
    Subject: [PATCH 093/122] Invite by email (msisdn not working), command line
     (#548)
    
    ---
     .../session/room/members/MembershipService.kt |  7 ++
     .../identity/DefaultIdentityService.kt        | 14 +---
     .../session/identity/EnsureIdentityToken.kt   | 59 +++++++++++++++++
     .../session/identity/IdentityModule.kt        |  3 +
     .../android/internal/session/room/RoomAPI.kt  |  9 +++
     .../internal/session/room/RoomModule.kt       |  5 ++
     .../membership/DefaultMembershipService.kt    | 12 ++++
     .../membership/threepid/InviteThreePidTask.kt | 65 +++++++++++++++++++
     .../membership/threepid/ThreePidInviteBody.kt | 41 ++++++++++++
     .../riotx/core/extensions/BasicExtensions.kt  | 15 +++++
     .../riotx/features/command/CommandParser.kt   | 30 ++++++---
     .../riotx/features/command/ParsedCommand.kt   |  3 +
     .../home/room/detail/RoomDetailFragment.kt    |  2 +-
     .../home/room/detail/RoomDetailViewModel.kt   | 10 +++
     14 files changed, 253 insertions(+), 22 deletions(-)
     create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/EnsureIdentityToken.kt
     create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/threepid/InviteThreePidTask.kt
     create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/threepid/ThreePidInviteBody.kt
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt
    index f011d317cd..bb74b5afa5 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt
    @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room.members
     
     import androidx.lifecycle.LiveData
     import im.vector.matrix.android.api.MatrixCallback
    +import im.vector.matrix.android.api.session.identity.ThreePid
     import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
     import im.vector.matrix.android.api.util.Cancelable
     
    @@ -63,6 +64,12 @@ interface MembershipService {
                    reason: String? = null,
                    callback: MatrixCallback): Cancelable
     
    +    /**
    +     * Invite a user with email or phone number in the room
    +     */
    +    fun invite3pid(threePid: ThreePid,
    +                   callback: MatrixCallback): Cancelable
    +
         /**
          * Ban a user from the room
          */
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt
    index 3f10bf791c..13c97599f7 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt
    @@ -62,6 +62,7 @@ import javax.net.ssl.HttpsURLConnection
     @SessionScope
     internal class DefaultIdentityService @Inject constructor(
             private val identityStore: IdentityStore,
    +        private val ensureIdentityTokenTask: EnsureIdentityTokenTask,
             private val getOpenIdTokenTask: GetOpenIdTokenTask,
             private val identityBulkLookupTask: IdentityBulkLookupTask,
             private val identityRegisterTask: IdentityRegisterTask,
    @@ -278,7 +279,7 @@ internal class DefaultIdentityService @Inject constructor(
         }
     
         private suspend fun lookUpInternal(canRetry: Boolean, threePids: List): List {
    -        ensureToken()
    +        ensureIdentityTokenTask.execute(Unit)
     
             return try {
                 identityBulkLookupTask.execute(IdentityBulkLookupTask.Params(threePids))
    @@ -295,17 +296,6 @@ internal class DefaultIdentityService @Inject constructor(
             }
         }
     
    -    private suspend fun ensureToken() {
    -        val identityData = identityStore.getIdentityData() ?: throw IdentityServiceError.NoIdentityServerConfigured
    -        val url = identityData.identityServerUrl ?: throw IdentityServiceError.NoIdentityServerConfigured
    -
    -        if (identityData.token == null) {
    -            // Try to get a token
    -            val token = getNewIdentityServerToken(url)
    -            identityStore.setToken(token)
    -        }
    -    }
    -
         private suspend fun getNewIdentityServerToken(url: String): String {
             val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java)
     
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/EnsureIdentityToken.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/EnsureIdentityToken.kt
    new file mode 100644
    index 0000000000..e727cd69bc
    --- /dev/null
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/EnsureIdentityToken.kt
    @@ -0,0 +1,59 @@
    +/*
    + * Copyright (c) 2020 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.session.identity
    +
    +import dagger.Lazy
    +import im.vector.matrix.android.api.session.identity.IdentityServiceError
    +import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate
    +import im.vector.matrix.android.internal.network.RetrofitFactory
    +import im.vector.matrix.android.internal.session.identity.data.IdentityStore
    +import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask
    +import im.vector.matrix.android.internal.task.Task
    +import okhttp3.OkHttpClient
    +import javax.inject.Inject
    +
    +internal interface EnsureIdentityTokenTask : Task
    +
    +internal class DefaultEnsureIdentityTokenTask @Inject constructor(
    +        private val identityStore: IdentityStore,
    +        private val retrofitFactory: RetrofitFactory,
    +        @UnauthenticatedWithCertificate
    +        private val unauthenticatedOkHttpClient: Lazy,
    +        private val getOpenIdTokenTask: GetOpenIdTokenTask,
    +        private val identityRegisterTask: IdentityRegisterTask
    +) : EnsureIdentityTokenTask {
    +
    +    override suspend fun execute(params: Unit) {
    +        val identityData = identityStore.getIdentityData() ?: throw IdentityServiceError.NoIdentityServerConfigured
    +        val url = identityData.identityServerUrl ?: throw IdentityServiceError.NoIdentityServerConfigured
    +
    +        if (identityData.token == null) {
    +            // Try to get a token
    +            val token = getNewIdentityServerToken(url)
    +            identityStore.setToken(token)
    +        }
    +    }
    +
    +    private suspend fun getNewIdentityServerToken(url: String): String {
    +        val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java)
    +
    +        val openIdToken = getOpenIdTokenTask.execute(Unit)
    +        val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken))
    +
    +        return token.token
    +    }
    +}
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityModule.kt
    index 9f902f79f1..79160b8c59 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityModule.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityModule.kt
    @@ -78,6 +78,9 @@ internal abstract class IdentityModule {
         @Binds
         abstract fun bindIdentityStore(store: RealmIdentityStore): IdentityStore
     
    +    @Binds
    +    abstract fun bindEnsureIdentityTokenTask(task: DefaultEnsureIdentityTokenTask): EnsureIdentityTokenTask
    +
         @Binds
         abstract fun bindIdentityPingTask(task: DefaultIdentityPingTask): IdentityPingTask
     
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt
    index 59fc0efbc0..e00a94297a 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt
    @@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.session.room.alias.RoomAliasDescription
     import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse
     import im.vector.matrix.android.internal.session.room.membership.admin.UserIdAndReason
     import im.vector.matrix.android.internal.session.room.membership.joining.InviteBody
    +import im.vector.matrix.android.internal.session.room.membership.threepid.ThreePidInviteBody
     import im.vector.matrix.android.internal.session.room.relation.RelationsResponse
     import im.vector.matrix.android.internal.session.room.reporting.ReportContentBody
     import im.vector.matrix.android.internal.session.room.send.SendResponse
    @@ -170,6 +171,14 @@ internal interface RoomAPI {
         @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite")
         fun invite(@Path("roomId") roomId: String, @Body body: InviteBody): Call
     
    +    /**
    +     * Invite a user to a room, using a ThreePid
    +     * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#id101
    +     * @param roomId Required. The room identifier (not alias) to which to invite the user.
    +     */
    +    @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite")
    +    fun invite3pid(@Path("roomId") roomId: String, @Body body: ThreePidInviteBody): Call
    +
         /**
          * Send a generic state events
          *
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt
    index 7fa9c1526a..3eb5427b70 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt
    @@ -44,6 +44,8 @@ import im.vector.matrix.android.internal.session.room.membership.joining.InviteT
     import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
     import im.vector.matrix.android.internal.session.room.membership.leaving.DefaultLeaveRoomTask
     import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask
    +import im.vector.matrix.android.internal.session.room.membership.threepid.DefaultInviteThreePidTask
    +import im.vector.matrix.android.internal.session.room.membership.threepid.InviteThreePidTask
     import im.vector.matrix.android.internal.session.room.read.DefaultMarkAllRoomsReadTask
     import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
     import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
    @@ -139,6 +141,9 @@ internal abstract class RoomModule {
         @Binds
         abstract fun bindInviteTask(task: DefaultInviteTask): InviteTask
     
    +    @Binds
    +    abstract fun bindInviteThreePidTask(task: DefaultInviteThreePidTask): InviteThreePidTask
    +
         @Binds
         abstract fun bindJoinRoomTask(task: DefaultJoinRoomTask): JoinRoomTask
     
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt
    index 8467e8b46c..f413f5c9c0 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt
    @@ -21,6 +21,7 @@ import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
     import com.zhuinden.monarchy.Monarchy
     import im.vector.matrix.android.api.MatrixCallback
    +import im.vector.matrix.android.api.session.identity.ThreePid
     import im.vector.matrix.android.api.session.room.members.MembershipService
     import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
     import im.vector.matrix.android.api.session.room.model.Membership
    @@ -36,6 +37,7 @@ import im.vector.matrix.android.internal.session.room.membership.admin.Membershi
     import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask
     import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
     import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask
    +import im.vector.matrix.android.internal.session.room.membership.threepid.InviteThreePidTask
     import im.vector.matrix.android.internal.task.TaskExecutor
     import im.vector.matrix.android.internal.task.configureWith
     import im.vector.matrix.android.internal.util.fetchCopied
    @@ -48,6 +50,7 @@ internal class DefaultMembershipService @AssistedInject constructor(
             private val taskExecutor: TaskExecutor,
             private val loadRoomMembersTask: LoadRoomMembersTask,
             private val inviteTask: InviteTask,
    +        private val inviteThreePidTask: InviteThreePidTask,
             private val joinTask: JoinRoomTask,
             private val leaveRoomTask: LeaveRoomTask,
             private val membershipAdminTask: MembershipAdminTask,
    @@ -152,6 +155,15 @@ internal class DefaultMembershipService @AssistedInject constructor(
                     .executeBy(taskExecutor)
         }
     
    +    override fun invite3pid(threePid: ThreePid, callback: MatrixCallback): Cancelable {
    +        val params = InviteThreePidTask.Params(roomId, threePid)
    +        return inviteThreePidTask
    +                .configureWith(params) {
    +                    this.callback = callback
    +                }
    +                .executeBy(taskExecutor)
    +    }
    +
         override fun join(reason: String?, viaServers: List, callback: MatrixCallback): Cancelable {
             val params = JoinRoomTask.Params(roomId, reason, viaServers)
             return joinTask
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/threepid/InviteThreePidTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/threepid/InviteThreePidTask.kt
    new file mode 100644
    index 0000000000..25fe7b4888
    --- /dev/null
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/threepid/InviteThreePidTask.kt
    @@ -0,0 +1,65 @@
    +/*
    + * Copyright 2019 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.session.room.membership.threepid
    +
    +import im.vector.matrix.android.api.session.identity.IdentityServiceError
    +import im.vector.matrix.android.api.session.identity.ThreePid
    +import im.vector.matrix.android.api.session.identity.toMedium
    +import im.vector.matrix.android.internal.di.AuthenticatedIdentity
    +import im.vector.matrix.android.internal.network.executeRequest
    +import im.vector.matrix.android.internal.network.token.AccessTokenProvider
    +import im.vector.matrix.android.internal.session.identity.EnsureIdentityTokenTask
    +import im.vector.matrix.android.internal.session.identity.data.IdentityStore
    +import im.vector.matrix.android.internal.session.identity.data.getIdentityServerUrlWithoutProtocol
    +import im.vector.matrix.android.internal.session.room.RoomAPI
    +import im.vector.matrix.android.internal.task.Task
    +import org.greenrobot.eventbus.EventBus
    +import javax.inject.Inject
    +
    +internal interface InviteThreePidTask : Task {
    +    data class Params(
    +            val roomId: String,
    +            val threePid: ThreePid
    +    )
    +}
    +
    +internal class DefaultInviteThreePidTask @Inject constructor(
    +        private val roomAPI: RoomAPI,
    +        private val eventBus: EventBus,
    +        private val identityStore: IdentityStore,
    +        private val ensureIdentityTokenTask: EnsureIdentityTokenTask,
    +        @AuthenticatedIdentity
    +        private val accessTokenProvider: AccessTokenProvider
    +) : InviteThreePidTask {
    +
    +    override suspend fun execute(params: InviteThreePidTask.Params) {
    +        ensureIdentityTokenTask.execute(Unit)
    +
    +        val identityServerUrlWithoutProtocol = identityStore.getIdentityServerUrlWithoutProtocol() ?: throw IdentityServiceError.NoIdentityServerConfigured
    +        val identityServerAccessToken = accessTokenProvider.getToken() ?: throw IdentityServiceError.NoIdentityServerConfigured
    +
    +        return executeRequest(eventBus) {
    +            val body = ThreePidInviteBody(
    +                    id_server = identityServerUrlWithoutProtocol,
    +                    id_access_token = identityServerAccessToken,
    +                    medium = params.threePid.toMedium(),
    +                    address = params.threePid.value
    +            )
    +            apiCall = roomAPI.invite3pid(params.roomId, body)
    +        }
    +    }
    +}
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/threepid/ThreePidInviteBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/threepid/ThreePidInviteBody.kt
    new file mode 100644
    index 0000000000..23dd6bad77
    --- /dev/null
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/threepid/ThreePidInviteBody.kt
    @@ -0,0 +1,41 @@
    +/*
    + * Copyright 2019 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.session.room.membership.threepid
    +
    +import com.squareup.moshi.Json
    +import com.squareup.moshi.JsonClass
    +
    +@JsonClass(generateAdapter = true)
    +internal data class ThreePidInviteBody(
    +        /**
    +         * Required. The hostname+port of the identity server which should be used for third party identifier lookups.
    +         */
    +        @Json(name = "id_server") val id_server: String,
    +        /**
    +         * Required. An access token previously registered with the identity server. Servers can treat this as optional
    +         * to distinguish between r0.5-compatible clients and this specification version.
    +         */
    +        @Json(name = "id_access_token") val id_access_token: String,
    +        /**
    +         * Required. The kind of address being passed in the address field, for example email.
    +         */
    +        @Json(name = "medium") val medium: String,
    +        /**
    +         * Required. The invitee's third party identifier.
    +         */
    +        @Json(name = "address") val address: String
    +)
    diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt b/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt
    index 5bd6852e8a..6afd4fb279 100644
    --- a/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt
    @@ -19,6 +19,8 @@ package im.vector.riotx.core.extensions
     import android.os.Bundle
     import android.util.Patterns
     import androidx.fragment.app.Fragment
    +import com.google.i18n.phonenumbers.NumberParseException
    +import com.google.i18n.phonenumbers.PhoneNumberUtil
     
     fun Boolean.toOnOff() = if (this) "ON" else "OFF"
     
    @@ -33,3 +35,16 @@ fun  T.withArgs(block: Bundle.() -> Unit) = apply { arguments = Bu
      * Check if a CharSequence is an email
      */
     fun CharSequence.isEmail() = Patterns.EMAIL_ADDRESS.matcher(this).matches()
    +
    +/**
    + * Check if a CharSequence is a phone number
    + * FIXME It does not work?
    + */
    +fun CharSequence.isMsisdn(): Boolean {
    +    return try {
    +        PhoneNumberUtil.getInstance().parse(this, null)
    +        true
    +    } catch (e: NumberParseException) {
    +        false
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt
    index 7c32a34aff..2b38a1ac25 100644
    --- a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt
    @@ -17,6 +17,9 @@
     package im.vector.riotx.features.command
     
     import im.vector.matrix.android.api.MatrixPatterns
    +import im.vector.matrix.android.api.session.identity.ThreePid
    +import im.vector.riotx.core.extensions.isEmail
    +import im.vector.riotx.core.extensions.isMsisdn
     import timber.log.Timber
     
     object CommandParser {
    @@ -139,15 +142,24 @@ object CommandParser {
                         if (messageParts.size >= 2) {
                             val userId = messageParts[1]
     
    -                        if (MatrixPatterns.isUserId(userId)) {
    -                            ParsedCommand.Invite(
    -                                    userId,
    -                                    textMessage.substring(Command.INVITE.length + userId.length)
    -                                            .trim()
    -                                            .takeIf { it.isNotBlank() }
    -                            )
    -                        } else {
    -                            ParsedCommand.ErrorSyntax(Command.INVITE)
    +                        when {
    +                            MatrixPatterns.isUserId(userId) -> {
    +                                ParsedCommand.Invite(
    +                                        userId,
    +                                        textMessage.substring(Command.INVITE.length + userId.length)
    +                                                .trim()
    +                                                .takeIf { it.isNotBlank() }
    +                                )
    +                            }
    +                            userId.isEmail()                -> {
    +                                ParsedCommand.Invite3Pid(ThreePid.Email(userId))
    +                            }
    +                            userId.isMsisdn()               -> {
    +                                ParsedCommand.Invite3Pid(ThreePid.Msisdn(userId))
    +                            }
    +                            else                            -> {
    +                                ParsedCommand.ErrorSyntax(Command.INVITE)
    +                            }
                             }
                         } else {
                             ParsedCommand.ErrorSyntax(Command.INVITE)
    diff --git a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt
    index 44ad2265e1..041da3dcac 100644
    --- a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt
    @@ -16,6 +16,8 @@
     
     package im.vector.riotx.features.command
     
    +import im.vector.matrix.android.api.session.identity.ThreePid
    +
     /**
      * Represent a parsed command
      */
    @@ -41,6 +43,7 @@ sealed class ParsedCommand {
         class UnbanUser(val userId: String, val reason: String?) : ParsedCommand()
         class SetUserPowerLevel(val userId: String, val powerLevel: Int?) : ParsedCommand()
         class Invite(val userId: String, val reason: String?) : ParsedCommand()
    +    class Invite3Pid(val threePid: ThreePid) : ParsedCommand()
         class JoinRoom(val roomAlias: String, val reason: String?) : ParsedCommand()
         class PartRoom(val roomAlias: String, val reason: String?) : ParsedCommand()
         class ChangeTopic(val topic: String) : ParsedCommand()
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    index 62078c3053..3c65b6281f 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    @@ -960,7 +960,7 @@ class RoomDetailFragment @Inject constructor(
                     updateComposerText("")
                 }
                 is RoomDetailViewEvents.SlashCommandResultError    -> {
    -                displayCommandError(sendMessageResult.throwable.localizedMessage ?: getString(R.string.unexpected_error))
    +                displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable))
                 }
                 is RoomDetailViewEvents.SlashCommandNotImplemented -> {
                     displayCommandError(getString(R.string.not_implemented))
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    index 982448d1c1..a396152f6b 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    @@ -457,6 +457,10 @@ class RoomDetailViewModel @AssistedInject constructor(
                                 handleInviteSlashCommand(slashCommandResult)
                                 popDraft()
                             }
    +                        is ParsedCommand.Invite3Pid               -> {
    +                            handleInvite3pidSlashCommand(slashCommandResult)
    +                            popDraft()
    +                        }
                             is ParsedCommand.SetUserPowerLevel        -> {
                                 handleSetUserPowerLevel(slashCommandResult)
                                 popDraft()
    @@ -678,6 +682,12 @@ class RoomDetailViewModel @AssistedInject constructor(
             }
         }
     
    +    private fun handleInvite3pidSlashCommand(invite: ParsedCommand.Invite3Pid) {
    +        launchSlashCommandFlow {
    +            room.invite3pid(invite.threePid, it)
    +        }
    +    }
    +
         private fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) {
             val currentPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)
                     ?.content
    
    From 3842ec6bb09e32a9a882c2aaa59fa1d7b25166d5 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Wed, 8 Jul 2020 09:54:57 +0200
    Subject: [PATCH 094/122] Invite by msisdn. Error 500 from matrix.org though
     (#548)
    
    ---
     .../matrix/android/api/extensions/Strings.kt  | 24 +++++++++++++++++++
     .../riotx/core/extensions/BasicExtensions.kt  |  4 ++--
     2 files changed, 26 insertions(+), 2 deletions(-)
     create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/Strings.kt
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/Strings.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/Strings.kt
    new file mode 100644
    index 0000000000..202c15b5b0
    --- /dev/null
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/Strings.kt
    @@ -0,0 +1,24 @@
    +/*
    + * Copyright (c) 2020 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.api.extensions
    +
    +fun CharSequence.ensurePrefix(prefix: CharSequence): CharSequence {
    +    return when {
    +        startsWith(prefix) -> this
    +        else               -> "$prefix$this"
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt b/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt
    index 6afd4fb279..99a5cb5a1a 100644
    --- a/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/extensions/BasicExtensions.kt
    @@ -21,6 +21,7 @@ import android.util.Patterns
     import androidx.fragment.app.Fragment
     import com.google.i18n.phonenumbers.NumberParseException
     import com.google.i18n.phonenumbers.PhoneNumberUtil
    +import im.vector.matrix.android.api.extensions.ensurePrefix
     
     fun Boolean.toOnOff() = if (this) "ON" else "OFF"
     
    @@ -38,11 +39,10 @@ fun CharSequence.isEmail() = Patterns.EMAIL_ADDRESS.matcher(this).matches()
     
     /**
      * Check if a CharSequence is a phone number
    - * FIXME It does not work?
      */
     fun CharSequence.isMsisdn(): Boolean {
         return try {
    -        PhoneNumberUtil.getInstance().parse(this, null)
    +        PhoneNumberUtil.getInstance().parse(ensurePrefix("+"), null)
             true
         } catch (e: NumberParseException) {
             false
    
    From 1c733e666180b478bacb6cb133b30e5c6b9171e0 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Wed, 8 Jul 2020 16:54:14 +0200
    Subject: [PATCH 095/122] Display Contact list (#548)
    
    WIP (#548)
    
    WIP (#548)
    
    WIP (#548)
    
    WIP (#548)
    
    WIP (#548)
    ---
     .../riotx/core/contacts/ContactModel.kt       |  58 ++++++
     .../riotx/core/contacts/ContactsDataSource.kt | 131 ++++++++++++++
     .../im/vector/riotx/core/di/FragmentModule.kt |   6 +
     .../createdirect/CreateDirectRoomActivity.kt  |  12 +-
     .../riotx/features/home/AvatarRenderer.kt     |  18 ++
     .../invite/InviteUsersToRoomActivity.kt       |   8 +-
     .../userdirectory/ContactDetailItem.kt        |  47 +++++
     .../features/userdirectory/ContactItem.kt     |  46 +++++
     .../userdirectory/KnownUsersFragment.kt       |   8 +
     .../features/userdirectory/PhoneBookAction.kt |  23 +++
     .../userdirectory/PhoneBookController.kt      | 140 +++++++++++++++
     .../userdirectory/PhoneBookFragment.kt        |  99 ++++++++++
     .../userdirectory/PhoneBookViewModel.kt       | 169 ++++++++++++++++++
     .../userdirectory/PhoneBookViewState.kt       |  35 ++++
     .../userdirectory/UserDirectoryAction.kt      |   2 +
     .../UserDirectorySharedAction.kt              |   1 +
     .../main/res/layout/fragment_known_users.xml  |  19 +-
     .../main/res/layout/fragment_phonebook.xml    | 109 +++++++++++
     .../main/res/layout/item_contact_detail.xml   |  46 +++++
     .../src/main/res/layout/item_contact_main.xml |  38 ++++
     vector/src/main/res/values/strings.xml        |   4 +
     21 files changed, 1013 insertions(+), 6 deletions(-)
     create mode 100644 vector/src/main/java/im/vector/riotx/core/contacts/ContactModel.kt
     create mode 100644 vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt
     create mode 100644 vector/src/main/java/im/vector/riotx/features/userdirectory/ContactDetailItem.kt
     create mode 100644 vector/src/main/java/im/vector/riotx/features/userdirectory/ContactItem.kt
     create mode 100644 vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookAction.kt
     create mode 100644 vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt
     create mode 100644 vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookFragment.kt
     create mode 100644 vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewModel.kt
     create mode 100644 vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewState.kt
     create mode 100644 vector/src/main/res/layout/fragment_phonebook.xml
     create mode 100644 vector/src/main/res/layout/item_contact_detail.xml
     create mode 100644 vector/src/main/res/layout/item_contact_main.xml
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/contacts/ContactModel.kt b/vector/src/main/java/im/vector/riotx/core/contacts/ContactModel.kt
    new file mode 100644
    index 0000000000..589c3030c3
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/core/contacts/ContactModel.kt
    @@ -0,0 +1,58 @@
    +/*
    + * Copyright (c) 2020 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.riotx.core.contacts
    +
    +import android.net.Uri
    +
    +/* TODO Rename to MxContact? */
    +
    +class ContactModelBuilder(
    +        val id: Long,
    +        val displayName: String) {
    +
    +    var photoURI: Uri? = null
    +    val msisdns = mutableListOf()
    +    val emails = mutableListOf()
    +
    +    fun toContactModel(): ContactModel {
    +        return ContactModel(
    +                id = id,
    +                displayName = displayName,
    +                photoURI = photoURI,
    +                msisdns = msisdns,
    +                emails = emails
    +        )
    +    }
    +}
    +
    +data class ContactModel(
    +        val id: Long,
    +        val displayName: String,
    +        val photoURI: Uri? = null,
    +        val msisdns: List = emptyList(),
    +        val emails: List = emptyList()
    +)
    +
    +data class MappedEmail(
    +        val email: String,
    +        val matrixId: String?
    +)
    +
    +data class MappedMsisdn(
    +        val phoneNumber: String,
    +        val matrixId: String?
    +)
    diff --git a/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt b/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt
    new file mode 100644
    index 0000000000..2160216d5d
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt
    @@ -0,0 +1,131 @@
    +/*
    + * Copyright (c) 2020 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.riotx.core.contacts
    +
    +import android.content.Context
    +import android.database.Cursor
    +import android.net.Uri
    +import android.provider.ContactsContract
    +import androidx.annotation.WorkerThread
    +import javax.inject.Inject
    +
    +class ContactsDataSource @Inject constructor(
    +        private val context: Context
    +) {
    +
    +    @WorkerThread
    +    fun getContacts(): List {
    +        val result = mutableListOf()
    +        val contentResolver = context.contentResolver
    +
    +        contentResolver.query(
    +                ContactsContract.Contacts.CONTENT_URI,
    +                null,
    +                /* TODO
    +                arrayOf(
    +                        ContactsContract.Contacts._ID,
    +                        ContactsContract.Data.DISPLAY_NAME,
    +                        ContactsContract.Data.PHOTO_URI,
    +                        ContactsContract.Data.MIMETYPE,
    +                        ContactsContract.CommonDataKinds.Phone.NUMBER,
    +                        ContactsContract.CommonDataKinds.Email.ADDRESS
    +                ),
    +                 */
    +                null,
    +                null,
    +                // Sort by Display name
    +                ContactsContract.Data.DISPLAY_NAME
    +        )
    +                ?.use { cursor ->
    +                    if (cursor.count > 0) {
    +                        while (cursor.moveToNext()) {
    +                            val id = cursor.getLong(ContactsContract.Contacts._ID) ?: continue
    +                            val displayName = cursor.getString(ContactsContract.Contacts.DISPLAY_NAME) ?: continue
    +
    +                            val currentContact = ContactModelBuilder(
    +                                    id = id,
    +                                    displayName = displayName
    +                            )
    +
    +                            cursor.getString(ContactsContract.Data.PHOTO_URI)
    +                                    ?.let { Uri.parse(it) }
    +                                    ?.let { currentContact.photoURI = it }
    +
    +                            // Get the phone numbers
    +                            contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
    +                                    null,
    +                                    ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
    +                                    arrayOf(id.toString()),
    +                                    null)
    +                                    ?.use { innerCursor ->
    +                                        while (innerCursor.moveToNext()) {
    +                                            innerCursor.getString(ContactsContract.CommonDataKinds.Phone.NUMBER)
    +                                                    ?.let {
    +                                                        currentContact.msisdns.add(
    +                                                                MappedMsisdn(
    +                                                                        phoneNumber = it,
    +                                                                        matrixId = null
    +                                                                )
    +                                                        )
    +                                                    }
    +                                        }
    +                                    }
    +
    +                            // Get Emails
    +                            contentResolver.query(
    +                                    ContactsContract.CommonDataKinds.Email.CONTENT_URI,
    +                                    null,
    +                                    ContactsContract.CommonDataKinds.Email.CONTACT_ID + " = ?",
    +                                    arrayOf(id.toString()),
    +                                    null)
    +                                    ?.use { innerCursor ->
    +                                        while (innerCursor.moveToNext()) {
    +                                            // This would allow you get several email addresses
    +                                            // if the email addresses were stored in an array
    +                                            innerCursor.getString(ContactsContract.CommonDataKinds.Email.DATA)
    +                                                    ?.let {
    +                                                        currentContact.emails.add(
    +                                                                MappedEmail(
    +                                                                        email = it,
    +                                                                        matrixId = null
    +                                                                )
    +                                                        )
    +                                                    }
    +                                        }
    +                                    }
    +
    +                            result.add(currentContact.toContactModel())
    +                        }
    +                    }
    +                }
    +
    +        return result
    +                .filter { it.emails.isNotEmpty() || it.msisdns.isNotEmpty() }
    +    }
    +
    +    private fun Cursor.getString(column: String): String? {
    +        return getColumnIndex(column)
    +                .takeIf { it != -1 }
    +                ?.let { getString(it) }
    +    }
    +
    +    private fun Cursor.getLong(column: String): Long? {
    +        return getColumnIndex(column)
    +                .takeIf { it != -1 }
    +                ?.let { getLong(it) }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
    index 21cff188d0..0201a44096 100644
    --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
    @@ -103,6 +103,7 @@ import im.vector.riotx.features.share.IncomingShareFragment
     import im.vector.riotx.features.signout.soft.SoftLogoutFragment
     import im.vector.riotx.features.terms.ReviewTermsFragment
     import im.vector.riotx.features.userdirectory.KnownUsersFragment
    +import im.vector.riotx.features.userdirectory.PhoneBookFragment
     import im.vector.riotx.features.userdirectory.UserDirectoryFragment
     import im.vector.riotx.features.widgets.WidgetFragment
     
    @@ -528,4 +529,9 @@ interface FragmentModule {
         @IntoMap
         @FragmentKey(WidgetFragment::class)
         fun bindWidgetFragment(fragment: WidgetFragment): Fragment
    +
    +    @Binds
    +    @IntoMap
    +    @FragmentKey(PhoneBookFragment::class)
    +    fun bindPhoneBookFragment(fragment: PhoneBookFragment): Fragment
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt
    index ef3e9bdeff..973a4b6f16 100644
    --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt
    @@ -35,10 +35,12 @@ import im.vector.riotx.core.di.ScreenComponent
     import im.vector.riotx.core.error.ErrorFormatter
     import im.vector.riotx.core.extensions.addFragment
     import im.vector.riotx.core.extensions.addFragmentToBackstack
    +import im.vector.riotx.core.extensions.exhaustive
     import im.vector.riotx.core.platform.SimpleFragmentActivity
     import im.vector.riotx.core.platform.WaitingViewData
     import im.vector.riotx.features.userdirectory.KnownUsersFragment
     import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs
    +import im.vector.riotx.features.userdirectory.PhoneBookViewModel
     import im.vector.riotx.features.userdirectory.UserDirectoryFragment
     import im.vector.riotx.features.userdirectory.UserDirectorySharedAction
     import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
    @@ -53,6 +55,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
         private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
         @Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
         @Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory
    +    @Inject lateinit var phoneBookViewModelFactory: PhoneBookViewModel.Factory
         @Inject lateinit var errorFormatter: ErrorFormatter
     
         override fun injectWith(injector: ScreenComponent) {
    @@ -68,12 +71,13 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
                     .observe()
                     .subscribe { sharedAction ->
                         when (sharedAction) {
    -                        UserDirectorySharedAction.OpenUsersDirectory ->
    +                        UserDirectorySharedAction.OpenUsersDirectory    ->
                                 addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java)
    -                        UserDirectorySharedAction.Close           -> finish()
    -                        UserDirectorySharedAction.GoBack          -> onBackPressed()
    +                        UserDirectorySharedAction.Close                 -> finish()
    +                        UserDirectorySharedAction.GoBack                -> onBackPressed()
                             is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
    -                    }
    +                        UserDirectorySharedAction.OpenPhoneBook         -> TODO()
    +                    }.exhaustive
                     }
                     .disposeOnDestroy()
             if (isFirstCreation()) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
    index f917b5a9f9..e0d41ca445 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
    @@ -30,6 +30,7 @@ import com.bumptech.glide.request.target.DrawableImageViewTarget
     import com.bumptech.glide.request.target.Target
     import im.vector.matrix.android.api.session.content.ContentUrlResolver
     import im.vector.matrix.android.api.util.MatrixItem
    +import im.vector.riotx.core.contacts.ContactModel
     import im.vector.riotx.core.di.ActiveSessionHolder
     import im.vector.riotx.core.glide.GlideApp
     import im.vector.riotx.core.glide.GlideRequest
    @@ -63,6 +64,23 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
                     DrawableImageViewTarget(imageView))
         }
     
    +    @UiThread
    +    fun render(contactModel: ContactModel, imageView: ImageView) {
    +        // Create a Fake MatrixItem, for the placeholder
    +        val matrixItem = MatrixItem.UserItem(
    +                // Need an id starting with @
    +                id = "@${contactModel.displayName}",
    +                displayName = contactModel.displayName
    +        )
    +
    +        val placeholder = getPlaceholderDrawable(imageView.context, matrixItem)
    +        GlideApp.with(imageView)
    +                .load(contactModel.photoURI)
    +                .apply(RequestOptions.circleCropTransform())
    +                .placeholder(placeholder)
    +                .into(imageView)
    +    }
    +
         @UiThread
         fun render(context: Context,
                    glideRequests: GlideRequests,
    diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt
    index 839a0767d8..af0e974c8a 100644
    --- a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt
    @@ -30,11 +30,14 @@ import im.vector.riotx.core.di.ScreenComponent
     import im.vector.riotx.core.error.ErrorFormatter
     import im.vector.riotx.core.extensions.addFragment
     import im.vector.riotx.core.extensions.addFragmentToBackstack
    +import im.vector.riotx.core.extensions.exhaustive
     import im.vector.riotx.core.platform.SimpleFragmentActivity
     import im.vector.riotx.core.platform.WaitingViewData
     import im.vector.riotx.core.utils.toast
     import im.vector.riotx.features.userdirectory.KnownUsersFragment
     import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs
    +import im.vector.riotx.features.userdirectory.PhoneBookFragment
    +import im.vector.riotx.features.userdirectory.PhoneBookViewModel
     import im.vector.riotx.features.userdirectory.UserDirectoryFragment
     import im.vector.riotx.features.userdirectory.UserDirectorySharedAction
     import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
    @@ -53,6 +56,7 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
         private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
         @Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
         @Inject lateinit var inviteUsersToRoomViewModelFactory: InviteUsersToRoomViewModel.Factory
    +    @Inject lateinit var phoneBookViewModelFactory: PhoneBookViewModel.Factory
         @Inject lateinit var errorFormatter: ErrorFormatter
     
         override fun injectWith(injector: ScreenComponent) {
    @@ -74,7 +78,9 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
                             UserDirectorySharedAction.Close                 -> finish()
                             UserDirectorySharedAction.GoBack                -> onBackPressed()
                             is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
    -                    }
    +                        UserDirectorySharedAction.OpenPhoneBook         ->
    +                            addFragmentToBackstack(R.id.container, PhoneBookFragment::class.java)
    +                    }.exhaustive
                     }
                     .disposeOnDestroy()
             if (isFirstCreation()) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/ContactDetailItem.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/ContactDetailItem.kt
    new file mode 100644
    index 0000000000..df29545201
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/ContactDetailItem.kt
    @@ -0,0 +1,47 @@
    +/*
    + * Copyright (c) 2020 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.riotx.features.userdirectory
    +
    +import android.widget.TextView
    +import com.airbnb.epoxy.EpoxyAttribute
    +import com.airbnb.epoxy.EpoxyModelClass
    +import im.vector.riotx.R
    +import im.vector.riotx.core.epoxy.ClickListener
    +import im.vector.riotx.core.epoxy.VectorEpoxyHolder
    +import im.vector.riotx.core.epoxy.VectorEpoxyModel
    +import im.vector.riotx.core.epoxy.onClick
    +import im.vector.riotx.core.extensions.setTextOrHide
    +
    +@EpoxyModelClass(layout = R.layout.item_contact_detail)
    +abstract class ContactDetailItem : VectorEpoxyModel() {
    +
    +    @EpoxyAttribute lateinit var threePid: String
    +    @EpoxyAttribute var matrixId: String? = null
    +    @EpoxyAttribute var clickListener: ClickListener? = null
    +
    +    override fun bind(holder: Holder) {
    +        super.bind(holder)
    +        holder.view.onClick(clickListener)
    +        holder.nameView.text = threePid
    +        holder.matrixIdView.setTextOrHide(matrixId)
    +    }
    +
    +    class Holder : VectorEpoxyHolder() {
    +        val nameView by bind(R.id.contactDetailName)
    +        val matrixIdView by bind(R.id.contactDetailMatrixId)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/ContactItem.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/ContactItem.kt
    new file mode 100644
    index 0000000000..67d762b4b2
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/ContactItem.kt
    @@ -0,0 +1,46 @@
    +/*
    + * Copyright (c) 2020 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.riotx.features.userdirectory
    +
    +import android.widget.ImageView
    +import android.widget.TextView
    +import com.airbnb.epoxy.EpoxyAttribute
    +import com.airbnb.epoxy.EpoxyModelClass
    +import im.vector.riotx.R
    +import im.vector.riotx.core.contacts.ContactModel
    +import im.vector.riotx.core.epoxy.VectorEpoxyHolder
    +import im.vector.riotx.core.epoxy.VectorEpoxyModel
    +import im.vector.riotx.features.home.AvatarRenderer
    +
    +@EpoxyModelClass(layout = R.layout.item_contact_main)
    +abstract class ContactItem : VectorEpoxyModel() {
    +
    +    @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
    +    @EpoxyAttribute lateinit var contact: ContactModel
    +
    +    override fun bind(holder: Holder) {
    +        super.bind(holder)
    +        // If name is empty, use userId as name and force it being centered
    +        holder.nameView.text = contact.displayName
    +        avatarRenderer.render(contact, holder.avatarImageView)
    +    }
    +
    +    class Holder : VectorEpoxyHolder() {
    +        val nameView by bind(R.id.contactDisplayName)
    +        val avatarImageView by bind(R.id.contactAvatar)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt
    index 42dd46bd01..d681e5d92f 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt
    @@ -63,6 +63,7 @@ class KnownUsersFragment @Inject constructor(
             setupRecyclerView()
             setupFilterView()
             setupAddByMatrixIdView()
    +        setupAddFromPhoneBookView()
             setupCloseView()
             viewModel.selectSubscribe(this, UserDirectoryViewState::selectedUsers) {
                 renderSelectedUsers(it)
    @@ -96,6 +97,13 @@ class KnownUsersFragment @Inject constructor(
             }
         }
     
    +    private fun setupAddFromPhoneBookView() {
    +        addFromPhoneBook.debouncedClicks {
    +            // TODO handle Permission first
    +            sharedActionViewModel.post(UserDirectorySharedAction.OpenPhoneBook)
    +        }
    +    }
    +
         private fun setupRecyclerView() {
             knownUsersController.callback = this
             // Don't activate animation as we might have way to much item animation when filtering
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookAction.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookAction.kt
    new file mode 100644
    index 0000000000..d3d534e694
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookAction.kt
    @@ -0,0 +1,23 @@
    +/*
    + * Copyright (c) 2020 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.riotx.features.userdirectory
    +
    +import im.vector.riotx.core.platform.VectorViewModelAction
    +
    +sealed class PhoneBookAction : VectorViewModelAction {
    +    data class FilterWith(val filter: String) : PhoneBookAction()
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt
    new file mode 100644
    index 0000000000..39f00d6557
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt
    @@ -0,0 +1,140 @@
    +/*
    + * Copyright (c) 2020 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.riotx.features.userdirectory
    +
    +import com.airbnb.epoxy.EpoxyController
    +import com.airbnb.mvrx.Fail
    +import com.airbnb.mvrx.Loading
    +import com.airbnb.mvrx.Success
    +import com.airbnb.mvrx.Uninitialized
    +import im.vector.matrix.android.api.session.identity.ThreePid
    +import im.vector.riotx.R
    +import im.vector.riotx.core.contacts.ContactModel
    +import im.vector.riotx.core.epoxy.errorWithRetryItem
    +import im.vector.riotx.core.epoxy.loadingItem
    +import im.vector.riotx.core.epoxy.noResultItem
    +import im.vector.riotx.core.error.ErrorFormatter
    +import im.vector.riotx.core.resources.StringProvider
    +import im.vector.riotx.features.home.AvatarRenderer
    +import javax.inject.Inject
    +
    +class PhoneBookController @Inject constructor(
    +        private val stringProvider: StringProvider,
    +        private val avatarRenderer: AvatarRenderer,
    +        private val errorFormatter: ErrorFormatter) : EpoxyController() {
    +
    +    private var state: PhoneBookViewState? = null
    +
    +    var callback: Callback? = null
    +
    +    init {
    +        requestModelBuild()
    +    }
    +
    +    fun setData(state: PhoneBookViewState) {
    +        this.state = state
    +        requestModelBuild()
    +    }
    +
    +    override fun buildModels() {
    +        val currentState = state ?: return
    +        val hasSearch = currentState.searchTerm.isNotBlank()
    +        when (val asyncMappedContacts = currentState.mappedContacts) {
    +            is Uninitialized -> renderEmptyState(false)
    +            is Loading       -> renderLoading()
    +            is Success       -> renderSuccess(currentState.filteredMappedContacts, hasSearch)
    +            is Fail          -> renderFailure(asyncMappedContacts.error)
    +        }
    +    }
    +
    +    private fun renderLoading() {
    +        loadingItem {
    +            id("loading")
    +        }
    +    }
    +
    +    private fun renderFailure(failure: Throwable) {
    +        errorWithRetryItem {
    +            id("error")
    +            text(errorFormatter.toHumanReadable(failure))
    +        }
    +    }
    +
    +    private fun renderSuccess(mappedContacts: List,
    +                              hasSearch: Boolean) {
    +        if (mappedContacts.isEmpty()) {
    +            renderEmptyState(hasSearch)
    +        } else {
    +            renderContacts(mappedContacts)
    +        }
    +    }
    +
    +    private fun renderContacts(mappedContacts: List) {
    +        for (mappedContact in mappedContacts) {
    +            contactItem {
    +                id(mappedContact.id)
    +                contact(mappedContact)
    +                avatarRenderer(avatarRenderer)
    +            }
    +            mappedContact.emails.forEach {
    +                contactDetailItem {
    +                    id("$mappedContact.id${it.email}")
    +                    threePid(it.email)
    +                    matrixId(it.matrixId)
    +                    clickListener {
    +                        if (it.matrixId != null) {
    +                            callback?.onMatrixIdClick(it.matrixId)
    +                        } else {
    +                            callback?.onThreePidClick(ThreePid.Email(it.email))
    +                        }
    +                    }
    +                }
    +            }
    +            mappedContact.msisdns.forEach {
    +                contactDetailItem {
    +                    id("$mappedContact.id${it.phoneNumber}")
    +                    threePid(it.phoneNumber)
    +                    matrixId(it.matrixId)
    +                    clickListener {
    +                        if (it.matrixId != null) {
    +                            callback?.onMatrixIdClick(it.matrixId)
    +                        } else {
    +                            callback?.onThreePidClick(ThreePid.Msisdn(it.phoneNumber))
    +                        }
    +                    }
    +                }
    +            }
    +        }
    +    }
    +
    +    private fun renderEmptyState(hasSearch: Boolean) {
    +        val noResultRes = if (hasSearch) {
    +            R.string.no_result_placeholder
    +        } else {
    +            R.string.empty_phone_book
    +        }
    +        noResultItem {
    +            id("noResult")
    +            text(stringProvider.getString(noResultRes))
    +        }
    +    }
    +
    +    interface Callback {
    +        fun onMatrixIdClick(matrixId: String)
    +        fun onThreePidClick(threePid: ThreePid)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookFragment.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookFragment.kt
    new file mode 100644
    index 0000000000..9f1f8268c3
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookFragment.kt
    @@ -0,0 +1,99 @@
    +/*
    + * Copyright (c) 2020 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.riotx.features.userdirectory
    +
    +import android.os.Bundle
    +import android.view.View
    +import com.airbnb.mvrx.activityViewModel
    +import com.airbnb.mvrx.withState
    +import com.jakewharton.rxbinding3.widget.textChanges
    +import im.vector.matrix.android.api.session.identity.ThreePid
    +import im.vector.matrix.android.api.session.user.model.User
    +import im.vector.riotx.R
    +import im.vector.riotx.core.extensions.cleanup
    +import im.vector.riotx.core.extensions.configureWith
    +import im.vector.riotx.core.extensions.hideKeyboard
    +import im.vector.riotx.core.platform.VectorBaseFragment
    +import kotlinx.android.synthetic.main.fragment_phonebook.*
    +import java.util.concurrent.TimeUnit
    +import javax.inject.Inject
    +
    +class PhoneBookFragment @Inject constructor(
    +        val phoneBookViewModelFactory: PhoneBookViewModel.Factory,
    +        private val phoneBookController: PhoneBookController
    +) : VectorBaseFragment(), PhoneBookController.Callback {
    +
    +    override fun getLayoutResId() = R.layout.fragment_phonebook
    +    private val viewModel: UserDirectoryViewModel by activityViewModel()
    +
    +    // Use activityViewModel to avoid loading several times the data
    +    private val phoneBookViewModel: PhoneBookViewModel by activityViewModel()
    +
    +    private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
    +
    +    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    +        super.onViewCreated(view, savedInstanceState)
    +        sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
    +        setupRecyclerView()
    +        setupFilterView()
    +        setupCloseView()
    +    }
    +
    +    private fun setupFilterView() {
    +        phoneBookFilter
    +                .textChanges()
    +                .skipInitialValue()
    +                .debounce(300, TimeUnit.MILLISECONDS)
    +                .subscribe {
    +                    phoneBookViewModel.handle(PhoneBookAction.FilterWith(it.toString()))
    +                }
    +                .disposeOnDestroyView()
    +    }
    +
    +    override fun onDestroyView() {
    +        phoneBookRecyclerView.cleanup()
    +        phoneBookController.callback = null
    +        super.onDestroyView()
    +    }
    +
    +    private fun setupRecyclerView() {
    +        phoneBookController.callback = this
    +        phoneBookRecyclerView.configureWith(phoneBookController)
    +    }
    +
    +    private fun setupCloseView() {
    +        phoneBookClose.debouncedClicks {
    +            sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
    +        }
    +    }
    +
    +    override fun invalidate() = withState(phoneBookViewModel) {
    +        phoneBookController.setData(it)
    +    }
    +
    +    override fun onMatrixIdClick(matrixId: String) {
    +        view?.hideKeyboard()
    +        viewModel.handle(UserDirectoryAction.SelectUser(User(matrixId)))
    +        sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
    +    }
    +
    +    override fun onThreePidClick(threePid: ThreePid) {
    +        view?.hideKeyboard()
    +        viewModel.handle(UserDirectoryAction.SelectThreePid(threePid))
    +        sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewModel.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewModel.kt
    new file mode 100644
    index 0000000000..d894bbe908
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewModel.kt
    @@ -0,0 +1,169 @@
    +/*
    + * Copyright (c) 2020 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.riotx.features.userdirectory
    +
    +import androidx.fragment.app.FragmentActivity
    +import androidx.lifecycle.viewModelScope
    +import com.airbnb.mvrx.ActivityViewModelContext
    +import com.airbnb.mvrx.FragmentViewModelContext
    +import com.airbnb.mvrx.Loading
    +import com.airbnb.mvrx.MvRxViewModelFactory
    +import com.airbnb.mvrx.Success
    +import com.airbnb.mvrx.ViewModelContext
    +import com.squareup.inject.assisted.Assisted
    +import com.squareup.inject.assisted.AssistedInject
    +import im.vector.matrix.android.api.MatrixCallback
    +import im.vector.matrix.android.api.session.Session
    +import im.vector.matrix.android.api.session.identity.FoundThreePid
    +import im.vector.matrix.android.api.session.identity.ThreePid
    +import im.vector.riotx.core.contacts.ContactModel
    +import im.vector.riotx.core.contacts.ContactsDataSource
    +import im.vector.riotx.core.extensions.exhaustive
    +import im.vector.riotx.core.platform.EmptyViewEvents
    +import im.vector.riotx.core.platform.VectorViewModel
    +import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
    +import im.vector.riotx.features.invite.InviteUsersToRoomActivity
    +import kotlinx.coroutines.launch
    +
    +private typealias PhoneBookSearch = String
    +
    +class PhoneBookViewModel @AssistedInject constructor(@Assisted
    +                                                     initialState: PhoneBookViewState,
    +                                                     private val contactsDataSource: ContactsDataSource,
    +                                                     private val session: Session)
    +    : VectorViewModel(initialState) {
    +
    +    @AssistedInject.Factory
    +    interface Factory {
    +        fun create(initialState: PhoneBookViewState): PhoneBookViewModel
    +    }
    +
    +    companion object : MvRxViewModelFactory {
    +
    +        override fun create(viewModelContext: ViewModelContext, state: PhoneBookViewState): PhoneBookViewModel? {
    +            return when (viewModelContext) {
    +                is FragmentViewModelContext -> (viewModelContext.fragment() as PhoneBookFragment).phoneBookViewModelFactory.create(state)
    +                is ActivityViewModelContext -> {
    +                    when (viewModelContext.activity()) {
    +                        is CreateDirectRoomActivity  -> viewModelContext.activity().phoneBookViewModelFactory.create(state)
    +                        is InviteUsersToRoomActivity -> viewModelContext.activity().phoneBookViewModelFactory.create(state)
    +                        else                         -> error("Wrong activity or fragment")
    +                    }
    +                }
    +                else                        -> error("Wrong activity or fragment")
    +            }
    +        }
    +    }
    +
    +    private var allContacts: List = emptyList()
    +    private var mappedContacts: List = emptyList()
    +    private var foundThreePid: List = emptyList()
    +
    +    init {
    +        loadContacts()
    +
    +        selectSubscribe(PhoneBookViewState::searchTerm) {
    +            updateState()
    +        }
    +    }
    +
    +    private fun loadContacts() {
    +        setState {
    +            copy(
    +                    mappedContacts = Loading()
    +            )
    +        }
    +
    +        viewModelScope.launch {
    +            allContacts = contactsDataSource.getContacts()
    +            mappedContacts = allContacts
    +
    +            setState {
    +                copy(
    +                        mappedContacts = Success(allContacts)
    +                )
    +            }
    +
    +            performLookup(allContacts)
    +            updateState()
    +        }
    +    }
    +
    +    private fun performLookup(data: List) {
    +        viewModelScope.launch {
    +            val threePids = data.flatMap { contact ->
    +                contact.emails.map { ThreePid.Email(it.email) } +
    +                        contact.msisdns.map { ThreePid.Msisdn(it.phoneNumber) }
    +            }
    +            session.identityService().lookUp(threePids, object : MatrixCallback> {
    +                override fun onFailure(failure: Throwable) {
    +                    // Ignore?
    +                }
    +
    +                override fun onSuccess(data: List) {
    +                    foundThreePid = data
    +
    +                    mappedContacts = allContacts.map { contactModel ->
    +                        contactModel.copy(
    +                                emails = contactModel.emails.map { email ->
    +                                    email.copy(
    +                                            matrixId = foundThreePid
    +                                                    .firstOrNull { foundThreePid -> foundThreePid.threePid.value == email.email }
    +                                                    ?.matrixId
    +                                    )
    +                                },
    +                                msisdns = contactModel.msisdns.map { msisdn ->
    +                                    msisdn.copy(
    +                                            matrixId = foundThreePid
    +                                                    .firstOrNull { foundThreePid -> foundThreePid.threePid.value == msisdn.phoneNumber }
    +                                                    ?.matrixId
    +                                    )
    +                                }
    +                        )
    +                    }
    +
    +                    updateState()
    +                }
    +            })
    +        }
    +    }
    +
    +    private fun updateState() = withState { state ->
    +        val filteredMappedContacts = mappedContacts
    +                .filter { it.displayName.contains(state.searchTerm, true) }
    +
    +        setState {
    +            copy(
    +                    filteredMappedContacts = filteredMappedContacts
    +            )
    +        }
    +    }
    +
    +    override fun handle(action: PhoneBookAction) {
    +        when (action) {
    +            is PhoneBookAction.FilterWith -> handleFilterWith(action)
    +        }.exhaustive
    +    }
    +
    +    private fun handleFilterWith(action: PhoneBookAction.FilterWith) {
    +        setState {
    +            copy(
    +                    searchTerm = action.filter
    +            )
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewState.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewState.kt
    new file mode 100644
    index 0000000000..81709f84b4
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewState.kt
    @@ -0,0 +1,35 @@
    +/*
    + * Copyright (c) 2020 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.riotx.features.userdirectory
    +
    +import com.airbnb.mvrx.Async
    +import com.airbnb.mvrx.Loading
    +import com.airbnb.mvrx.MvRxState
    +import im.vector.riotx.core.contacts.ContactModel
    +
    +data class PhoneBookViewState(
    +        val searchTerm: String = "",
    +        val mappedContacts: Async> = Loading(),
    +        val filteredMappedContacts: List = emptyList()
    +        /*
    +        val knownUsers: Async> = Uninitialized,
    +        val directoryUsers: Async> = Uninitialized,
    +        val selectedUsers: Set = emptySet(),
    +        val createAndInviteState: Async = Uninitialized,
    +        val filterKnownUsersValue: Option = Option.empty()
    +         */
    +) : MvRxState
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryAction.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryAction.kt
    index 1df3c02736..3051e14bea 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryAction.kt
    @@ -16,6 +16,7 @@
     
     package im.vector.riotx.features.userdirectory
     
    +import im.vector.matrix.android.api.session.identity.ThreePid
     import im.vector.matrix.android.api.session.user.model.User
     import im.vector.riotx.core.platform.VectorViewModelAction
     
    @@ -24,5 +25,6 @@ sealed class UserDirectoryAction : VectorViewModelAction {
         data class SearchDirectoryUsers(val value: String) : UserDirectoryAction()
         object ClearFilterKnownUsers : UserDirectoryAction()
         data class SelectUser(val user: User) : UserDirectoryAction()
    +    data class SelectThreePid(val threePid: ThreePid) : UserDirectoryAction()
         data class RemoveSelectedUser(val user: User) : UserDirectoryAction()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedAction.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedAction.kt
    index 7d1987aa4b..b071b6f7a8 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedAction.kt
    @@ -21,6 +21,7 @@ import im.vector.riotx.core.platform.VectorSharedAction
     
     sealed class UserDirectorySharedAction : VectorSharedAction {
         object OpenUsersDirectory : UserDirectorySharedAction()
    +    object OpenPhoneBook : UserDirectorySharedAction()
         object Close : UserDirectorySharedAction()
         object GoBack : UserDirectorySharedAction()
         data class OnMenuItemSelected(val itemId: Int, val selectedUsers: Set) : UserDirectorySharedAction()
    diff --git a/vector/src/main/res/layout/fragment_known_users.xml b/vector/src/main/res/layout/fragment_known_users.xml
    index c04cf027a6..16e6858e60 100644
    --- a/vector/src/main/res/layout/fragment_known_users.xml
    +++ b/vector/src/main/res/layout/fragment_known_users.xml
    @@ -123,6 +123,23 @@
                 app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintTop_toBottomOf="@id/knownUsersFilterDivider" />
     
    +        
    +
             
     
         
    diff --git a/vector/src/main/res/layout/fragment_phonebook.xml b/vector/src/main/res/layout/fragment_phonebook.xml
    new file mode 100644
    index 0000000000..297201e2b1
    --- /dev/null
    +++ b/vector/src/main/res/layout/fragment_phonebook.xml
    @@ -0,0 +1,109 @@
    +
    +
    +
    +
    +    
    +
    +        
    +
    +            
    +
    +                
    +
    +                
    +
    +            
    +
    +        
    +
    +        
    +
    +            
    +
    +        
    +
    +        
    +
    +        
    +
    +    
    +
    +
    +
    diff --git a/vector/src/main/res/layout/item_contact_detail.xml b/vector/src/main/res/layout/item_contact_detail.xml
    new file mode 100644
    index 0000000000..6e44797baa
    --- /dev/null
    +++ b/vector/src/main/res/layout/item_contact_detail.xml
    @@ -0,0 +1,46 @@
    +
    +
    +
    +    
    +
    +    
    +
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/layout/item_contact_main.xml b/vector/src/main/res/layout/item_contact_main.xml
    new file mode 100644
    index 0000000000..c4482241ae
    --- /dev/null
    +++ b/vector/src/main/res/layout/item_contact_main.xml
    @@ -0,0 +1,38 @@
    +
    +
    +
    +    
    +
    +    
    +
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
    index 32de32e094..df596ae2d8 100644
    --- a/vector/src/main/res/values/strings.xml
    +++ b/vector/src/main/res/values/strings.xml
    @@ -2541,4 +2541,8 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
         Waiting for encryption history
     
         Save recovery key in
    +
    +    Add from my phone book
    +    Your phone book is empty
    +    Phone book
     
    
    From 6ceac578a33752a4360e9c4ae2c253f6f2c45800 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Wed, 8 Jul 2020 21:52:14 +0200
    Subject: [PATCH 096/122] Add checkbox to filter contacts with MatrixId only
    
    ---
     .../features/userdirectory/PhoneBookAction.kt |  1 +
     .../userdirectory/PhoneBookController.kt      | 63 ++++++++++---------
     .../userdirectory/PhoneBookFragment.kt        | 16 ++++-
     .../userdirectory/PhoneBookViewModel.kt       | 45 +++++++++----
     .../userdirectory/PhoneBookViewState.kt       | 18 +++---
     .../main/res/layout/fragment_phonebook.xml    | 17 ++++-
     .../src/main/res/layout/item_contact_main.xml |  5 +-
     7 files changed, 108 insertions(+), 57 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookAction.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookAction.kt
    index d3d534e694..a8c993e99d 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookAction.kt
    @@ -20,4 +20,5 @@ import im.vector.riotx.core.platform.VectorViewModelAction
     
     sealed class PhoneBookAction : VectorViewModelAction {
         data class FilterWith(val filter: String) : PhoneBookAction()
    +    data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : PhoneBookAction()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt
    index 39f00d6557..6a79a4b15d 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt
    @@ -52,11 +52,11 @@ class PhoneBookController @Inject constructor(
     
         override fun buildModels() {
             val currentState = state ?: return
    -        val hasSearch = currentState.searchTerm.isNotBlank()
    +        val hasSearch = currentState.searchTerm.isNotEmpty()
             when (val asyncMappedContacts = currentState.mappedContacts) {
                 is Uninitialized -> renderEmptyState(false)
                 is Loading       -> renderLoading()
    -            is Success       -> renderSuccess(currentState.filteredMappedContacts, hasSearch)
    +            is Success       -> renderSuccess(currentState.filteredMappedContacts, hasSearch, currentState.onlyBoundContacts)
                 is Fail          -> renderFailure(asyncMappedContacts.error)
             }
         }
    @@ -75,49 +75,54 @@ class PhoneBookController @Inject constructor(
         }
     
         private fun renderSuccess(mappedContacts: List,
    -                              hasSearch: Boolean) {
    +                              hasSearch: Boolean,
    +                              onlyBoundContacts: Boolean) {
             if (mappedContacts.isEmpty()) {
                 renderEmptyState(hasSearch)
             } else {
    -            renderContacts(mappedContacts)
    +            renderContacts(mappedContacts, onlyBoundContacts)
             }
         }
     
    -    private fun renderContacts(mappedContacts: List) {
    +    private fun renderContacts(mappedContacts: List, onlyBoundContacts: Boolean) {
             for (mappedContact in mappedContacts) {
                 contactItem {
                     id(mappedContact.id)
                     contact(mappedContact)
                     avatarRenderer(avatarRenderer)
                 }
    -            mappedContact.emails.forEach {
    -                contactDetailItem {
    -                    id("$mappedContact.id${it.email}")
    -                    threePid(it.email)
    -                    matrixId(it.matrixId)
    -                    clickListener {
    -                        if (it.matrixId != null) {
    -                            callback?.onMatrixIdClick(it.matrixId)
    -                        } else {
    -                            callback?.onThreePidClick(ThreePid.Email(it.email))
    +            mappedContact.emails
    +                    .filter { !onlyBoundContacts || it.matrixId != null }
    +                    .forEach {
    +                        contactDetailItem {
    +                            id("$mappedContact.id${it.email}")
    +                            threePid(it.email)
    +                            matrixId(it.matrixId)
    +                            clickListener {
    +                                if (it.matrixId != null) {
    +                                    callback?.onMatrixIdClick(it.matrixId)
    +                                } else {
    +                                    callback?.onThreePidClick(ThreePid.Email(it.email))
    +                                }
    +                            }
                             }
                         }
    -                }
    -            }
    -            mappedContact.msisdns.forEach {
    -                contactDetailItem {
    -                    id("$mappedContact.id${it.phoneNumber}")
    -                    threePid(it.phoneNumber)
    -                    matrixId(it.matrixId)
    -                    clickListener {
    -                        if (it.matrixId != null) {
    -                            callback?.onMatrixIdClick(it.matrixId)
    -                        } else {
    -                            callback?.onThreePidClick(ThreePid.Msisdn(it.phoneNumber))
    +            mappedContact.msisdns
    +                    .filter { !onlyBoundContacts || it.matrixId != null }
    +                    .forEach {
    +                        contactDetailItem {
    +                            id("$mappedContact.id${it.phoneNumber}")
    +                            threePid(it.phoneNumber)
    +                            matrixId(it.matrixId)
    +                            clickListener {
    +                                if (it.matrixId != null) {
    +                                    callback?.onMatrixIdClick(it.matrixId)
    +                                } else {
    +                                    callback?.onThreePidClick(ThreePid.Msisdn(it.phoneNumber))
    +                                }
    +                            }
                             }
                         }
    -                }
    -            }
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookFragment.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookFragment.kt
    index 9f1f8268c3..ac8d3290cb 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookFragment.kt
    @@ -18,8 +18,10 @@ package im.vector.riotx.features.userdirectory
     
     import android.os.Bundle
     import android.view.View
    +import androidx.core.view.isVisible
     import com.airbnb.mvrx.activityViewModel
     import com.airbnb.mvrx.withState
    +import com.jakewharton.rxbinding3.widget.checkedChanges
     import com.jakewharton.rxbinding3.widget.textChanges
     import im.vector.matrix.android.api.session.identity.ThreePid
     import im.vector.matrix.android.api.session.user.model.User
    @@ -50,9 +52,18 @@ class PhoneBookFragment @Inject constructor(
             sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
             setupRecyclerView()
             setupFilterView()
    +        setupOnlyBoundContactsView()
             setupCloseView()
         }
     
    +    private fun setupOnlyBoundContactsView() {
    +        phoneBookOnlyBoundContacts.checkedChanges()
    +                .subscribe {
    +                    phoneBookViewModel.handle(PhoneBookAction.OnlyBoundContacts(it))
    +                }
    +                .disposeOnDestroyView()
    +    }
    +
         private fun setupFilterView() {
             phoneBookFilter
                     .textChanges()
    @@ -81,8 +92,9 @@ class PhoneBookFragment @Inject constructor(
             }
         }
     
    -    override fun invalidate() = withState(phoneBookViewModel) {
    -        phoneBookController.setData(it)
    +    override fun invalidate() = withState(phoneBookViewModel) { state ->
    +        phoneBookOnlyBoundContacts.isVisible = state.isBoundRetrieved
    +        phoneBookController.setData(state)
         }
     
         override fun onMatrixIdClick(matrixId: String) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewModel.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewModel.kt
    index d894bbe908..d78932ccf2 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewModel.kt
    @@ -37,7 +37,9 @@ import im.vector.riotx.core.platform.EmptyViewEvents
     import im.vector.riotx.core.platform.VectorViewModel
     import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
     import im.vector.riotx.features.invite.InviteUsersToRoomActivity
    +import kotlinx.coroutines.Dispatchers
     import kotlinx.coroutines.launch
    +import timber.log.Timber
     
     private typealias PhoneBookSearch = String
     
    @@ -71,13 +73,12 @@ class PhoneBookViewModel @AssistedInject constructor(@Assisted
     
         private var allContacts: List = emptyList()
         private var mappedContacts: List = emptyList()
    -    private var foundThreePid: List = emptyList()
     
         init {
             loadContacts()
     
    -        selectSubscribe(PhoneBookViewState::searchTerm) {
    -            updateState()
    +        selectSubscribe(PhoneBookViewState::searchTerm, PhoneBookViewState::onlyBoundContacts) { _, _ ->
    +            updateFilteredMappedContacts()
             }
         }
     
    @@ -88,7 +89,7 @@ class PhoneBookViewModel @AssistedInject constructor(@Assisted
                 )
             }
     
    -        viewModelScope.launch {
    +        viewModelScope.launch(Dispatchers.IO) {
                 allContacts = contactsDataSource.getContacts()
                 mappedContacts = allContacts
     
    @@ -99,7 +100,7 @@ class PhoneBookViewModel @AssistedInject constructor(@Assisted
                 }
     
                 performLookup(allContacts)
    -            updateState()
    +            updateFilteredMappedContacts()
             }
         }
     
    @@ -111,24 +112,23 @@ class PhoneBookViewModel @AssistedInject constructor(@Assisted
                 }
                 session.identityService().lookUp(threePids, object : MatrixCallback> {
                     override fun onFailure(failure: Throwable) {
    -                    // Ignore?
    +                    // Ignore
    +                    Timber.w(failure, "Unable to perform the lookup")
                     }
     
                     override fun onSuccess(data: List) {
    -                    foundThreePid = data
    -
                         mappedContacts = allContacts.map { contactModel ->
                             contactModel.copy(
                                     emails = contactModel.emails.map { email ->
                                         email.copy(
    -                                            matrixId = foundThreePid
    +                                            matrixId = data
                                                         .firstOrNull { foundThreePid -> foundThreePid.threePid.value == email.email }
                                                         ?.matrixId
                                         )
                                     },
                                     msisdns = contactModel.msisdns.map { msisdn ->
                                         msisdn.copy(
    -                                            matrixId = foundThreePid
    +                                            matrixId = data
                                                         .firstOrNull { foundThreePid -> foundThreePid.threePid.value == msisdn.phoneNumber }
                                                         ?.matrixId
                                         )
    @@ -136,15 +136,25 @@ class PhoneBookViewModel @AssistedInject constructor(@Assisted
                             )
                         }
     
    -                    updateState()
    +                    setState {
    +                        copy(
    +                                isBoundRetrieved = true
    +                        )
    +                    }
    +
    +                    updateFilteredMappedContacts()
                     }
                 })
             }
         }
     
    -    private fun updateState() = withState { state ->
    +    private fun updateFilteredMappedContacts() = withState { state ->
             val filteredMappedContacts = mappedContacts
                     .filter { it.displayName.contains(state.searchTerm, true) }
    +                .filter { contactModel ->
    +                    !state.onlyBoundContacts
    +                            || contactModel.emails.any { it.matrixId != null } || contactModel.msisdns.any { it.matrixId != null }
    +                }
     
             setState {
                 copy(
    @@ -155,10 +165,19 @@ class PhoneBookViewModel @AssistedInject constructor(@Assisted
     
         override fun handle(action: PhoneBookAction) {
             when (action) {
    -            is PhoneBookAction.FilterWith -> handleFilterWith(action)
    +            is PhoneBookAction.FilterWith        -> handleFilterWith(action)
    +            is PhoneBookAction.OnlyBoundContacts -> handleOnlyBoundContacts(action)
             }.exhaustive
         }
     
    +    private fun handleOnlyBoundContacts(action: PhoneBookAction.OnlyBoundContacts) {
    +        setState {
    +            copy(
    +                    onlyBoundContacts = action.onlyBoundContacts
    +            )
    +        }
    +    }
    +
         private fun handleFilterWith(action: PhoneBookAction.FilterWith) {
             setState {
                 copy(
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewState.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewState.kt
    index 81709f84b4..bfca2bc6b0 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewState.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewState.kt
    @@ -22,14 +22,14 @@ import com.airbnb.mvrx.MvRxState
     import im.vector.riotx.core.contacts.ContactModel
     
     data class PhoneBookViewState(
    -        val searchTerm: String = "",
    +        // All the contacts on the phone
             val mappedContacts: Async> = Loading(),
    -        val filteredMappedContacts: List = emptyList()
    -        /*
    -        val knownUsers: Async> = Uninitialized,
    -        val directoryUsers: Async> = Uninitialized,
    -        val selectedUsers: Set = emptySet(),
    -        val createAndInviteState: Async = Uninitialized,
    -        val filterKnownUsersValue: Option = Option.empty()
    -         */
    +        // Use to filter contacts by display name
    +        val searchTerm: String = "",
    +        // Tru to display only bound contacts with their bound 2pid
    +        val onlyBoundContacts: Boolean = false,
    +        // All contacts, filtered by searchTerm and onlyBoundContacts
    +        val filteredMappedContacts: List = emptyList(),
    +        // True when the identity service has return some data
    +        val isBoundRetrieved: Boolean = false
     ) : MvRxState
    diff --git a/vector/src/main/res/layout/fragment_phonebook.xml b/vector/src/main/res/layout/fragment_phonebook.xml
    index 297201e2b1..14c44c11f0 100644
    --- a/vector/src/main/res/layout/fragment_phonebook.xml
    +++ b/vector/src/main/res/layout/fragment_phonebook.xml
    @@ -79,6 +79,20 @@
     
             
     
    +        
    +
             
    +            app:layout_constraintTop_toBottomOf="@+id/phoneBookOnlyBoundContacts" />
     
             
    +    android:paddingStart="8dp"
    +    android:paddingTop="12dp"
    +    android:paddingEnd="8dp">
     
         
    Date: Wed, 8 Jul 2020 22:09:39 +0200
    Subject: [PATCH 097/122] Fix a crash (#548)
    
    ---
     .../features/userdirectory/PhoneBookController.kt  | 14 ++++++++------
     1 file changed, 8 insertions(+), 6 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt
    index 6a79a4b15d..e4266183df 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt
    @@ -92,10 +92,11 @@ class PhoneBookController @Inject constructor(
                     avatarRenderer(avatarRenderer)
                 }
                 mappedContact.emails
    -                    .filter { !onlyBoundContacts || it.matrixId != null }
    -                    .forEach {
    +                    .forEachIndexed { index, it ->
    +                        if (onlyBoundContacts && it.matrixId == null) return@forEachIndexed
    +
                             contactDetailItem {
    -                            id("$mappedContact.id${it.email}")
    +                            id("${mappedContact.id}-$index-${it.email}")
                                 threePid(it.email)
                                 matrixId(it.matrixId)
                                 clickListener {
    @@ -108,10 +109,11 @@ class PhoneBookController @Inject constructor(
                             }
                         }
                 mappedContact.msisdns
    -                    .filter { !onlyBoundContacts || it.matrixId != null }
    -                    .forEach {
    +                    .forEachIndexed { index, it ->
    +                        if (onlyBoundContacts && it.matrixId == null) return@forEachIndexed
    +
                             contactDetailItem {
    -                            id("$mappedContact.id${it.phoneNumber}")
    +                            id("${mappedContact.id}-$index-${it.phoneNumber}")
                                 threePid(it.phoneNumber)
                                 matrixId(it.matrixId)
                                 clickListener {
    
    From cc4603b61f1a316d4e9ffd5eb6790790787b3f68 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Thu, 9 Jul 2020 09:55:33 +0200
    Subject: [PATCH 098/122] Rename classes
    
    ---
     .../riotx/core/contacts/ContactsDataSource.kt      |  8 ++++----
     .../contacts/{ContactModel.kt => MappedContact.kt} | 14 ++++++--------
     .../vector/riotx/features/home/AvatarRenderer.kt   | 10 +++++-----
     .../riotx/features/userdirectory/ContactItem.kt    |  8 ++++----
     .../features/userdirectory/PhoneBookController.kt  |  8 ++++----
     .../features/userdirectory/PhoneBookViewModel.kt   |  8 ++++----
     .../features/userdirectory/PhoneBookViewState.kt   |  6 +++---
     7 files changed, 30 insertions(+), 32 deletions(-)
     rename vector/src/main/java/im/vector/riotx/core/contacts/{ContactModel.kt => MappedContact.kt} (87%)
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt b/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt
    index 2160216d5d..b7762dd135 100644
    --- a/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt
    @@ -28,8 +28,8 @@ class ContactsDataSource @Inject constructor(
     ) {
     
         @WorkerThread
    -    fun getContacts(): List {
    -        val result = mutableListOf()
    +    fun getContacts(): List {
    +        val result = mutableListOf()
             val contentResolver = context.contentResolver
     
             contentResolver.query(
    @@ -56,7 +56,7 @@ class ContactsDataSource @Inject constructor(
                                 val id = cursor.getLong(ContactsContract.Contacts._ID) ?: continue
                                 val displayName = cursor.getString(ContactsContract.Contacts.DISPLAY_NAME) ?: continue
     
    -                            val currentContact = ContactModelBuilder(
    +                            val currentContact = MappedContactBuilder(
                                         id = id,
                                         displayName = displayName
                                 )
    @@ -108,7 +108,7 @@ class ContactsDataSource @Inject constructor(
                                             }
                                         }
     
    -                            result.add(currentContact.toContactModel())
    +                            result.add(currentContact.build())
                             }
                         }
                     }
    diff --git a/vector/src/main/java/im/vector/riotx/core/contacts/ContactModel.kt b/vector/src/main/java/im/vector/riotx/core/contacts/MappedContact.kt
    similarity index 87%
    rename from vector/src/main/java/im/vector/riotx/core/contacts/ContactModel.kt
    rename to vector/src/main/java/im/vector/riotx/core/contacts/MappedContact.kt
    index 589c3030c3..c89a3d4b01 100644
    --- a/vector/src/main/java/im/vector/riotx/core/contacts/ContactModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/contacts/MappedContact.kt
    @@ -18,18 +18,16 @@ package im.vector.riotx.core.contacts
     
     import android.net.Uri
     
    -/* TODO Rename to MxContact? */
    -
    -class ContactModelBuilder(
    +class MappedContactBuilder(
             val id: Long,
    -        val displayName: String) {
    -
    +        val displayName: String
    +) {
         var photoURI: Uri? = null
         val msisdns = mutableListOf()
         val emails = mutableListOf()
     
    -    fun toContactModel(): ContactModel {
    -        return ContactModel(
    +    fun build(): MappedContact {
    +        return MappedContact(
                     id = id,
                     displayName = displayName,
                     photoURI = photoURI,
    @@ -39,7 +37,7 @@ class ContactModelBuilder(
         }
     }
     
    -data class ContactModel(
    +data class MappedContact(
             val id: Long,
             val displayName: String,
             val photoURI: Uri? = null,
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
    index e0d41ca445..3bf2f13d48 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
    @@ -30,7 +30,7 @@ import com.bumptech.glide.request.target.DrawableImageViewTarget
     import com.bumptech.glide.request.target.Target
     import im.vector.matrix.android.api.session.content.ContentUrlResolver
     import im.vector.matrix.android.api.util.MatrixItem
    -import im.vector.riotx.core.contacts.ContactModel
    +import im.vector.riotx.core.contacts.MappedContact
     import im.vector.riotx.core.di.ActiveSessionHolder
     import im.vector.riotx.core.glide.GlideApp
     import im.vector.riotx.core.glide.GlideRequest
    @@ -65,17 +65,17 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
         }
     
         @UiThread
    -    fun render(contactModel: ContactModel, imageView: ImageView) {
    +    fun render(mappedContact: MappedContact, imageView: ImageView) {
             // Create a Fake MatrixItem, for the placeholder
             val matrixItem = MatrixItem.UserItem(
                     // Need an id starting with @
    -                id = "@${contactModel.displayName}",
    -                displayName = contactModel.displayName
    +                id = "@${mappedContact.displayName}",
    +                displayName = mappedContact.displayName
             )
     
             val placeholder = getPlaceholderDrawable(imageView.context, matrixItem)
             GlideApp.with(imageView)
    -                .load(contactModel.photoURI)
    +                .load(mappedContact.photoURI)
                     .apply(RequestOptions.circleCropTransform())
                     .placeholder(placeholder)
                     .into(imageView)
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/ContactItem.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/ContactItem.kt
    index 67d762b4b2..157ce9c7f6 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/ContactItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/ContactItem.kt
    @@ -21,7 +21,7 @@ import android.widget.TextView
     import com.airbnb.epoxy.EpoxyAttribute
     import com.airbnb.epoxy.EpoxyModelClass
     import im.vector.riotx.R
    -import im.vector.riotx.core.contacts.ContactModel
    +import im.vector.riotx.core.contacts.MappedContact
     import im.vector.riotx.core.epoxy.VectorEpoxyHolder
     import im.vector.riotx.core.epoxy.VectorEpoxyModel
     import im.vector.riotx.features.home.AvatarRenderer
    @@ -30,13 +30,13 @@ import im.vector.riotx.features.home.AvatarRenderer
     abstract class ContactItem : VectorEpoxyModel() {
     
         @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
    -    @EpoxyAttribute lateinit var contact: ContactModel
    +    @EpoxyAttribute lateinit var mappedContact: MappedContact
     
         override fun bind(holder: Holder) {
             super.bind(holder)
             // If name is empty, use userId as name and force it being centered
    -        holder.nameView.text = contact.displayName
    -        avatarRenderer.render(contact, holder.avatarImageView)
    +        holder.nameView.text = mappedContact.displayName
    +        avatarRenderer.render(mappedContact, holder.avatarImageView)
         }
     
         class Holder : VectorEpoxyHolder() {
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt
    index e4266183df..4f33a4fa8f 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt
    @@ -23,7 +23,7 @@ import com.airbnb.mvrx.Success
     import com.airbnb.mvrx.Uninitialized
     import im.vector.matrix.android.api.session.identity.ThreePid
     import im.vector.riotx.R
    -import im.vector.riotx.core.contacts.ContactModel
    +import im.vector.riotx.core.contacts.MappedContact
     import im.vector.riotx.core.epoxy.errorWithRetryItem
     import im.vector.riotx.core.epoxy.loadingItem
     import im.vector.riotx.core.epoxy.noResultItem
    @@ -74,7 +74,7 @@ class PhoneBookController @Inject constructor(
             }
         }
     
    -    private fun renderSuccess(mappedContacts: List,
    +    private fun renderSuccess(mappedContacts: List,
                                   hasSearch: Boolean,
                                   onlyBoundContacts: Boolean) {
             if (mappedContacts.isEmpty()) {
    @@ -84,11 +84,11 @@ class PhoneBookController @Inject constructor(
             }
         }
     
    -    private fun renderContacts(mappedContacts: List, onlyBoundContacts: Boolean) {
    +    private fun renderContacts(mappedContacts: List, onlyBoundContacts: Boolean) {
             for (mappedContact in mappedContacts) {
                 contactItem {
                     id(mappedContact.id)
    -                contact(mappedContact)
    +                mappedContact(mappedContact)
                     avatarRenderer(avatarRenderer)
                 }
                 mappedContact.emails
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewModel.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewModel.kt
    index d78932ccf2..d76c36847d 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewModel.kt
    @@ -30,8 +30,8 @@ import im.vector.matrix.android.api.MatrixCallback
     import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.identity.FoundThreePid
     import im.vector.matrix.android.api.session.identity.ThreePid
    -import im.vector.riotx.core.contacts.ContactModel
     import im.vector.riotx.core.contacts.ContactsDataSource
    +import im.vector.riotx.core.contacts.MappedContact
     import im.vector.riotx.core.extensions.exhaustive
     import im.vector.riotx.core.platform.EmptyViewEvents
     import im.vector.riotx.core.platform.VectorViewModel
    @@ -71,8 +71,8 @@ class PhoneBookViewModel @AssistedInject constructor(@Assisted
             }
         }
     
    -    private var allContacts: List = emptyList()
    -    private var mappedContacts: List = emptyList()
    +    private var allContacts: List = emptyList()
    +    private var mappedContacts: List = emptyList()
     
         init {
             loadContacts()
    @@ -104,7 +104,7 @@ class PhoneBookViewModel @AssistedInject constructor(@Assisted
             }
         }
     
    -    private fun performLookup(data: List) {
    +    private fun performLookup(data: List) {
             viewModelScope.launch {
                 val threePids = data.flatMap { contact ->
                     contact.emails.map { ThreePid.Email(it.email) } +
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewState.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewState.kt
    index bfca2bc6b0..60603b8785 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewState.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewState.kt
    @@ -19,17 +19,17 @@ package im.vector.riotx.features.userdirectory
     import com.airbnb.mvrx.Async
     import com.airbnb.mvrx.Loading
     import com.airbnb.mvrx.MvRxState
    -import im.vector.riotx.core.contacts.ContactModel
    +import im.vector.riotx.core.contacts.MappedContact
     
     data class PhoneBookViewState(
             // All the contacts on the phone
    -        val mappedContacts: Async> = Loading(),
    +        val mappedContacts: Async> = Loading(),
             // Use to filter contacts by display name
             val searchTerm: String = "",
             // Tru to display only bound contacts with their bound 2pid
             val onlyBoundContacts: Boolean = false,
             // All contacts, filtered by searchTerm and onlyBoundContacts
    -        val filteredMappedContacts: List = emptyList(),
    +        val filteredMappedContacts: List = emptyList(),
             // True when the identity service has return some data
             val isBoundRetrieved: Boolean = false
     ) : MvRxState
    
    From 327a596de5f254616420f3b7f9be6fa8d99b2491 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Thu, 9 Jul 2020 10:02:04 +0200
    Subject: [PATCH 099/122] Move classes
    
    ---
     .../src/main/java/im/vector/riotx/core/di/FragmentModule.kt | 2 +-
     .../riotx/features/createdirect/CreateDirectRoomActivity.kt | 2 +-
     .../riotx/features/invite/InviteUsersToRoomActivity.kt      | 4 ++--
     .../{userdirectory => phonebook}/ContactDetailItem.kt       | 2 +-
     .../features/{userdirectory => phonebook}/ContactItem.kt    | 2 +-
     .../{userdirectory => phonebook}/PhoneBookAction.kt         | 2 +-
     .../{userdirectory => phonebook}/PhoneBookController.kt     | 2 +-
     .../{userdirectory => phonebook}/PhoneBookFragment.kt       | 6 +++++-
     .../{userdirectory => phonebook}/PhoneBookViewModel.kt      | 2 +-
     .../{userdirectory => phonebook}/PhoneBookViewState.kt      | 2 +-
     10 files changed, 15 insertions(+), 11 deletions(-)
     rename vector/src/main/java/im/vector/riotx/features/{userdirectory => phonebook}/ContactDetailItem.kt (97%)
     rename vector/src/main/java/im/vector/riotx/features/{userdirectory => phonebook}/ContactItem.kt (97%)
     rename vector/src/main/java/im/vector/riotx/features/{userdirectory => phonebook}/PhoneBookAction.kt (94%)
     rename vector/src/main/java/im/vector/riotx/features/{userdirectory => phonebook}/PhoneBookController.kt (99%)
     rename vector/src/main/java/im/vector/riotx/features/{userdirectory => phonebook}/PhoneBookFragment.kt (92%)
     rename vector/src/main/java/im/vector/riotx/features/{userdirectory => phonebook}/PhoneBookViewModel.kt (99%)
     rename vector/src/main/java/im/vector/riotx/features/{userdirectory => phonebook}/PhoneBookViewState.kt (96%)
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
    index 0201a44096..f5f236364f 100644
    --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
    @@ -103,7 +103,7 @@ import im.vector.riotx.features.share.IncomingShareFragment
     import im.vector.riotx.features.signout.soft.SoftLogoutFragment
     import im.vector.riotx.features.terms.ReviewTermsFragment
     import im.vector.riotx.features.userdirectory.KnownUsersFragment
    -import im.vector.riotx.features.userdirectory.PhoneBookFragment
    +import im.vector.riotx.features.phonebook.PhoneBookFragment
     import im.vector.riotx.features.userdirectory.UserDirectoryFragment
     import im.vector.riotx.features.widgets.WidgetFragment
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt
    index 973a4b6f16..256d9902f6 100644
    --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt
    @@ -40,7 +40,7 @@ import im.vector.riotx.core.platform.SimpleFragmentActivity
     import im.vector.riotx.core.platform.WaitingViewData
     import im.vector.riotx.features.userdirectory.KnownUsersFragment
     import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs
    -import im.vector.riotx.features.userdirectory.PhoneBookViewModel
    +import im.vector.riotx.features.phonebook.PhoneBookViewModel
     import im.vector.riotx.features.userdirectory.UserDirectoryFragment
     import im.vector.riotx.features.userdirectory.UserDirectorySharedAction
     import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
    diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt
    index af0e974c8a..4d869aacee 100644
    --- a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt
    @@ -36,8 +36,8 @@ import im.vector.riotx.core.platform.WaitingViewData
     import im.vector.riotx.core.utils.toast
     import im.vector.riotx.features.userdirectory.KnownUsersFragment
     import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs
    -import im.vector.riotx.features.userdirectory.PhoneBookFragment
    -import im.vector.riotx.features.userdirectory.PhoneBookViewModel
    +import im.vector.riotx.features.phonebook.PhoneBookFragment
    +import im.vector.riotx.features.phonebook.PhoneBookViewModel
     import im.vector.riotx.features.userdirectory.UserDirectoryFragment
     import im.vector.riotx.features.userdirectory.UserDirectorySharedAction
     import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/ContactDetailItem.kt b/vector/src/main/java/im/vector/riotx/features/phonebook/ContactDetailItem.kt
    similarity index 97%
    rename from vector/src/main/java/im/vector/riotx/features/userdirectory/ContactDetailItem.kt
    rename to vector/src/main/java/im/vector/riotx/features/phonebook/ContactDetailItem.kt
    index df29545201..3c739b7829 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/ContactDetailItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/phonebook/ContactDetailItem.kt
    @@ -14,7 +14,7 @@
      * limitations under the License.
      */
     
    -package im.vector.riotx.features.userdirectory
    +package im.vector.riotx.features.phonebook
     
     import android.widget.TextView
     import com.airbnb.epoxy.EpoxyAttribute
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/ContactItem.kt b/vector/src/main/java/im/vector/riotx/features/phonebook/ContactItem.kt
    similarity index 97%
    rename from vector/src/main/java/im/vector/riotx/features/userdirectory/ContactItem.kt
    rename to vector/src/main/java/im/vector/riotx/features/phonebook/ContactItem.kt
    index 157ce9c7f6..b962b0a40d 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/ContactItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/phonebook/ContactItem.kt
    @@ -14,7 +14,7 @@
      * limitations under the License.
      */
     
    -package im.vector.riotx.features.userdirectory
    +package im.vector.riotx.features.phonebook
     
     import android.widget.ImageView
     import android.widget.TextView
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookAction.kt b/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookAction.kt
    similarity index 94%
    rename from vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookAction.kt
    rename to vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookAction.kt
    index a8c993e99d..3a5eb4d7ff 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookAction.kt
    @@ -14,7 +14,7 @@
      * limitations under the License.
      */
     
    -package im.vector.riotx.features.userdirectory
    +package im.vector.riotx.features.phonebook
     
     import im.vector.riotx.core.platform.VectorViewModelAction
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt b/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookController.kt
    similarity index 99%
    rename from vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt
    rename to vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookController.kt
    index 4f33a4fa8f..0fe940b21e 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookController.kt
    @@ -14,7 +14,7 @@
      * limitations under the License.
      */
     
    -package im.vector.riotx.features.userdirectory
    +package im.vector.riotx.features.phonebook
     
     import com.airbnb.epoxy.EpoxyController
     import com.airbnb.mvrx.Fail
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookFragment.kt b/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookFragment.kt
    similarity index 92%
    rename from vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookFragment.kt
    rename to vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookFragment.kt
    index ac8d3290cb..0d268e4dc7 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookFragment.kt
    @@ -14,7 +14,7 @@
      * limitations under the License.
      */
     
    -package im.vector.riotx.features.userdirectory
    +package im.vector.riotx.features.phonebook
     
     import android.os.Bundle
     import android.view.View
    @@ -30,6 +30,10 @@ import im.vector.riotx.core.extensions.cleanup
     import im.vector.riotx.core.extensions.configureWith
     import im.vector.riotx.core.extensions.hideKeyboard
     import im.vector.riotx.core.platform.VectorBaseFragment
    +import im.vector.riotx.features.userdirectory.UserDirectoryAction
    +import im.vector.riotx.features.userdirectory.UserDirectorySharedAction
    +import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
    +import im.vector.riotx.features.userdirectory.UserDirectoryViewModel
     import kotlinx.android.synthetic.main.fragment_phonebook.*
     import java.util.concurrent.TimeUnit
     import javax.inject.Inject
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewModel.kt b/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookViewModel.kt
    similarity index 99%
    rename from vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewModel.kt
    rename to vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookViewModel.kt
    index d76c36847d..a609b63b67 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookViewModel.kt
    @@ -14,7 +14,7 @@
      * limitations under the License.
      */
     
    -package im.vector.riotx.features.userdirectory
    +package im.vector.riotx.features.phonebook
     
     import androidx.fragment.app.FragmentActivity
     import androidx.lifecycle.viewModelScope
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewState.kt b/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookViewState.kt
    similarity index 96%
    rename from vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewState.kt
    rename to vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookViewState.kt
    index 60603b8785..d4a578cd8a 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/PhoneBookViewState.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookViewState.kt
    @@ -14,7 +14,7 @@
      * limitations under the License.
      */
     
    -package im.vector.riotx.features.userdirectory
    +package im.vector.riotx.features.phonebook
     
     import com.airbnb.mvrx.Async
     import com.airbnb.mvrx.Loading
    
    From f7145662008d3b66e4c3deade9c21171b752b605 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Thu, 9 Jul 2020 10:43:08 +0200
    Subject: [PATCH 100/122] use projection to gain 25% of time
    
    ---
     .../riotx/core/contacts/ContactsDataSource.kt | 144 +++++++++---------
     1 file changed, 73 insertions(+), 71 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt b/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt
    index b7762dd135..f694a9e3f0 100644
    --- a/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt
    @@ -21,7 +21,9 @@ import android.database.Cursor
     import android.net.Uri
     import android.provider.ContactsContract
     import androidx.annotation.WorkerThread
    +import timber.log.Timber
     import javax.inject.Inject
    +import kotlin.system.measureTimeMillis
     
     class ContactsDataSource @Inject constructor(
             private val context: Context
    @@ -32,86 +34,86 @@ class ContactsDataSource @Inject constructor(
             val result = mutableListOf()
             val contentResolver = context.contentResolver
     
    -        contentResolver.query(
    -                ContactsContract.Contacts.CONTENT_URI,
    -                null,
    -                /* TODO
    -                arrayOf(
    -                        ContactsContract.Contacts._ID,
    -                        ContactsContract.Data.DISPLAY_NAME,
    -                        ContactsContract.Data.PHOTO_URI,
    -                        ContactsContract.Data.MIMETYPE,
    -                        ContactsContract.CommonDataKinds.Phone.NUMBER,
    -                        ContactsContract.CommonDataKinds.Email.ADDRESS
    -                ),
    -                 */
    -                null,
    -                null,
    -                // Sort by Display name
    -                ContactsContract.Data.DISPLAY_NAME
    -        )
    -                ?.use { cursor ->
    -                    if (cursor.count > 0) {
    -                        while (cursor.moveToNext()) {
    -                            val id = cursor.getLong(ContactsContract.Contacts._ID) ?: continue
    -                            val displayName = cursor.getString(ContactsContract.Contacts.DISPLAY_NAME) ?: continue
    +        measureTimeMillis {
    +            contentResolver.query(
    +                    ContactsContract.Contacts.CONTENT_URI,
    +                    arrayOf(
    +                            ContactsContract.Contacts._ID,
    +                            ContactsContract.Data.DISPLAY_NAME,
    +                            ContactsContract.Data.PHOTO_URI
    +                    ),
    +                    null,
    +                    null,
    +                    // Sort by Display name
    +                    ContactsContract.Data.DISPLAY_NAME
    +            )
    +                    ?.use { cursor ->
    +                        if (cursor.count > 0) {
    +                            while (cursor.moveToNext()) {
    +                                val id = cursor.getLong(ContactsContract.Contacts._ID) ?: continue
    +                                val displayName = cursor.getString(ContactsContract.Contacts.DISPLAY_NAME) ?: continue
     
    -                            val currentContact = MappedContactBuilder(
    -                                    id = id,
    -                                    displayName = displayName
    -                            )
    +                                val currentContact = MappedContactBuilder(
    +                                        id = id,
    +                                        displayName = displayName
    +                                )
     
    -                            cursor.getString(ContactsContract.Data.PHOTO_URI)
    -                                    ?.let { Uri.parse(it) }
    -                                    ?.let { currentContact.photoURI = it }
    +                                cursor.getString(ContactsContract.Data.PHOTO_URI)
    +                                        ?.let { Uri.parse(it) }
    +                                        ?.let { currentContact.photoURI = it }
     
    -                            // Get the phone numbers
    -                            contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
    -                                    null,
    -                                    ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
    -                                    arrayOf(id.toString()),
    -                                    null)
    -                                    ?.use { innerCursor ->
    -                                        while (innerCursor.moveToNext()) {
    -                                            innerCursor.getString(ContactsContract.CommonDataKinds.Phone.NUMBER)
    -                                                    ?.let {
    -                                                        currentContact.msisdns.add(
    -                                                                MappedMsisdn(
    -                                                                        phoneNumber = it,
    -                                                                        matrixId = null
    -                                                                )
    -                                                        )
    -                                                    }
    +                                // Get the phone numbers
    +                                contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
    +                                        arrayOf(
    +                                                ContactsContract.CommonDataKinds.Phone.NUMBER
    +                                        ),
    +                                        ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
    +                                        arrayOf(id.toString()),
    +                                        null)
    +                                        ?.use { innerCursor ->
    +                                            while (innerCursor.moveToNext()) {
    +                                                innerCursor.getString(ContactsContract.CommonDataKinds.Phone.NUMBER)
    +                                                        ?.let {
    +                                                            currentContact.msisdns.add(
    +                                                                    MappedMsisdn(
    +                                                                            phoneNumber = it,
    +                                                                            matrixId = null
    +                                                                    )
    +                                                            )
    +                                                        }
    +                                            }
                                             }
    -                                    }
     
    -                            // Get Emails
    -                            contentResolver.query(
    -                                    ContactsContract.CommonDataKinds.Email.CONTENT_URI,
    -                                    null,
    -                                    ContactsContract.CommonDataKinds.Email.CONTACT_ID + " = ?",
    -                                    arrayOf(id.toString()),
    -                                    null)
    -                                    ?.use { innerCursor ->
    -                                        while (innerCursor.moveToNext()) {
    -                                            // This would allow you get several email addresses
    -                                            // if the email addresses were stored in an array
    -                                            innerCursor.getString(ContactsContract.CommonDataKinds.Email.DATA)
    -                                                    ?.let {
    -                                                        currentContact.emails.add(
    -                                                                MappedEmail(
    -                                                                        email = it,
    -                                                                        matrixId = null
    -                                                                )
    -                                                        )
    -                                                    }
    +                                // Get Emails
    +                                contentResolver.query(
    +                                        ContactsContract.CommonDataKinds.Email.CONTENT_URI,
    +                                        arrayOf(
    +                                                ContactsContract.CommonDataKinds.Email.DATA
    +                                        ),
    +                                        ContactsContract.CommonDataKinds.Email.CONTACT_ID + " = ?",
    +                                        arrayOf(id.toString()),
    +                                        null)
    +                                        ?.use { innerCursor ->
    +                                            while (innerCursor.moveToNext()) {
    +                                                // This would allow you get several email addresses
    +                                                // if the email addresses were stored in an array
    +                                                innerCursor.getString(ContactsContract.CommonDataKinds.Email.DATA)
    +                                                        ?.let {
    +                                                            currentContact.emails.add(
    +                                                                    MappedEmail(
    +                                                                            email = it,
    +                                                                            matrixId = null
    +                                                                    )
    +                                                            )
    +                                                        }
    +                                            }
                                             }
    -                                    }
     
    -                            result.add(currentContact.build())
    +                                result.add(currentContact.build())
    +                            }
                             }
                         }
    -                }
    +        }.also { Timber.d("Took ${it}ms to fetch ${result.size} contact(s)") }
     
             return result
                     .filter { it.emails.isNotEmpty() || it.msisdns.isNotEmpty() }
    
    From 6c0bb2a949a4709bbe0fcce8f68cbc9bab960dfe Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Thu, 9 Jul 2020 11:27:45 +0200
    Subject: [PATCH 101/122] Add 3Pid to the list. Not compiling, I have to modify
     CreateRoomParam
    
    ---
     .../session/room/model/create/Invite3Pid.kt   |  8 ++++++
     .../createdirect/CreateDirectRoomAction.kt    |  4 +--
     .../createdirect/CreateDirectRoomViewModel.kt |  4 +--
     .../features/phonebook/PhoneBookFragment.kt   |  5 ++--
     .../userdirectory/DirectoryUsersController.kt |  2 +-
     .../userdirectory/KnownUsersController.kt     |  2 +-
     .../userdirectory/KnownUsersFragment.kt       | 13 ++++++----
     .../features/userdirectory/PendingInvitee.kt  | 25 +++++++++++++++++++
     .../userdirectory/UserDirectoryAction.kt      |  7 ++----
     .../userdirectory/UserDirectoryFragment.kt    |  5 ++--
     .../UserDirectorySharedAction.kt              |  3 +--
     .../userdirectory/UserDirectoryViewModel.kt   | 19 +++++++-------
     .../userdirectory/UserDirectoryViewState.kt   | 12 ++++++++-
     13 files changed, 76 insertions(+), 33 deletions(-)
     create mode 100644 vector/src/main/java/im/vector/riotx/features/userdirectory/PendingInvitee.kt
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/Invite3Pid.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/Invite3Pid.kt
    index 8e3386080f..66c8f1b2e8 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/Invite3Pid.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/Invite3Pid.kt
    @@ -28,6 +28,14 @@ data class Invite3Pid(
             @Json(name = "id_server")
             val idServer: String,
     
    +        /**
    +         * Required.
    +         * An access token previously registered with the identity server. Servers can treat this as optional to
    +         * distinguish between r0.5-compatible clients and this specification version.
    +         */
    +        @Json(name = "id_access_token")
    +        val idAccessToken: String,
    +
             /**
              * Required.
              * The kind of address being passed in the address field, for example email.
    diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt
    index f995f82ff7..2af01b8964 100644
    --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt
    @@ -16,9 +16,9 @@
     
     package im.vector.riotx.features.createdirect
     
    -import im.vector.matrix.android.api.session.user.model.User
     import im.vector.riotx.core.platform.VectorViewModelAction
    +import im.vector.riotx.features.userdirectory.PendingInvitee
     
     sealed class CreateDirectRoomAction : VectorViewModelAction {
    -    data class CreateRoomAndInviteSelectedUsers(val selectedUsers: Set) : CreateDirectRoomAction()
    +    data class CreateRoomAndInviteSelectedUsers(val selectedUsers: Set) : CreateDirectRoomAction()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt
    index 1800759da6..90340c5cd8 100644
    --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt
    @@ -23,9 +23,9 @@ import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
    -import im.vector.matrix.android.api.session.user.model.User
     import im.vector.matrix.rx.rx
     import im.vector.riotx.core.platform.VectorViewModel
    +import im.vector.riotx.features.userdirectory.PendingInvitee
     
     class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
                                                                 initialState: CreateDirectRoomViewState,
    @@ -52,7 +52,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
             }
         }
     
    -    private fun createRoomAndInviteSelectedUsers(selectedUsers: Set) {
    +    private fun createRoomAndInviteSelectedUsers(selectedUsers: Set) {
             val roomParams = CreateRoomParams(
                     invitedUserIds = selectedUsers.map { it.userId }
             )
    diff --git a/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookFragment.kt b/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookFragment.kt
    index 0d268e4dc7..1da9c6c306 100644
    --- a/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookFragment.kt
    @@ -30,6 +30,7 @@ import im.vector.riotx.core.extensions.cleanup
     import im.vector.riotx.core.extensions.configureWith
     import im.vector.riotx.core.extensions.hideKeyboard
     import im.vector.riotx.core.platform.VectorBaseFragment
    +import im.vector.riotx.features.userdirectory.PendingInvitee
     import im.vector.riotx.features.userdirectory.UserDirectoryAction
     import im.vector.riotx.features.userdirectory.UserDirectorySharedAction
     import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
    @@ -103,13 +104,13 @@ class PhoneBookFragment @Inject constructor(
     
         override fun onMatrixIdClick(matrixId: String) {
             view?.hideKeyboard()
    -        viewModel.handle(UserDirectoryAction.SelectUser(User(matrixId)))
    +        viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId))))
             sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
         }
     
         override fun onThreePidClick(threePid: ThreePid) {
             view?.hideKeyboard()
    -        viewModel.handle(UserDirectoryAction.SelectThreePid(threePid))
    +        viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid)))
             sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/DirectoryUsersController.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/DirectoryUsersController.kt
    index 9d11387fe8..d5fc34728a 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/DirectoryUsersController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/DirectoryUsersController.kt
    @@ -60,7 +60,7 @@ class DirectoryUsersController @Inject constructor(private val session: Session,
                 is Loading       -> renderLoading()
                 is Success       -> renderSuccess(
                         computeUsersList(asyncUsers(), currentState.directorySearchTerm),
    -                    currentState.selectedUsers.map { it.userId },
    +                    currentState.getSelectedMatrixId(),
                         hasSearch
                 )
                 is Fail          -> renderFailure(asyncUsers.error)
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersController.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersController.kt
    index 7a1ad49b8c..c78368f01b 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersController.kt
    @@ -51,7 +51,7 @@ class KnownUsersController @Inject constructor(private val session: Session,
     
         fun setData(state: UserDirectoryViewState) {
             this.isFiltering = !state.filterKnownUsersValue.isEmpty()
    -        val newSelection = state.selectedUsers.map { it.userId }
    +        val newSelection = state.getSelectedMatrixId()
             this.users = state.knownUsers
             if (newSelection != selectedUsers) {
                 this.selectedUsers = newSelection
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt
    index d681e5d92f..dc7ec5ee04 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt
    @@ -139,7 +139,7 @@ class KnownUsersFragment @Inject constructor(
             knownUsersController.setData(it)
         }
     
    -    private fun renderSelectedUsers(selectedUsers: Set) {
    +    private fun renderSelectedUsers(selectedUsers: Set) {
             invalidateOptionsMenu()
     
             val currentNumberOfChips = chipGroup.childCount
    @@ -156,22 +156,25 @@ class KnownUsersFragment @Inject constructor(
             }
         }
     
    -    private fun addChipToGroup(user: User) {
    +    private fun addChipToGroup(pendingInvitee: PendingInvitee) {
             val chip = Chip(requireContext())
             chip.setChipBackgroundColorResource(android.R.color.transparent)
             chip.chipStrokeWidth = dimensionConverter.dpToPx(1).toFloat()
    -        chip.text = user.getBestName()
    +        chip.text = when (pendingInvitee) {
    +            is PendingInvitee.UserPendingInvitee     -> pendingInvitee.user.getBestName()
    +            is PendingInvitee.ThreePidPendingInvitee -> pendingInvitee.threePid.value
    +        }
             chip.isClickable = true
             chip.isCheckable = false
             chip.isCloseIconVisible = true
             chipGroup.addView(chip)
             chip.setOnCloseIconClickListener {
    -            viewModel.handle(UserDirectoryAction.RemoveSelectedUser(user))
    +            viewModel.handle(UserDirectoryAction.RemovePendingInvitee(pendingInvitee))
             }
         }
     
         override fun onItemClick(user: User) {
             view?.hideKeyboard()
    -        viewModel.handle(UserDirectoryAction.SelectUser(user))
    +        viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(user)))
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PendingInvitee.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/PendingInvitee.kt
    new file mode 100644
    index 0000000000..b213061e4a
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/PendingInvitee.kt
    @@ -0,0 +1,25 @@
    +/*
    + * Copyright (c) 2020 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.riotx.features.userdirectory
    +
    +import im.vector.matrix.android.api.session.identity.ThreePid
    +import im.vector.matrix.android.api.session.user.model.User
    +
    +sealed class PendingInvitee {
    +    data class UserPendingInvitee(val user: User): PendingInvitee()
    +    data class ThreePidPendingInvitee(val threePid: ThreePid): PendingInvitee()
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryAction.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryAction.kt
    index 3051e14bea..fde71cff5c 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryAction.kt
    @@ -16,15 +16,12 @@
     
     package im.vector.riotx.features.userdirectory
     
    -import im.vector.matrix.android.api.session.identity.ThreePid
    -import im.vector.matrix.android.api.session.user.model.User
     import im.vector.riotx.core.platform.VectorViewModelAction
     
     sealed class UserDirectoryAction : VectorViewModelAction {
         data class FilterKnownUsers(val value: String) : UserDirectoryAction()
         data class SearchDirectoryUsers(val value: String) : UserDirectoryAction()
         object ClearFilterKnownUsers : UserDirectoryAction()
    -    data class SelectUser(val user: User) : UserDirectoryAction()
    -    data class SelectThreePid(val threePid: ThreePid) : UserDirectoryAction()
    -    data class RemoveSelectedUser(val user: User) : UserDirectoryAction()
    +    data class SelectPendingInvitee(val pendingInvitee: PendingInvitee) : UserDirectoryAction()
    +    data class RemovePendingInvitee(val pendingInvitee: PendingInvitee) : UserDirectoryAction()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryFragment.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryFragment.kt
    index 12de191b54..8dd4025350 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryFragment.kt
    @@ -21,7 +21,6 @@ import android.view.View
     import com.airbnb.mvrx.activityViewModel
     import com.airbnb.mvrx.withState
     import com.jakewharton.rxbinding3.widget.textChanges
    -import im.vector.matrix.android.api.session.user.model.User
     import im.vector.riotx.R
     import im.vector.riotx.core.extensions.cleanup
     import im.vector.riotx.core.extensions.configureWith
    @@ -82,9 +81,9 @@ class UserDirectoryFragment @Inject constructor(
             directRoomController.setData(it)
         }
     
    -    override fun onItemClick(user: User) {
    +    override fun onItemClick(pendingInvitee: PendingInvitee) {
             view?.hideKeyboard()
    -        viewModel.handle(UserDirectoryAction.SelectUser(user))
    +        viewModel.handle(UserDirectoryAction.SelectPendingInvitee(pendingInvitee))
             sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
         }
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedAction.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedAction.kt
    index b071b6f7a8..7506b97be3 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedAction.kt
    @@ -16,7 +16,6 @@
     
     package im.vector.riotx.features.userdirectory
     
    -import im.vector.matrix.android.api.session.user.model.User
     import im.vector.riotx.core.platform.VectorSharedAction
     
     sealed class UserDirectorySharedAction : VectorSharedAction {
    @@ -24,5 +23,5 @@ sealed class UserDirectorySharedAction : VectorSharedAction {
         object OpenPhoneBook : UserDirectorySharedAction()
         object Close : UserDirectorySharedAction()
         object GoBack : UserDirectorySharedAction()
    -    data class OnMenuItemSelected(val itemId: Int, val selectedUsers: Set) : UserDirectorySharedAction()
    +    data class OnMenuItemSelected(val itemId: Int, val selectedUsers: Set) : UserDirectorySharedAction()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewModel.kt
    index 3111a86bf7..d7fb800aa4 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewModel.kt
    @@ -28,6 +28,7 @@ import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.util.toMatrixItem
     import im.vector.matrix.rx.rx
    +import im.vector.riotx.core.extensions.exhaustive
     import im.vector.riotx.core.extensions.toggle
     import im.vector.riotx.core.platform.VectorViewModel
     import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
    @@ -59,9 +60,9 @@ class UserDirectoryViewModel @AssistedInject constructor(@Assisted
                     is FragmentViewModelContext -> (viewModelContext.fragment() as KnownUsersFragment).userDirectoryViewModelFactory.create(state)
                     is ActivityViewModelContext -> {
                         when (viewModelContext.activity()) {
    -                        is CreateDirectRoomActivity -> viewModelContext.activity().userDirectoryViewModelFactory.create(state)
    +                        is CreateDirectRoomActivity  -> viewModelContext.activity().userDirectoryViewModelFactory.create(state)
                             is InviteUsersToRoomActivity -> viewModelContext.activity().userDirectoryViewModelFactory.create(state)
    -                        else                        -> error("Wrong activity or fragment")
    +                        else                         -> error("Wrong activity or fragment")
                         }
                     }
                     else                        -> error("Wrong activity or fragment")
    @@ -79,20 +80,20 @@ class UserDirectoryViewModel @AssistedInject constructor(@Assisted
                 is UserDirectoryAction.FilterKnownUsers      -> knownUsersFilter.accept(Option.just(action.value))
                 is UserDirectoryAction.ClearFilterKnownUsers -> knownUsersFilter.accept(Option.empty())
                 is UserDirectoryAction.SearchDirectoryUsers  -> directoryUsersSearch.accept(action.value)
    -            is UserDirectoryAction.SelectUser            -> handleSelectUser(action)
    -            is UserDirectoryAction.RemoveSelectedUser    -> handleRemoveSelectedUser(action)
    -        }
    +            is UserDirectoryAction.SelectPendingInvitee  -> handleSelectUser(action)
    +            is UserDirectoryAction.RemovePendingInvitee  -> handleRemoveSelectedUser(action)
    +        }.exhaustive
         }
     
    -    private fun handleRemoveSelectedUser(action: UserDirectoryAction.RemoveSelectedUser) = withState { state ->
    -        val selectedUsers = state.selectedUsers.minus(action.user)
    +    private fun handleRemoveSelectedUser(action: UserDirectoryAction.RemovePendingInvitee) = withState { state ->
    +        val selectedUsers = state.selectedUsers.minus(action.pendingInvitee)
             setState { copy(selectedUsers = selectedUsers) }
         }
     
    -    private fun handleSelectUser(action: UserDirectoryAction.SelectUser) = withState { state ->
    +    private fun handleSelectUser(action: UserDirectoryAction.SelectPendingInvitee) = withState { state ->
             // Reset the filter asap
             directoryUsersSearch.accept("")
    -        val selectedUsers = state.selectedUsers.toggle(action.user)
    +        val selectedUsers = state.selectedUsers.toggle(action.pendingInvitee)
             setState { copy(selectedUsers = selectedUsers) }
         }
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewState.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewState.kt
    index 52f92a9994..4dee0fe264 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewState.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewState.kt
    @@ -27,11 +27,21 @@ data class UserDirectoryViewState(
             val excludedUserIds: Set? = null,
             val knownUsers: Async> = Uninitialized,
             val directoryUsers: Async> = Uninitialized,
    -        val selectedUsers: Set = emptySet(),
    +        val selectedUsers: Set = emptySet(),
             val createAndInviteState: Async = Uninitialized,
             val directorySearchTerm: String = "",
             val filterKnownUsersValue: Option = Option.empty()
     ) : MvRxState {
     
         constructor(args: KnownUsersFragmentArgs) : this(excludedUserIds = args.excludedUserIds)
    +
    +    fun getSelectedMatrixId(): List {
    +        return selectedUsers
    +                .mapNotNull {
    +                    when (it) {
    +                        is PendingInvitee.UserPendingInvitee     -> it.user.userId
    +                        is PendingInvitee.ThreePidPendingInvitee -> null
    +                    }
    +                }
    +    }
     }
    
    From 4b3a6a883d003bd20206429b08098487839ff25f Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Thu, 9 Jul 2020 14:07:00 +0200
    Subject: [PATCH 102/122] CreateRoomParams has been replaced by
     CreateRoomParamsBuilder, to be able to invite 3pids
    
    ---
     CHANGES.md                                    |   2 +-
     .../main/java/im/vector/matrix/rx/RxRoom.kt   |   5 +
     .../java/im/vector/matrix/rx/RxSession.kt     |   4 +-
     .../matrix/android/common/CryptoTestHelper.kt |  12 +-
     .../crypto/gossiping/KeyShareTests.kt         |   9 +-
     .../android/api/session/room/RoomService.kt   |   6 +-
     .../room/model/create/CreateRoomParams.kt     | 268 ------------------
     .../model/create/CreateRoomParamsBuilder.kt   |  86 ++++++
     .../session/room/model/create/Invite3Pid.kt   |  50 ----
     .../session/room/DefaultRoomService.kt        |   4 +-
     .../android/internal/session/room/RoomAPI.kt  |   6 +-
     .../session/room/create/CreateRoomParams.kt   | 115 ++++++++
     .../create/CreateRoomParamsInternalBuilder.kt | 147 ++++++++++
     .../room}/create/CreateRoomResponse.kt        |   4 +-
     .../session/room/create/CreateRoomTask.kt     |  46 +--
     .../room/membership/joining/JoinRoomTask.kt   |   2 +-
     .../createdirect/CreateDirectRoomViewModel.kt |  19 +-
     .../VerificationBottomSheetViewModel.kt       |  13 +-
     .../invite/InviteUsersToRoomAction.kt         |   4 +-
     .../invite/InviteUsersToRoomViewModel.kt      |   9 +-
     .../createroom/CreateRoomViewModel.kt         |  24 +-
     .../userdirectory/KnownUsersFragment.kt       |   5 +-
     .../features/userdirectory/PendingInvitee.kt  |  11 +-
     .../userdirectory/UserDirectoryFragment.kt    |   5 +-
     24 files changed, 443 insertions(+), 413 deletions(-)
     delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt
     create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParamsBuilder.kt
     delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/Invite3Pid.kt
     create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParams.kt
     create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParamsInternalBuilder.kt
     rename matrix-sdk-android/src/main/java/im/vector/matrix/android/{api/session/room/model => internal/session/room}/create/CreateRoomResponse.kt (89%)
    
    diff --git a/CHANGES.md b/CHANGES.md
    index 32ab57fda8..5daced2228 100644
    --- a/CHANGES.md
    +++ b/CHANGES.md
    @@ -28,7 +28,7 @@ Translations 🗣:
      -
     
     SDK API changes ⚠️:
    - - 
    + - CreateRoomParams has been replaced by CreateRoomParamsBuilder
     
     Build 🧱:
      - Upgrade some dependencies
    diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt
    index b91949778d..2e96863d60 100644
    --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt
    +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt
    @@ -19,6 +19,7 @@ package im.vector.matrix.rx
     import android.net.Uri
     import im.vector.matrix.android.api.query.QueryStringValue
     import im.vector.matrix.android.api.session.events.model.Event
    +import im.vector.matrix.android.api.session.identity.ThreePid
     import im.vector.matrix.android.api.session.room.Room
     import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
     import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
    @@ -104,6 +105,10 @@ class RxRoom(private val room: Room) {
             room.invite(userId, reason, it)
         }
     
    +    fun invite3pid(threePid: ThreePid): Completable = completableBuilder {
    +        room.invite3pid(threePid, it)
    +    }
    +
         fun updateTopic(topic: String): Completable = completableBuilder {
             room.updateTopic(topic, it)
         }
    diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt
    index ca0bb46f4b..93e2dcae19 100644
    --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt
    +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt
    @@ -32,7 +32,7 @@ import im.vector.matrix.android.api.session.pushers.Pusher
     import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
     import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
     import im.vector.matrix.android.api.session.room.model.RoomSummary
    -import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParamsBuilder
     import im.vector.matrix.android.api.session.sync.SyncState
     import im.vector.matrix.android.api.session.user.model.User
     import im.vector.matrix.android.api.session.widgets.model.Widget
    @@ -110,7 +110,7 @@ class RxSession(private val session: Session) {
                     .startWithCallable { session.getThreePids() }
         }
     
    -    fun createRoom(roomParams: CreateRoomParams): Single = singleBuilder {
    +    fun createRoom(roomParams: CreateRoomParamsBuilder): Single = singleBuilder {
             session.createRoom(roomParams, it)
         }
     
    diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt
    index 5425f97fc4..7e8410a440 100644
    --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt
    +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt
    @@ -30,7 +30,7 @@ import im.vector.matrix.android.api.session.events.model.toContent
     import im.vector.matrix.android.api.session.room.Room
     import im.vector.matrix.android.api.session.room.model.Membership
     import im.vector.matrix.android.api.session.room.model.RoomSummary
    -import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParamsBuilder
     import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
     import im.vector.matrix.android.api.session.room.timeline.Timeline
     import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
    @@ -65,7 +65,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
             val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
     
             val roomId = mTestHelper.doSync {
    -            aliceSession.createRoom(CreateRoomParams(name = "MyRoom"), it)
    +            aliceSession.createRoom(CreateRoomParamsBuilder().apply { name = "MyRoom" }, it)
             }
     
             if (encryptedRoom) {
    @@ -286,9 +286,11 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
         fun createDM(alice: Session, bob: Session): String {
             val roomId = mTestHelper.doSync {
                 alice.createRoom(
    -                    CreateRoomParams(invitedUserIds = listOf(bob.myUserId))
    -                            .setDirectMessage()
    -                            .enableEncryptionIfInvitedUsersSupportIt(),
    +                    CreateRoomParamsBuilder().apply {
    +                        invitedUserIds.add(bob.myUserId)
    +                        setDirectMessage()
    +                        enableEncryptionIfInvitedUsersSupportIt = true
    +                    },
                         it
                 )
             }
    diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt
    index e78ef04050..e90822a0c7 100644
    --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt
    +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt
    @@ -27,7 +27,7 @@ import im.vector.matrix.android.api.session.crypto.verification.VerificationTran
     import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
     import im.vector.matrix.android.api.session.events.model.toModel
     import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
    -import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParamsBuilder
     import im.vector.matrix.android.common.CommonTestHelper
     import im.vector.matrix.android.common.CryptoTestHelper
     import im.vector.matrix.android.common.SessionTestParams
    @@ -66,7 +66,10 @@ class KeyShareTests : InstrumentedTest {
             // Create an encrypted room and add a message
             val roomId = mTestHelper.doSync {
                 aliceSession.createRoom(
    -                    CreateRoomParams(RoomDirectoryVisibility.PRIVATE).enableEncryptionWithAlgorithm(true),
    +                    CreateRoomParamsBuilder().apply {
    +                        visibility = RoomDirectoryVisibility.PRIVATE
    +                        enableEncryption()
    +                    },
                         it
                 )
             }
    @@ -285,7 +288,7 @@ class KeyShareTests : InstrumentedTest {
             mTestHelper.waitWithLatch(60_000) { latch ->
                 val keysBackupService = aliceSession2.cryptoService().keysBackupService()
                 mTestHelper.retryPeriodicallyWithLatch(latch) {
    -                Log.d("#TEST", "Recovery :${ keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
    +                Log.d("#TEST", "Recovery :${keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
                     keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey
                 }
             }
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt
    index 3319cecfef..788a074c65 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt
    @@ -20,7 +20,7 @@ import androidx.lifecycle.LiveData
     import im.vector.matrix.android.api.MatrixCallback
     import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
     import im.vector.matrix.android.api.session.room.model.RoomSummary
    -import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParamsBuilder
     import im.vector.matrix.android.api.util.Cancelable
     import im.vector.matrix.android.api.util.Optional
     
    @@ -32,7 +32,7 @@ interface RoomService {
         /**
          * Create a room asynchronously
          */
    -    fun createRoom(createRoomParams: CreateRoomParams,
    +    fun createRoom(createRoomParams: CreateRoomParamsBuilder,
                        callback: MatrixCallback): Cancelable
     
         /**
    @@ -113,5 +113,5 @@ interface RoomService {
          */
         fun getChangeMembershipsLive(): LiveData>
     
    -    fun getExistingDirectRoomWithUser(otherUserId: String) : Room?
    +    fun getExistingDirectRoomWithUser(otherUserId: String): Room?
     }
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt
    deleted file mode 100644
    index 1abbe9ef3a..0000000000
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt
    +++ /dev/null
    @@ -1,268 +0,0 @@
    -/*
    - * Copyright 2019 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.api.session.room.model.create
    -
    -import android.util.Patterns
    -import androidx.annotation.CheckResult
    -import com.squareup.moshi.Json
    -import com.squareup.moshi.JsonClass
    -import im.vector.matrix.android.api.MatrixPatterns.isUserId
    -import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
    -import im.vector.matrix.android.api.session.events.model.Event
    -import im.vector.matrix.android.api.session.events.model.EventType
    -import im.vector.matrix.android.api.session.events.model.toContent
    -import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
    -import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
    -import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
    -import im.vector.matrix.android.internal.auth.data.ThreePidMedium
    -import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
    -import timber.log.Timber
    -
    -/**
    - * Parameter to create a room, with facilities functions to configure it
    - */
    -@JsonClass(generateAdapter = true)
    -data class CreateRoomParams(
    -        /**
    -         * A public visibility indicates that the room will be shown in the published room list.
    -         * A private visibility will hide the room from the published room list.
    -         * Rooms default to private visibility if this key is not included.
    -         * NB: This should not be confused with join_rules which also uses the word public. One of: ["public", "private"]
    -         */
    -        @Json(name = "visibility")
    -        val visibility: RoomDirectoryVisibility? = null,
    -
    -        /**
    -         * The desired room alias local part. If this is included, a room alias will be created and mapped to the newly created room.
    -         * The alias will belong on the same homeserver which created the room.
    -         * For example, if this was set to "foo" and sent to the homeserver "example.com" the complete room alias would be #foo:example.com.
    -         */
    -        @Json(name = "room_alias_name")
    -        val roomAliasName: String? = null,
    -
    -        /**
    -         * If this is included, an m.room.name event will be sent into the room to indicate the name of the room.
    -         * See Room Events for more information on m.room.name.
    -         */
    -        @Json(name = "name")
    -        val name: String? = null,
    -
    -        /**
    -         * If this is included, an m.room.topic event will be sent into the room to indicate the topic for the room.
    -         * See Room Events for more information on m.room.topic.
    -         */
    -        @Json(name = "topic")
    -        val topic: String? = null,
    -
    -        /**
    -         * A list of user IDs to invite to the room.
    -         * This will tell the server to invite everyone in the list to the newly created room.
    -         */
    -        @Json(name = "invite")
    -        val invitedUserIds: List? = null,
    -
    -        /**
    -         * A list of objects representing third party IDs to invite into the room.
    -         */
    -        @Json(name = "invite_3pid")
    -        val invite3pids: List? = null,
    -
    -        /**
    -         * Extra keys to be added to the content of the m.room.create.
    -         * The server will clobber the following keys: creator.
    -         * Future versions of the specification may allow the server to clobber other keys.
    -         */
    -        @Json(name = "creation_content")
    -        val creationContent: Any? = null,
    -
    -        /**
    -         * A list of state events to set in the new room.
    -         * This allows the user to override the default state events set in the new room.
    -         * The expected format of the state events are an object with type, state_key and content keys set.
    -         * Takes precedence over events set by presets, but gets overridden by name and topic keys.
    -         */
    -        @Json(name = "initial_state")
    -        val initialStates: List? = null,
    -
    -        /**
    -         * Convenience parameter for setting various default state events based on a preset. Must be either:
    -         * private_chat => join_rules is set to invite. history_visibility is set to shared.
    -         * trusted_private_chat => join_rules is set to invite. history_visibility is set to shared. All invitees are given the same power level as the
    -         * room creator.
    -         * public_chat: => join_rules is set to public. history_visibility is set to shared.
    -         */
    -        @Json(name = "preset")
    -        val preset: CreateRoomPreset? = null,
    -
    -        /**
    -         * This flag makes the server set the is_direct flag on the m.room.member events sent to the users in invite and invite_3pid.
    -         * See Direct Messaging for more information.
    -         */
    -        @Json(name = "is_direct")
    -        val isDirect: Boolean? = null,
    -
    -        /**
    -         * The power level content to override in the default power level event
    -         */
    -        @Json(name = "power_level_content_override")
    -        val powerLevelContentOverride: PowerLevelsContent? = null
    -) {
    -    @Transient
    -    internal var enableEncryptionIfInvitedUsersSupportIt: Boolean = false
    -        private set
    -
    -    /**
    -     * After calling this method, when the room will be created, if cross-signing is enabled and we can get keys for every invited users,
    -     * the encryption will be enabled on the created room
    -     * @param value true to activate this behavior.
    -     * @return this, to allow chaining methods
    -     */
    -    fun enableEncryptionIfInvitedUsersSupportIt(value: Boolean = true): CreateRoomParams {
    -        enableEncryptionIfInvitedUsersSupportIt = value
    -        return this
    -    }
    -
    -    /**
    -     * Add the crypto algorithm to the room creation parameters.
    -     *
    -     * @param enable true to enable encryption.
    -     * @param algorithm the algorithm, default to [MXCRYPTO_ALGORITHM_MEGOLM], which is actually the only supported algorithm for the moment
    -     * @return a modified copy of the CreateRoomParams object, or this if there is no modification
    -     */
    -    @CheckResult
    -    fun enableEncryptionWithAlgorithm(enable: Boolean = true,
    -                                      algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM): CreateRoomParams {
    -        // Remove the existing value if any.
    -        val newInitialStates = initialStates
    -                ?.filter { it.type != EventType.STATE_ROOM_ENCRYPTION }
    -
    -        return if (algorithm == MXCRYPTO_ALGORITHM_MEGOLM) {
    -            if (enable) {
    -                val contentMap = mapOf("algorithm" to algorithm)
    -
    -                val algoEvent = Event(
    -                        type = EventType.STATE_ROOM_ENCRYPTION,
    -                        stateKey = "",
    -                        content = contentMap.toContent()
    -                )
    -
    -                copy(
    -                        initialStates = newInitialStates.orEmpty() + algoEvent
    -                )
    -            } else {
    -                return copy(
    -                        initialStates = newInitialStates
    -                )
    -            }
    -        } else {
    -            Timber.e("Unsupported algorithm: $algorithm")
    -            this
    -        }
    -    }
    -
    -    /**
    -     * Force the history visibility in the room creation parameters.
    -     *
    -     * @param historyVisibility the expected history visibility, set null to remove any existing value.
    -     * @return a modified copy of the CreateRoomParams object
    -     */
    -    @CheckResult
    -    fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?): CreateRoomParams {
    -        // Remove the existing value if any.
    -        val newInitialStates = initialStates
    -                ?.filter { it.type != EventType.STATE_ROOM_HISTORY_VISIBILITY }
    -
    -        if (historyVisibility != null) {
    -            val contentMap = mapOf("history_visibility" to historyVisibility)
    -
    -            val historyVisibilityEvent = Event(
    -                    type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
    -                    stateKey = "",
    -                    content = contentMap.toContent())
    -
    -            return copy(
    -                    initialStates = newInitialStates.orEmpty() + historyVisibilityEvent
    -            )
    -        } else {
    -            return copy(
    -                    initialStates = newInitialStates
    -            )
    -        }
    -    }
    -
    -    /**
    -     * Mark as a direct message room.
    -     * @return a modified copy of the CreateRoomParams object
    -     */
    -    @CheckResult
    -    fun setDirectMessage(): CreateRoomParams {
    -        return copy(
    -                preset = CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT,
    -                isDirect = true
    -        )
    -    }
    -
    -    /**
    -     * Tells if the created room can be a direct chat one.
    -     *
    -     * @return true if it is a direct chat
    -     */
    -    fun isDirect(): Boolean {
    -        return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
    -                && isDirect == true
    -    }
    -
    -    /**
    -     * @return the first invited user id
    -     */
    -    fun getFirstInvitedUserId(): String? {
    -        return invitedUserIds?.firstOrNull() ?: invite3pids?.firstOrNull()?.address
    -    }
    -
    -    /**
    -     * Add some ids to the room creation
    -     * ids might be a matrix id or an email address.
    -     *
    -     * @param ids the participant ids to add.
    -     * @return a modified copy of the CreateRoomParams object
    -     */
    -    @CheckResult
    -    fun addParticipantIds(hsConfig: HomeServerConnectionConfig,
    -                          userId: String,
    -                          ids: List): CreateRoomParams {
    -        return copy(
    -                invite3pids = (invite3pids.orEmpty() + ids
    -                        .takeIf { hsConfig.identityServerUri != null }
    -                        ?.filter { id -> Patterns.EMAIL_ADDRESS.matcher(id).matches() }
    -                        ?.map { id ->
    -                            Invite3Pid(
    -                                    idServer = hsConfig.identityServerUri!!.host!!,
    -                                    medium = ThreePidMedium.EMAIL,
    -                                    address = id
    -                            )
    -                        }
    -                        .orEmpty())
    -                        .distinct(),
    -                invitedUserIds = (invitedUserIds.orEmpty() + ids
    -                        .filter { id -> isUserId(id) }
    -                        // do not invite oneself
    -                        .filter { id -> id != userId })
    -                        .distinct()
    -        )
    -        // TODO add phonenumbers when it will be available
    -    }
    -}
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParamsBuilder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParamsBuilder.kt
    new file mode 100644
    index 0000000000..6637e3bcb2
    --- /dev/null
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParamsBuilder.kt
    @@ -0,0 +1,86 @@
    +/*
    + * Copyright (c) 2020 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.api.session.room.model.create
    +
    +import im.vector.matrix.android.api.session.identity.ThreePid
    +import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
    +import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
    +import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
    +
    +class CreateRoomParamsBuilder {
    +    var visibility: RoomDirectoryVisibility? = null
    +    var roomAliasName: String? = null
    +    var name: String? = null
    +    var topic: String? = null
    +
    +    /**
    +     * UserIds to invite
    +     */
    +    val invitedUserIds = mutableListOf()
    +
    +    /**
    +     * ThreePids to invite
    +     */
    +    val invite3pids = mutableListOf()
    +
    +    /**
    +     * If set to true, when the room will be created, if cross-signing is enabled and we can get keys for every invited users,
    +     * the encryption will be enabled on the created room
    +     */
    +    var enableEncryptionIfInvitedUsersSupportIt: Boolean = false
    +
    +    var preset: CreateRoomPreset? = null
    +
    +    var isDirect: Boolean? = null
    +
    +    /**
    +     * Mark as a direct message room.
    +     */
    +    fun setDirectMessage() {
    +        preset = CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
    +        isDirect = true
    +    }
    +
    +    /**
    +     * Supported value: MXCRYPTO_ALGORITHM_MEGOLM
    +     */
    +    var algorithm: String? = null
    +        private set
    +
    +    var historyVisibility: RoomHistoryVisibility? = null
    +
    +    fun enableEncryption() {
    +        algorithm = MXCRYPTO_ALGORITHM_MEGOLM
    +    }
    +
    +    /**
    +     * Tells if the created room can be a direct chat one.
    +     *
    +     * @return true if it is a direct chat
    +     */
    +    fun isDirect(): Boolean {
    +        return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
    +                && isDirect == true
    +    }
    +
    +    /**
    +     * @return the first invited user id
    +     */
    +    fun getFirstInvitedUserId(): String? {
    +        return invitedUserIds.firstOrNull() ?: invite3pids.firstOrNull()?.value
    +    }
    +}
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/Invite3Pid.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/Invite3Pid.kt
    deleted file mode 100644
    index 66c8f1b2e8..0000000000
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/Invite3Pid.kt
    +++ /dev/null
    @@ -1,50 +0,0 @@
    -/*
    - * Copyright 2019 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.api.session.room.model.create
    -
    -import com.squareup.moshi.Json
    -import com.squareup.moshi.JsonClass
    -
    -@JsonClass(generateAdapter = true)
    -data class Invite3Pid(
    -        /**
    -         * Required.
    -         * The hostname+port of the identity server which should be used for third party identifier lookups.
    -         */
    -        @Json(name = "id_server")
    -        val idServer: String,
    -
    -        /**
    -         * Required.
    -         * An access token previously registered with the identity server. Servers can treat this as optional to
    -         * distinguish between r0.5-compatible clients and this specification version.
    -         */
    -        @Json(name = "id_access_token")
    -        val idAccessToken: String,
    -
    -        /**
    -         * Required.
    -         * The kind of address being passed in the address field, for example email.
    -         */
    -        val medium: String,
    -
    -        /**
    -         * Required.
    -         * The invitee's third party identifier.
    -         */
    -        val address: String
    -)
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt
    index b8b4c968b1..7d5b8ac341 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt
    @@ -23,7 +23,7 @@ import im.vector.matrix.android.api.session.room.RoomService
     import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
     import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
     import im.vector.matrix.android.api.session.room.model.RoomSummary
    -import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParamsBuilder
     import im.vector.matrix.android.api.util.Cancelable
     import im.vector.matrix.android.api.util.Optional
     import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
    @@ -49,7 +49,7 @@ internal class DefaultRoomService @Inject constructor(
             private val taskExecutor: TaskExecutor
     ) : RoomService {
     
    -    override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback): Cancelable {
    +    override fun createRoom(createRoomParams: CreateRoomParamsBuilder, callback: MatrixCallback): Cancelable {
             return createRoomTask
                     .configureWith(createRoomParams) {
                         this.callback = callback
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt
    index e00a94297a..a82c96f93d 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt
    @@ -18,9 +18,6 @@ package im.vector.matrix.android.internal.session.room
     
     import im.vector.matrix.android.api.session.events.model.Content
     import im.vector.matrix.android.api.session.events.model.Event
    -import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
    -import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
    -import im.vector.matrix.android.api.session.room.model.create.JoinRoomResponse
     import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams
     import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse
     import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol
    @@ -28,6 +25,9 @@ import im.vector.matrix.android.api.util.JsonDict
     import im.vector.matrix.android.internal.network.NetworkConstants
     import im.vector.matrix.android.internal.session.room.alias.AddRoomAliasBody
     import im.vector.matrix.android.internal.session.room.alias.RoomAliasDescription
    +import im.vector.matrix.android.internal.session.room.create.CreateRoomParams
    +import im.vector.matrix.android.internal.session.room.create.CreateRoomResponse
    +import im.vector.matrix.android.internal.session.room.create.JoinRoomResponse
     import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse
     import im.vector.matrix.android.internal.session.room.membership.admin.UserIdAndReason
     import im.vector.matrix.android.internal.session.room.membership.joining.InviteBody
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParams.kt
    new file mode 100644
    index 0000000000..525a0501fc
    --- /dev/null
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParams.kt
    @@ -0,0 +1,115 @@
    +/*
    + * Copyright (c) 2020 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.session.room.create
    +
    +import com.squareup.moshi.Json
    +import com.squareup.moshi.JsonClass
    +import im.vector.matrix.android.api.session.events.model.Event
    +import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
    +import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomPreset
    +import im.vector.matrix.android.internal.session.room.membership.threepid.ThreePidInviteBody
    +
    +/**
    + * Parameter to create a room
    + */
    +@JsonClass(generateAdapter = true)
    +internal data class CreateRoomParams(
    +        /**
    +         * A public visibility indicates that the room will be shown in the published room list.
    +         * A private visibility will hide the room from the published room list.
    +         * Rooms default to private visibility if this key is not included.
    +         * NB: This should not be confused with join_rules which also uses the word public. One of: ["public", "private"]
    +         */
    +        @Json(name = "visibility")
    +        val visibility: RoomDirectoryVisibility?,
    +
    +        /**
    +         * The desired room alias local part. If this is included, a room alias will be created and mapped to the newly created room.
    +         * The alias will belong on the same homeserver which created the room.
    +         * For example, if this was set to "foo" and sent to the homeserver "example.com" the complete room alias would be #foo:example.com.
    +         */
    +        @Json(name = "room_alias_name")
    +        val roomAliasName: String?,
    +
    +        /**
    +         * If this is included, an m.room.name event will be sent into the room to indicate the name of the room.
    +         * See Room Events for more information on m.room.name.
    +         */
    +        @Json(name = "name")
    +        val name: String?,
    +
    +        /**
    +         * If this is included, an m.room.topic event will be sent into the room to indicate the topic for the room.
    +         * See Room Events for more information on m.room.topic.
    +         */
    +        @Json(name = "topic")
    +        val topic: String?,
    +
    +        /**
    +         * A list of user IDs to invite to the room.
    +         * This will tell the server to invite everyone in the list to the newly created room.
    +         */
    +        @Json(name = "invite")
    +        val invitedUserIds: List?,
    +
    +        /**
    +         * A list of objects representing third party IDs to invite into the room.
    +         */
    +        @Json(name = "invite_3pid")
    +        val invite3pids: List?,
    +
    +        /**
    +         * Extra keys to be added to the content of the m.room.create.
    +         * The server will clobber the following keys: creator.
    +         * Future versions of the specification may allow the server to clobber other keys.
    +         */
    +        @Json(name = "creation_content")
    +        val creationContent: Any?,
    +
    +        /**
    +         * A list of state events to set in the new room.
    +         * This allows the user to override the default state events set in the new room.
    +         * The expected format of the state events are an object with type, state_key and content keys set.
    +         * Takes precedence over events set by presets, but gets overridden by name and topic keys.
    +         */
    +        @Json(name = "initial_state")
    +        val initialStates: List?,
    +
    +        /**
    +         * Convenience parameter for setting various default state events based on a preset. Must be either:
    +         * private_chat => join_rules is set to invite. history_visibility is set to shared.
    +         * trusted_private_chat => join_rules is set to invite. history_visibility is set to shared. All invitees are given the same power level as the
    +         * room creator.
    +         * public_chat: => join_rules is set to public. history_visibility is set to shared.
    +         */
    +        @Json(name = "preset")
    +        val preset: CreateRoomPreset?,
    +
    +        /**
    +         * This flag makes the server set the is_direct flag on the m.room.member events sent to the users in invite and invite_3pid.
    +         * See Direct Messaging for more information.
    +         */
    +        @Json(name = "is_direct")
    +        val isDirect: Boolean?,
    +
    +        /**
    +         * The power level content to override in the default power level event
    +         */
    +        @Json(name = "power_level_content_override")
    +        val powerLevelContentOverride: PowerLevelsContent?
    +)
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParamsInternalBuilder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParamsInternalBuilder.kt
    new file mode 100644
    index 0000000000..29cf0bbac6
    --- /dev/null
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParamsInternalBuilder.kt
    @@ -0,0 +1,147 @@
    +/*
    + * Copyright (c) 2020 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.session.room.create
    +
    +import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
    +import im.vector.matrix.android.api.session.events.model.Event
    +import im.vector.matrix.android.api.session.events.model.EventType
    +import im.vector.matrix.android.api.session.events.model.toContent
    +import im.vector.matrix.android.api.session.identity.IdentityServiceError
    +import im.vector.matrix.android.api.session.identity.toMedium
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParamsBuilder
    +import im.vector.matrix.android.internal.crypto.DeviceListManager
    +import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
    +import im.vector.matrix.android.internal.di.AuthenticatedIdentity
    +import im.vector.matrix.android.internal.network.token.AccessTokenProvider
    +import im.vector.matrix.android.internal.session.identity.EnsureIdentityTokenTask
    +import im.vector.matrix.android.internal.session.identity.data.IdentityStore
    +import im.vector.matrix.android.internal.session.identity.data.getIdentityServerUrlWithoutProtocol
    +import im.vector.matrix.android.internal.session.room.membership.threepid.ThreePidInviteBody
    +import java.security.InvalidParameterException
    +import javax.inject.Inject
    +
    +internal class CreateRoomParamsInternalBuilder @Inject constructor(
    +        private val ensureIdentityTokenTask: EnsureIdentityTokenTask,
    +        private val crossSigningService: CrossSigningService,
    +        private val deviceListManager: DeviceListManager,
    +        private val identityStore: IdentityStore,
    +        @AuthenticatedIdentity
    +        private val accessTokenProvider: AccessTokenProvider
    +) {
    +
    +    suspend fun build(builder: CreateRoomParamsBuilder): CreateRoomParams {
    +        val invite3pids = builder.invite3pids
    +                .takeIf { it.isNotEmpty() }
    +                .let {
    +                    // This can throw Exception if Identity server is not configured
    +                    ensureIdentityTokenTask.execute(Unit)
    +
    +                    val identityServerUrlWithoutProtocol = identityStore.getIdentityServerUrlWithoutProtocol()
    +                            ?: throw IdentityServiceError.NoIdentityServerConfigured
    +                    val identityServerAccessToken = accessTokenProvider.getToken() ?: throw IdentityServiceError.NoIdentityServerConfigured
    +
    +                    builder.invite3pids.map {
    +                        ThreePidInviteBody(
    +                                id_server = identityServerUrlWithoutProtocol,
    +                                id_access_token = identityServerAccessToken,
    +                                medium = it.toMedium(),
    +                                address = it.value
    +                        )
    +                    }
    +                }
    +
    +        val initialStates = listOfNotNull(
    +                buildEncryptionWithAlgorithmEvent(builder),
    +                buildHistoryVisibilityEvent(builder)
    +        )
    +                .takeIf { it.isNotEmpty() }
    +
    +        return CreateRoomParams(
    +                visibility = builder.visibility,
    +                roomAliasName = builder.roomAliasName,
    +                name = builder.name,
    +                topic = builder.topic,
    +                invitedUserIds = builder.invitedUserIds,
    +                invite3pids = invite3pids,
    +                // TODO Support this
    +                creationContent = null,
    +                initialStates = initialStates,
    +                preset = builder.preset,
    +                isDirect = builder.isDirect,
    +                // TODO Support this
    +                powerLevelContentOverride = null
    +        )
    +    }
    +
    +    private fun buildHistoryVisibilityEvent(builder: CreateRoomParamsBuilder): Event? {
    +        return builder.historyVisibility
    +                ?.let {
    +                    val contentMap = mapOf("history_visibility" to it)
    +
    +                    Event(
    +                            type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
    +                            stateKey = "",
    +                            content = contentMap.toContent())
    +                }
    +    }
    +
    +    /**
    +     * Add the crypto algorithm to the room creation parameters.
    +     */
    +    private suspend fun buildEncryptionWithAlgorithmEvent(builder: CreateRoomParamsBuilder): Event? {
    +        if (builder.algorithm == null
    +                && canEnableEncryption(builder)) {
    +            // Enable the encryption
    +            builder.enableEncryption()
    +        }
    +        return builder.algorithm
    +                ?.let {
    +                    if (it != MXCRYPTO_ALGORITHM_MEGOLM) {
    +                        throw InvalidParameterException("Unsupported algorithm: $it")
    +                    }
    +                    val contentMap = mapOf("algorithm" to it)
    +
    +                    Event(
    +                            type = EventType.STATE_ROOM_ENCRYPTION,
    +                            stateKey = "",
    +                            content = contentMap.toContent()
    +                    )
    +                }
    +    }
    +
    +    private suspend fun canEnableEncryption(builder: CreateRoomParamsBuilder): Boolean {
    +        return (builder.enableEncryptionIfInvitedUsersSupportIt
    +                && crossSigningService.isCrossSigningVerified()
    +                && builder.invite3pids.isEmpty())
    +                && builder.invitedUserIds.isNotEmpty()
    +                && builder.invitedUserIds.let { userIds ->
    +            val keys = deviceListManager.downloadKeys(userIds, forceDownload = false)
    +
    +            userIds.all { userId ->
    +                keys.map[userId].let { deviceMap ->
    +                    if (deviceMap.isNullOrEmpty()) {
    +                        // A user has no device, so do not enable encryption
    +                        false
    +                    } else {
    +                        // Check that every user's device have at least one key
    +                        deviceMap.values.all { !it.keys.isNullOrEmpty() }
    +                    }
    +                }
    +            }
    +        }
    +    }
    +}
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomResponse.kt
    similarity index 89%
    rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomResponse.kt
    rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomResponse.kt
    index da54b344a2..62208941cc 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomResponse.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomResponse.kt
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2019 New Vector Ltd
    + * Copyright (c) 2020 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.
    @@ -14,7 +14,7 @@
      * limitations under the License.
      */
     
    -package im.vector.matrix.android.api.session.room.model.create
    +package im.vector.matrix.android.internal.session.room.create
     
     import com.squareup.moshi.Json
     import com.squareup.moshi.JsonClass
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt
    index 2071b7736e..e32f8e39ab 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt
    @@ -17,11 +17,8 @@
     package im.vector.matrix.android.internal.session.room.create
     
     import com.zhuinden.monarchy.Monarchy
    -import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
     import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure
    -import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
    -import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
    -import im.vector.matrix.android.internal.crypto.DeviceListManager
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParamsBuilder
     import im.vector.matrix.android.internal.database.awaitNotEmptyResult
     import im.vector.matrix.android.internal.database.model.RoomEntity
     import im.vector.matrix.android.internal.database.model.RoomEntityFields
    @@ -41,7 +38,7 @@ import org.greenrobot.eventbus.EventBus
     import java.util.concurrent.TimeUnit
     import javax.inject.Inject
     
    -internal interface CreateRoomTask : Task
    +internal interface CreateRoomTask : Task
     
     internal class DefaultCreateRoomTask @Inject constructor(
             private val roomAPI: RoomAPI,
    @@ -51,17 +48,12 @@ internal class DefaultCreateRoomTask @Inject constructor(
             private val readMarkersTask: SetReadMarkersTask,
             @SessionDatabase
             private val realmConfiguration: RealmConfiguration,
    -        private val crossSigningService: CrossSigningService,
    -        private val deviceListManager: DeviceListManager,
    +        private val createRoomParamsInternalBuilder: CreateRoomParamsInternalBuilder,
             private val eventBus: EventBus
     ) : CreateRoomTask {
     
    -    override suspend fun execute(params: CreateRoomParams): String {
    -        val createRoomParams = if (canEnableEncryption(params)) {
    -            params.enableEncryptionWithAlgorithm()
    -        } else {
    -            params
    -        }
    +    override suspend fun execute(params: CreateRoomParamsBuilder): String {
    +        val createRoomParams = createRoomParamsInternalBuilder.build(params)
     
             val createRoomResponse = executeRequest(eventBus) {
                 apiCall = roomAPI.createRoom(createRoomParams)
    @@ -76,36 +68,14 @@ internal class DefaultCreateRoomTask @Inject constructor(
             } catch (exception: TimeoutCancellationException) {
                 throw CreateRoomFailure.CreatedWithTimeout
             }
    -        if (createRoomParams.isDirect()) {
    -            handleDirectChatCreation(createRoomParams, roomId)
    +        if (params.isDirect()) {
    +            handleDirectChatCreation(params, roomId)
             }
             setReadMarkers(roomId)
             return roomId
         }
     
    -    private suspend fun canEnableEncryption(params: CreateRoomParams): Boolean {
    -        return params.enableEncryptionIfInvitedUsersSupportIt
    -                && crossSigningService.isCrossSigningVerified()
    -                && params.invite3pids.isNullOrEmpty()
    -                && params.invitedUserIds?.isNotEmpty() == true
    -                && params.invitedUserIds.let { userIds ->
    -            val keys = deviceListManager.downloadKeys(userIds, forceDownload = false)
    -
    -            userIds.all { userId ->
    -                keys.map[userId].let { deviceMap ->
    -                    if (deviceMap.isNullOrEmpty()) {
    -                        // A user has no device, so do not enable encryption
    -                        false
    -                    } else {
    -                        // Check that every user's device have at least one key
    -                        deviceMap.values.all { !it.keys.isNullOrEmpty() }
    -                    }
    -                }
    -            }
    -        }
    -    }
    -
    -    private suspend fun handleDirectChatCreation(params: CreateRoomParams, roomId: String) {
    +    private suspend fun handleDirectChatCreation(params: CreateRoomParamsBuilder, roomId: String) {
             val otherUserId = params.getFirstInvitedUserId()
                     ?: throw IllegalStateException("You can't create a direct room without an invitedUser")
     
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt
    index 7467a595bc..8fb9a1f065 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt
    @@ -18,13 +18,13 @@ package im.vector.matrix.android.internal.session.room.membership.joining
     
     import im.vector.matrix.android.api.session.room.failure.JoinRoomFailure
     import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
    -import im.vector.matrix.android.api.session.room.model.create.JoinRoomResponse
     import im.vector.matrix.android.internal.database.awaitNotEmptyResult
     import im.vector.matrix.android.internal.database.model.RoomEntity
     import im.vector.matrix.android.internal.database.model.RoomEntityFields
     import im.vector.matrix.android.internal.di.SessionDatabase
     import im.vector.matrix.android.internal.network.executeRequest
     import im.vector.matrix.android.internal.session.room.RoomAPI
    +import im.vector.matrix.android.internal.session.room.create.JoinRoomResponse
     import im.vector.matrix.android.internal.session.room.membership.RoomChangeMembershipStateDataSource
     import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
     import im.vector.matrix.android.internal.task.Task
    diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt
    index 90340c5cd8..44acdc0032 100644
    --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt
    @@ -22,8 +22,9 @@ import com.airbnb.mvrx.ViewModelContext
     import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.session.Session
    -import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParamsBuilder
     import im.vector.matrix.rx.rx
    +import im.vector.riotx.core.extensions.exhaustive
     import im.vector.riotx.core.platform.VectorViewModel
     import im.vector.riotx.features.userdirectory.PendingInvitee
     
    @@ -53,11 +54,17 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
         }
     
         private fun createRoomAndInviteSelectedUsers(selectedUsers: Set) {
    -        val roomParams = CreateRoomParams(
    -                invitedUserIds = selectedUsers.map { it.userId }
    -        )
    -                .setDirectMessage()
    -                .enableEncryptionIfInvitedUsersSupportIt()
    +        val roomParams = CreateRoomParamsBuilder()
    +                .apply {
    +                    selectedUsers.forEach {
    +                        when (it) {
    +                            is PendingInvitee.UserPendingInvitee     -> invitedUserIds.add(it.user.userId)
    +                            is PendingInvitee.ThreePidPendingInvitee -> invite3pids.add(it.threePid)
    +                        }.exhaustive
    +                    }
    +                    setDirectMessage()
    +                    enableEncryptionIfInvitedUsersSupportIt = true
    +                }
     
             session.rx()
                     .createRoom(roomParams)
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
    index 9b454436d9..1833688c35 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
    @@ -43,7 +43,7 @@ import im.vector.matrix.android.api.session.crypto.verification.VerificationServ
     import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
     import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
     import im.vector.matrix.android.api.session.events.model.LocalEcho
    -import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParamsBuilder
     import im.vector.matrix.android.api.util.MatrixItem
     import im.vector.matrix.android.api.util.toMatrixItem
     import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
    @@ -235,11 +235,12 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
                                     pendingRequest = Loading()
                             )
                         }
    -                    val roomParams = CreateRoomParams(
    -                            invitedUserIds = listOf(otherUserId)
    -                    )
    -                            .setDirectMessage()
    -                            .enableEncryptionIfInvitedUsersSupportIt()
    +                    val roomParams = CreateRoomParamsBuilder()
    +                            .apply {
    +                                invitedUserIds.add(otherUserId)
    +                                setDirectMessage()
    +                                enableEncryptionIfInvitedUsersSupportIt = true
    +                            }
     
                         session.createRoom(roomParams, object : MatrixCallback {
                             override fun onSuccess(data: String) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomAction.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomAction.kt
    index 8a62935bdd..253b557cca 100644
    --- a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomAction.kt
    @@ -16,9 +16,9 @@
     
     package im.vector.riotx.features.invite
     
    -import im.vector.matrix.android.api.session.user.model.User
     import im.vector.riotx.core.platform.VectorViewModelAction
    +import im.vector.riotx.features.userdirectory.PendingInvitee
     
     sealed class InviteUsersToRoomAction : VectorViewModelAction {
    -    data class InviteSelectedUsers(val selectedUsers: Set) : InviteUsersToRoomAction()
    +    data class InviteSelectedUsers(val selectedUsers: Set) : InviteUsersToRoomAction()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewModel.kt
    index fc2f34b7a0..78a9961884 100644
    --- a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewModel.kt
    @@ -22,11 +22,11 @@ import com.airbnb.mvrx.ViewModelContext
     import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.session.Session
    -import im.vector.matrix.android.api.session.user.model.User
     import im.vector.matrix.rx.rx
     import im.vector.riotx.R
     import im.vector.riotx.core.platform.VectorViewModel
     import im.vector.riotx.core.resources.StringProvider
    +import im.vector.riotx.features.userdirectory.PendingInvitee
     import io.reactivex.Observable
     
     class InviteUsersToRoomViewModel @AssistedInject constructor(@Assisted
    @@ -57,11 +57,14 @@ class InviteUsersToRoomViewModel @AssistedInject constructor(@Assisted
             }
         }
     
    -    private fun inviteUsersToRoom(selectedUsers: Set) {
    +    private fun inviteUsersToRoom(selectedUsers: Set) {
             _viewEvents.post(InviteUsersToRoomViewEvents.Loading)
     
             Observable.fromIterable(selectedUsers).flatMapCompletable { user ->
    -            room.rx().invite(user.userId, null)
    +            when (user) {
    +                is PendingInvitee.UserPendingInvitee     -> room.rx().invite(user.user.userId, null)
    +                is PendingInvitee.ThreePidPendingInvitee -> room.rx().invite3pid(user.threePid)
    +            }
             }.subscribe(
                     {
                         val successMessage = when (selectedUsers.size) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewModel.kt
    index cfe50bb2f7..5cb279c848 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewModel.kt
    @@ -28,7 +28,7 @@ import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.MatrixCallback
     import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
    -import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParamsBuilder
     import im.vector.matrix.android.api.session.room.model.create.CreateRoomPreset
     import im.vector.riotx.core.platform.EmptyViewEvents
     import im.vector.riotx.core.platform.VectorViewModel
    @@ -84,15 +84,19 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
                 copy(asyncCreateRoomRequest = Loading())
             }
     
    -        val createRoomParams = CreateRoomParams(
    -                name = state.roomName.takeIf { it.isNotBlank() },
    -                // Directory visibility
    -                visibility = if (state.isInRoomDirectory) RoomDirectoryVisibility.PUBLIC else RoomDirectoryVisibility.PRIVATE,
    -                // Public room
    -                preset = if (state.isPublic) CreateRoomPreset.PRESET_PUBLIC_CHAT else CreateRoomPreset.PRESET_PRIVATE_CHAT
    -        )
    -                // Encryption
    -                .enableEncryptionWithAlgorithm(state.isEncrypted)
    +        val createRoomParams = CreateRoomParamsBuilder()
    +                .apply {
    +                    name = state.roomName.takeIf { it.isNotBlank() }
    +                    // Directory visibility
    +                    visibility = if (state.isInRoomDirectory) RoomDirectoryVisibility.PUBLIC else RoomDirectoryVisibility.PRIVATE
    +                    // Public room
    +                    preset = if (state.isPublic) CreateRoomPreset.PRESET_PUBLIC_CHAT else CreateRoomPreset.PRESET_PRIVATE_CHAT
    +
    +                    // Encryption
    +                    if (state.isEncrypted) {
    +                        enableEncryption()
    +                    }
    +                }
     
             session.createRoom(createRoomParams, object : MatrixCallback {
                 override fun onSuccess(data: String) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt
    index dc7ec5ee04..5367ec270c 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt
    @@ -160,10 +160,7 @@ class KnownUsersFragment @Inject constructor(
             val chip = Chip(requireContext())
             chip.setChipBackgroundColorResource(android.R.color.transparent)
             chip.chipStrokeWidth = dimensionConverter.dpToPx(1).toFloat()
    -        chip.text = when (pendingInvitee) {
    -            is PendingInvitee.UserPendingInvitee     -> pendingInvitee.user.getBestName()
    -            is PendingInvitee.ThreePidPendingInvitee -> pendingInvitee.threePid.value
    -        }
    +        chip.text = pendingInvitee.getBestName()
             chip.isClickable = true
             chip.isCheckable = false
             chip.isCloseIconVisible = true
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/PendingInvitee.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/PendingInvitee.kt
    index b213061e4a..c9aad1cf65 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/PendingInvitee.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/PendingInvitee.kt
    @@ -20,6 +20,13 @@ import im.vector.matrix.android.api.session.identity.ThreePid
     import im.vector.matrix.android.api.session.user.model.User
     
     sealed class PendingInvitee {
    -    data class UserPendingInvitee(val user: User): PendingInvitee()
    -    data class ThreePidPendingInvitee(val threePid: ThreePid): PendingInvitee()
    +    data class UserPendingInvitee(val user: User) : PendingInvitee()
    +    data class ThreePidPendingInvitee(val threePid: ThreePid) : PendingInvitee()
    +
    +    fun getBestName(): String {
    +        return when (this) {
    +            is UserPendingInvitee     -> user.getBestName()
    +            is ThreePidPendingInvitee -> threePid.value
    +        }
    +    }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryFragment.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryFragment.kt
    index 8dd4025350..a6d22dfbe3 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryFragment.kt
    @@ -21,6 +21,7 @@ import android.view.View
     import com.airbnb.mvrx.activityViewModel
     import com.airbnb.mvrx.withState
     import com.jakewharton.rxbinding3.widget.textChanges
    +import im.vector.matrix.android.api.session.user.model.User
     import im.vector.riotx.R
     import im.vector.riotx.core.extensions.cleanup
     import im.vector.riotx.core.extensions.configureWith
    @@ -81,9 +82,9 @@ class UserDirectoryFragment @Inject constructor(
             directRoomController.setData(it)
         }
     
    -    override fun onItemClick(pendingInvitee: PendingInvitee) {
    +    override fun onItemClick(user: User) {
             view?.hideKeyboard()
    -        viewModel.handle(UserDirectoryAction.SelectPendingInvitee(pendingInvitee))
    +        viewModel.handle(UserDirectoryAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(user)))
             sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
         }
     
    
    From 25e7bbcd796263c5284ecfc778be2016896cac5a Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Thu, 9 Jul 2020 15:09:48 +0200
    Subject: [PATCH 103/122] Handle contacts permission
    
    ---
     .../riotx/core/utils/PermissionsTools.kt      |  1 +
     .../createdirect/CreateDirectRoomActivity.kt  | 27 +++++++++++++++--
     .../invite/InviteUsersToRoomActivity.kt       | 29 ++++++++++++++++---
     3 files changed, 51 insertions(+), 6 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt
    index 360a5efccc..6f081d52de 100644
    --- a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt
    @@ -68,6 +68,7 @@ const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575
     const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576
     const val PERMISSION_REQUEST_CODE_INCOMING_URI = 577
     const val PERMISSION_REQUEST_CODE_PREVIEW_FRAGMENT = 578
    +const val PERMISSION_REQUEST_CODE_READ_CONTACTS = 579
     
     /**
      * Log the used permissions statuses.
    diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt
    index 256d9902f6..6acd0e099b 100644
    --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt
    @@ -38,9 +38,14 @@ import im.vector.riotx.core.extensions.addFragmentToBackstack
     import im.vector.riotx.core.extensions.exhaustive
     import im.vector.riotx.core.platform.SimpleFragmentActivity
     import im.vector.riotx.core.platform.WaitingViewData
    +import im.vector.riotx.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH
    +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS
    +import im.vector.riotx.core.utils.allGranted
    +import im.vector.riotx.core.utils.checkPermissions
    +import im.vector.riotx.features.phonebook.PhoneBookFragment
    +import im.vector.riotx.features.phonebook.PhoneBookViewModel
     import im.vector.riotx.features.userdirectory.KnownUsersFragment
     import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs
    -import im.vector.riotx.features.phonebook.PhoneBookViewModel
     import im.vector.riotx.features.userdirectory.UserDirectoryFragment
     import im.vector.riotx.features.userdirectory.UserDirectorySharedAction
     import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
    @@ -76,7 +81,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
                             UserDirectorySharedAction.Close                 -> finish()
                             UserDirectorySharedAction.GoBack                -> onBackPressed()
                             is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
    -                        UserDirectorySharedAction.OpenPhoneBook         -> TODO()
    +                        UserDirectorySharedAction.OpenPhoneBook         -> openPhoneBook()
                         }.exhaustive
                     }
                     .disposeOnDestroy()
    @@ -95,6 +100,24 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
             }
         }
     
    +    private fun openPhoneBook() {
    +        // Check permission first
    +        if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH,
    +                        this,
    +                        PERMISSION_REQUEST_CODE_READ_CONTACTS,
    +                        0)) {
    +            addFragmentToBackstack(R.id.container, PhoneBookFragment::class.java)
    +        }
    +    }
    +
    +    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
    +        if (allGranted(grantResults)) {
    +            if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
    +                addFragmentToBackstack(R.id.container, PhoneBookFragment::class.java)
    +            }
    +        }
    +    }
    +
         private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) {
             if (action.itemId == R.id.action_create_direct_room) {
                 viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(action.selectedUsers))
    diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt
    index 4d869aacee..29d230ba34 100644
    --- a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt
    @@ -33,11 +33,15 @@ import im.vector.riotx.core.extensions.addFragmentToBackstack
     import im.vector.riotx.core.extensions.exhaustive
     import im.vector.riotx.core.platform.SimpleFragmentActivity
     import im.vector.riotx.core.platform.WaitingViewData
    +import im.vector.riotx.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH
    +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS
    +import im.vector.riotx.core.utils.allGranted
    +import im.vector.riotx.core.utils.checkPermissions
     import im.vector.riotx.core.utils.toast
    -import im.vector.riotx.features.userdirectory.KnownUsersFragment
    -import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs
     import im.vector.riotx.features.phonebook.PhoneBookFragment
     import im.vector.riotx.features.phonebook.PhoneBookViewModel
    +import im.vector.riotx.features.userdirectory.KnownUsersFragment
    +import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs
     import im.vector.riotx.features.userdirectory.UserDirectoryFragment
     import im.vector.riotx.features.userdirectory.UserDirectorySharedAction
     import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
    @@ -78,8 +82,7 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
                             UserDirectorySharedAction.Close                 -> finish()
                             UserDirectorySharedAction.GoBack                -> onBackPressed()
                             is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
    -                        UserDirectorySharedAction.OpenPhoneBook         ->
    -                            addFragmentToBackstack(R.id.container, PhoneBookFragment::class.java)
    +                        UserDirectorySharedAction.OpenPhoneBook         -> openPhoneBook()
                         }.exhaustive
                     }
                     .disposeOnDestroy()
    @@ -98,6 +101,24 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
             viewModel.observeViewEvents { renderInviteEvents(it) }
         }
     
    +    private fun openPhoneBook() {
    +        // Check permission first
    +        if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH,
    +                        this,
    +                        PERMISSION_REQUEST_CODE_READ_CONTACTS,
    +                        0)) {
    +            addFragmentToBackstack(R.id.container, PhoneBookFragment::class.java)
    +        }
    +    }
    +
    +    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
    +        if (allGranted(grantResults)) {
    +            if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
    +                addFragmentToBackstack(R.id.container, PhoneBookFragment::class.java)
    +            }
    +        }
    +    }
    +
         private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) {
             if (action.itemId == R.id.action_invite_users_to_room_invite) {
                 viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.selectedUsers))
    
    From 4c1d50d55433e30592692ac2476606e6f5085065 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Thu, 9 Jul 2020 15:34:30 +0200
    Subject: [PATCH 104/122] Renames package and some other things
    
    ---
     .../im/vector/riotx/core/di/FragmentModule.kt |  6 ++--
     .../ContactDetailItem.kt                      |  2 +-
     .../ContactItem.kt                            |  2 +-
     .../ContactsBookAction.kt}                    |  8 ++---
     .../ContactsBookController.kt}                | 15 ++++----
     .../ContactsBookFragment.kt}                  | 30 ++++++++--------
     .../ContactsBookViewModel.kt}                 | 36 +++++++++----------
     .../ContactsBookViewState.kt}                 |  4 +--
     .../createdirect/CreateDirectRoomAction.kt    |  2 +-
     .../createdirect/CreateDirectRoomActivity.kt  | 12 +++----
     .../createdirect/CreateDirectRoomViewModel.kt |  6 ++--
     .../invite/InviteUsersToRoomAction.kt         |  2 +-
     .../invite/InviteUsersToRoomActivity.kt       | 12 +++----
     .../invite/InviteUsersToRoomViewModel.kt      | 20 +++++------
     .../userdirectory/KnownUsersFragment.kt       | 12 +++----
     .../UserDirectorySharedAction.kt              |  2 +-
     .../userdirectory/UserDirectoryViewModel.kt   |  8 ++---
     .../userdirectory/UserDirectoryViewState.kt   |  4 +--
     ...onebook.xml => fragment_contacts_book.xml} |  4 +--
     .../main/res/layout/fragment_known_users.xml  |  2 +-
     vector/src/main/res/values/strings.xml        |  4 +++
     21 files changed, 99 insertions(+), 94 deletions(-)
     rename vector/src/main/java/im/vector/riotx/features/{phonebook => contactsbook}/ContactDetailItem.kt (97%)
     rename vector/src/main/java/im/vector/riotx/features/{phonebook => contactsbook}/ContactItem.kt (97%)
     rename vector/src/main/java/im/vector/riotx/features/{phonebook/PhoneBookAction.kt => contactsbook/ContactsBookAction.kt} (78%)
     rename vector/src/main/java/im/vector/riotx/features/{phonebook/PhoneBookController.kt => contactsbook/ContactsBookController.kt} (91%)
     rename vector/src/main/java/im/vector/riotx/features/{phonebook/PhoneBookFragment.kt => contactsbook/ContactsBookFragment.kt} (78%)
     rename vector/src/main/java/im/vector/riotx/features/{phonebook/PhoneBookViewModel.kt => contactsbook/ContactsBookViewModel.kt} (81%)
     rename vector/src/main/java/im/vector/riotx/features/{phonebook/PhoneBookViewState.kt => contactsbook/ContactsBookViewState.kt} (94%)
     rename vector/src/main/res/layout/{fragment_phonebook.xml => fragment_contacts_book.xml} (98%)
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
    index f5f236364f..8e4f95ed54 100644
    --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
    @@ -23,6 +23,7 @@ import dagger.Binds
     import dagger.Module
     import dagger.multibindings.IntoMap
     import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment
    +import im.vector.riotx.features.contactsbook.ContactsBookFragment
     import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
     import im.vector.riotx.features.crypto.quads.SharedSecuredStorageKeyFragment
     import im.vector.riotx.features.crypto.quads.SharedSecuredStoragePassphraseFragment
    @@ -103,7 +104,6 @@ import im.vector.riotx.features.share.IncomingShareFragment
     import im.vector.riotx.features.signout.soft.SoftLogoutFragment
     import im.vector.riotx.features.terms.ReviewTermsFragment
     import im.vector.riotx.features.userdirectory.KnownUsersFragment
    -import im.vector.riotx.features.phonebook.PhoneBookFragment
     import im.vector.riotx.features.userdirectory.UserDirectoryFragment
     import im.vector.riotx.features.widgets.WidgetFragment
     
    @@ -532,6 +532,6 @@ interface FragmentModule {
     
         @Binds
         @IntoMap
    -    @FragmentKey(PhoneBookFragment::class)
    -    fun bindPhoneBookFragment(fragment: PhoneBookFragment): Fragment
    +    @FragmentKey(ContactsBookFragment::class)
    +    fun bindPhoneBookFragment(fragment: ContactsBookFragment): Fragment
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/phonebook/ContactDetailItem.kt b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactDetailItem.kt
    similarity index 97%
    rename from vector/src/main/java/im/vector/riotx/features/phonebook/ContactDetailItem.kt
    rename to vector/src/main/java/im/vector/riotx/features/contactsbook/ContactDetailItem.kt
    index 3c739b7829..8615838571 100644
    --- a/vector/src/main/java/im/vector/riotx/features/phonebook/ContactDetailItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactDetailItem.kt
    @@ -14,7 +14,7 @@
      * limitations under the License.
      */
     
    -package im.vector.riotx.features.phonebook
    +package im.vector.riotx.features.contactsbook
     
     import android.widget.TextView
     import com.airbnb.epoxy.EpoxyAttribute
    diff --git a/vector/src/main/java/im/vector/riotx/features/phonebook/ContactItem.kt b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactItem.kt
    similarity index 97%
    rename from vector/src/main/java/im/vector/riotx/features/phonebook/ContactItem.kt
    rename to vector/src/main/java/im/vector/riotx/features/contactsbook/ContactItem.kt
    index b962b0a40d..9a6bf8f144 100644
    --- a/vector/src/main/java/im/vector/riotx/features/phonebook/ContactItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactItem.kt
    @@ -14,7 +14,7 @@
      * limitations under the License.
      */
     
    -package im.vector.riotx.features.phonebook
    +package im.vector.riotx.features.contactsbook
     
     import android.widget.ImageView
     import android.widget.TextView
    diff --git a/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookAction.kt b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookAction.kt
    similarity index 78%
    rename from vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookAction.kt
    rename to vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookAction.kt
    index 3a5eb4d7ff..001630d398 100644
    --- a/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookAction.kt
    @@ -14,11 +14,11 @@
      * limitations under the License.
      */
     
    -package im.vector.riotx.features.phonebook
    +package im.vector.riotx.features.contactsbook
     
     import im.vector.riotx.core.platform.VectorViewModelAction
     
    -sealed class PhoneBookAction : VectorViewModelAction {
    -    data class FilterWith(val filter: String) : PhoneBookAction()
    -    data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : PhoneBookAction()
    +sealed class ContactsBookAction : VectorViewModelAction {
    +    data class FilterWith(val filter: String) : ContactsBookAction()
    +    data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : ContactsBookAction()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookController.kt b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookController.kt
    similarity index 91%
    rename from vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookController.kt
    rename to vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookController.kt
    index 0fe940b21e..796ed0d80c 100644
    --- a/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookController.kt
    @@ -14,7 +14,7 @@
      * limitations under the License.
      */
     
    -package im.vector.riotx.features.phonebook
    +package im.vector.riotx.features.contactsbook
     
     import com.airbnb.epoxy.EpoxyController
     import com.airbnb.mvrx.Fail
    @@ -32,12 +32,12 @@ import im.vector.riotx.core.resources.StringProvider
     import im.vector.riotx.features.home.AvatarRenderer
     import javax.inject.Inject
     
    -class PhoneBookController @Inject constructor(
    +class ContactsBookController @Inject constructor(
             private val stringProvider: StringProvider,
             private val avatarRenderer: AvatarRenderer,
             private val errorFormatter: ErrorFormatter) : EpoxyController() {
     
    -    private var state: PhoneBookViewState? = null
    +    private var state: ContactsBookViewState? = null
     
         var callback: Callback? = null
     
    @@ -45,7 +45,7 @@ class PhoneBookController @Inject constructor(
             requestModelBuild()
         }
     
    -    fun setData(state: PhoneBookViewState) {
    +    fun setData(state: ContactsBookViewState) {
             this.state = state
             requestModelBuild()
         }
    @@ -64,6 +64,7 @@ class PhoneBookController @Inject constructor(
         private fun renderLoading() {
             loadingItem {
                 id("loading")
    +            loadingText(stringProvider.getString(R.string.loading_contact_book))
             }
         }
     
    @@ -96,7 +97,7 @@ class PhoneBookController @Inject constructor(
                             if (onlyBoundContacts && it.matrixId == null) return@forEachIndexed
     
                             contactDetailItem {
    -                            id("${mappedContact.id}-$index-${it.email}")
    +                            id("${mappedContact.id}-e-$index-${it.email}")
                                 threePid(it.email)
                                 matrixId(it.matrixId)
                                 clickListener {
    @@ -113,7 +114,7 @@ class PhoneBookController @Inject constructor(
                             if (onlyBoundContacts && it.matrixId == null) return@forEachIndexed
     
                             contactDetailItem {
    -                            id("${mappedContact.id}-$index-${it.phoneNumber}")
    +                            id("${mappedContact.id}-m-$index-${it.phoneNumber}")
                                 threePid(it.phoneNumber)
                                 matrixId(it.matrixId)
                                 clickListener {
    @@ -132,7 +133,7 @@ class PhoneBookController @Inject constructor(
             val noResultRes = if (hasSearch) {
                 R.string.no_result_placeholder
             } else {
    -            R.string.empty_phone_book
    +            R.string.empty_contact_book
             }
             noResultItem {
                 id("noResult")
    diff --git a/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookFragment.kt b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookFragment.kt
    similarity index 78%
    rename from vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookFragment.kt
    rename to vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookFragment.kt
    index 1da9c6c306..2a2fd9fb5d 100644
    --- a/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookFragment.kt
    @@ -14,7 +14,7 @@
      * limitations under the License.
      */
     
    -package im.vector.riotx.features.phonebook
    +package im.vector.riotx.features.contactsbook
     
     import android.os.Bundle
     import android.view.View
    @@ -35,20 +35,20 @@ import im.vector.riotx.features.userdirectory.UserDirectoryAction
     import im.vector.riotx.features.userdirectory.UserDirectorySharedAction
     import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
     import im.vector.riotx.features.userdirectory.UserDirectoryViewModel
    -import kotlinx.android.synthetic.main.fragment_phonebook.*
    +import kotlinx.android.synthetic.main.fragment_contacts_book.*
     import java.util.concurrent.TimeUnit
     import javax.inject.Inject
     
    -class PhoneBookFragment @Inject constructor(
    -        val phoneBookViewModelFactory: PhoneBookViewModel.Factory,
    -        private val phoneBookController: PhoneBookController
    -) : VectorBaseFragment(), PhoneBookController.Callback {
    +class ContactsBookFragment @Inject constructor(
    +        val contactsBookViewModelFactory: ContactsBookViewModel.Factory,
    +        private val contactsBookController: ContactsBookController
    +) : VectorBaseFragment(), ContactsBookController.Callback {
     
    -    override fun getLayoutResId() = R.layout.fragment_phonebook
    +    override fun getLayoutResId() = R.layout.fragment_contacts_book
         private val viewModel: UserDirectoryViewModel by activityViewModel()
     
         // Use activityViewModel to avoid loading several times the data
    -    private val phoneBookViewModel: PhoneBookViewModel by activityViewModel()
    +    private val contactsBookViewModel: ContactsBookViewModel by activityViewModel()
     
         private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
     
    @@ -64,7 +64,7 @@ class PhoneBookFragment @Inject constructor(
         private fun setupOnlyBoundContactsView() {
             phoneBookOnlyBoundContacts.checkedChanges()
                     .subscribe {
    -                    phoneBookViewModel.handle(PhoneBookAction.OnlyBoundContacts(it))
    +                    contactsBookViewModel.handle(ContactsBookAction.OnlyBoundContacts(it))
                     }
                     .disposeOnDestroyView()
         }
    @@ -75,20 +75,20 @@ class PhoneBookFragment @Inject constructor(
                     .skipInitialValue()
                     .debounce(300, TimeUnit.MILLISECONDS)
                     .subscribe {
    -                    phoneBookViewModel.handle(PhoneBookAction.FilterWith(it.toString()))
    +                    contactsBookViewModel.handle(ContactsBookAction.FilterWith(it.toString()))
                     }
                     .disposeOnDestroyView()
         }
     
         override fun onDestroyView() {
             phoneBookRecyclerView.cleanup()
    -        phoneBookController.callback = null
    +        contactsBookController.callback = null
             super.onDestroyView()
         }
     
         private fun setupRecyclerView() {
    -        phoneBookController.callback = this
    -        phoneBookRecyclerView.configureWith(phoneBookController)
    +        contactsBookController.callback = this
    +        phoneBookRecyclerView.configureWith(contactsBookController)
         }
     
         private fun setupCloseView() {
    @@ -97,9 +97,9 @@ class PhoneBookFragment @Inject constructor(
             }
         }
     
    -    override fun invalidate() = withState(phoneBookViewModel) { state ->
    +    override fun invalidate() = withState(contactsBookViewModel) { state ->
             phoneBookOnlyBoundContacts.isVisible = state.isBoundRetrieved
    -        phoneBookController.setData(state)
    +        contactsBookController.setData(state)
         }
     
         override fun onMatrixIdClick(matrixId: String) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookViewModel.kt b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookViewModel.kt
    similarity index 81%
    rename from vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookViewModel.kt
    rename to vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookViewModel.kt
    index a609b63b67..c09eac2948 100644
    --- a/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookViewModel.kt
    @@ -14,7 +14,7 @@
      * limitations under the License.
      */
     
    -package im.vector.riotx.features.phonebook
    +package im.vector.riotx.features.contactsbook
     
     import androidx.fragment.app.FragmentActivity
     import androidx.lifecycle.viewModelScope
    @@ -43,26 +43,26 @@ import timber.log.Timber
     
     private typealias PhoneBookSearch = String
     
    -class PhoneBookViewModel @AssistedInject constructor(@Assisted
    -                                                     initialState: PhoneBookViewState,
    -                                                     private val contactsDataSource: ContactsDataSource,
    -                                                     private val session: Session)
    -    : VectorViewModel(initialState) {
    +class ContactsBookViewModel @AssistedInject constructor(@Assisted
    +                                                     initialState: ContactsBookViewState,
    +                                                        private val contactsDataSource: ContactsDataSource,
    +                                                        private val session: Session)
    +    : VectorViewModel(initialState) {
     
         @AssistedInject.Factory
         interface Factory {
    -        fun create(initialState: PhoneBookViewState): PhoneBookViewModel
    +        fun create(initialState: ContactsBookViewState): ContactsBookViewModel
         }
     
    -    companion object : MvRxViewModelFactory {
    +    companion object : MvRxViewModelFactory {
     
    -        override fun create(viewModelContext: ViewModelContext, state: PhoneBookViewState): PhoneBookViewModel? {
    +        override fun create(viewModelContext: ViewModelContext, state: ContactsBookViewState): ContactsBookViewModel? {
                 return when (viewModelContext) {
    -                is FragmentViewModelContext -> (viewModelContext.fragment() as PhoneBookFragment).phoneBookViewModelFactory.create(state)
    +                is FragmentViewModelContext -> (viewModelContext.fragment() as ContactsBookFragment).contactsBookViewModelFactory.create(state)
                     is ActivityViewModelContext -> {
                         when (viewModelContext.activity()) {
    -                        is CreateDirectRoomActivity  -> viewModelContext.activity().phoneBookViewModelFactory.create(state)
    -                        is InviteUsersToRoomActivity -> viewModelContext.activity().phoneBookViewModelFactory.create(state)
    +                        is CreateDirectRoomActivity  -> viewModelContext.activity().contactsBookViewModelFactory.create(state)
    +                        is InviteUsersToRoomActivity -> viewModelContext.activity().contactsBookViewModelFactory.create(state)
                             else                         -> error("Wrong activity or fragment")
                         }
                     }
    @@ -77,7 +77,7 @@ class PhoneBookViewModel @AssistedInject constructor(@Assisted
         init {
             loadContacts()
     
    -        selectSubscribe(PhoneBookViewState::searchTerm, PhoneBookViewState::onlyBoundContacts) { _, _ ->
    +        selectSubscribe(ContactsBookViewState::searchTerm, ContactsBookViewState::onlyBoundContacts) { _, _ ->
                 updateFilteredMappedContacts()
             }
         }
    @@ -163,14 +163,14 @@ class PhoneBookViewModel @AssistedInject constructor(@Assisted
             }
         }
     
    -    override fun handle(action: PhoneBookAction) {
    +    override fun handle(action: ContactsBookAction) {
             when (action) {
    -            is PhoneBookAction.FilterWith        -> handleFilterWith(action)
    -            is PhoneBookAction.OnlyBoundContacts -> handleOnlyBoundContacts(action)
    +            is ContactsBookAction.FilterWith        -> handleFilterWith(action)
    +            is ContactsBookAction.OnlyBoundContacts -> handleOnlyBoundContacts(action)
             }.exhaustive
         }
     
    -    private fun handleOnlyBoundContacts(action: PhoneBookAction.OnlyBoundContacts) {
    +    private fun handleOnlyBoundContacts(action: ContactsBookAction.OnlyBoundContacts) {
             setState {
                 copy(
                         onlyBoundContacts = action.onlyBoundContacts
    @@ -178,7 +178,7 @@ class PhoneBookViewModel @AssistedInject constructor(@Assisted
             }
         }
     
    -    private fun handleFilterWith(action: PhoneBookAction.FilterWith) {
    +    private fun handleFilterWith(action: ContactsBookAction.FilterWith) {
             setState {
                 copy(
                         searchTerm = action.filter
    diff --git a/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookViewState.kt b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookViewState.kt
    similarity index 94%
    rename from vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookViewState.kt
    rename to vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookViewState.kt
    index d4a578cd8a..8f59403d6a 100644
    --- a/vector/src/main/java/im/vector/riotx/features/phonebook/PhoneBookViewState.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookViewState.kt
    @@ -14,14 +14,14 @@
      * limitations under the License.
      */
     
    -package im.vector.riotx.features.phonebook
    +package im.vector.riotx.features.contactsbook
     
     import com.airbnb.mvrx.Async
     import com.airbnb.mvrx.Loading
     import com.airbnb.mvrx.MvRxState
     import im.vector.riotx.core.contacts.MappedContact
     
    -data class PhoneBookViewState(
    +data class ContactsBookViewState(
             // All the contacts on the phone
             val mappedContacts: Async> = Loading(),
             // Use to filter contacts by display name
    diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt
    index 2af01b8964..fad36cc281 100644
    --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt
    @@ -20,5 +20,5 @@ import im.vector.riotx.core.platform.VectorViewModelAction
     import im.vector.riotx.features.userdirectory.PendingInvitee
     
     sealed class CreateDirectRoomAction : VectorViewModelAction {
    -    data class CreateRoomAndInviteSelectedUsers(val selectedUsers: Set) : CreateDirectRoomAction()
    +    data class CreateRoomAndInviteSelectedUsers(val invitees: Set) : CreateDirectRoomAction()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt
    index 6acd0e099b..72244d1c94 100644
    --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt
    @@ -42,8 +42,8 @@ import im.vector.riotx.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH
     import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS
     import im.vector.riotx.core.utils.allGranted
     import im.vector.riotx.core.utils.checkPermissions
    -import im.vector.riotx.features.phonebook.PhoneBookFragment
    -import im.vector.riotx.features.phonebook.PhoneBookViewModel
    +import im.vector.riotx.features.contactsbook.ContactsBookFragment
    +import im.vector.riotx.features.contactsbook.ContactsBookViewModel
     import im.vector.riotx.features.userdirectory.KnownUsersFragment
     import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs
     import im.vector.riotx.features.userdirectory.UserDirectoryFragment
    @@ -60,7 +60,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
         private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
         @Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
         @Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory
    -    @Inject lateinit var phoneBookViewModelFactory: PhoneBookViewModel.Factory
    +    @Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory
         @Inject lateinit var errorFormatter: ErrorFormatter
     
         override fun injectWith(injector: ScreenComponent) {
    @@ -106,21 +106,21 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
                             this,
                             PERMISSION_REQUEST_CODE_READ_CONTACTS,
                             0)) {
    -            addFragmentToBackstack(R.id.container, PhoneBookFragment::class.java)
    +            addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java)
             }
         }
     
         override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
             if (allGranted(grantResults)) {
                 if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
    -                addFragmentToBackstack(R.id.container, PhoneBookFragment::class.java)
    +                addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java)
                 }
             }
         }
     
         private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) {
             if (action.itemId == R.id.action_create_direct_room) {
    -            viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(action.selectedUsers))
    +            viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(action.invitees))
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt
    index 44acdc0032..da81c13747 100644
    --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt
    @@ -49,14 +49,14 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
     
         override fun handle(action: CreateDirectRoomAction) {
             when (action) {
    -            is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers(action.selectedUsers)
    +            is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers(action.invitees)
             }
         }
     
    -    private fun createRoomAndInviteSelectedUsers(selectedUsers: Set) {
    +    private fun createRoomAndInviteSelectedUsers(invitees: Set) {
             val roomParams = CreateRoomParamsBuilder()
                     .apply {
    -                    selectedUsers.forEach {
    +                    invitees.forEach {
                             when (it) {
                                 is PendingInvitee.UserPendingInvitee     -> invitedUserIds.add(it.user.userId)
                                 is PendingInvitee.ThreePidPendingInvitee -> invite3pids.add(it.threePid)
    diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomAction.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomAction.kt
    index 253b557cca..6c059c917f 100644
    --- a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomAction.kt
    @@ -20,5 +20,5 @@ import im.vector.riotx.core.platform.VectorViewModelAction
     import im.vector.riotx.features.userdirectory.PendingInvitee
     
     sealed class InviteUsersToRoomAction : VectorViewModelAction {
    -    data class InviteSelectedUsers(val selectedUsers: Set) : InviteUsersToRoomAction()
    +    data class InviteSelectedUsers(val invitees: Set) : InviteUsersToRoomAction()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt
    index 29d230ba34..af78457d96 100644
    --- a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt
    @@ -38,8 +38,8 @@ import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS
     import im.vector.riotx.core.utils.allGranted
     import im.vector.riotx.core.utils.checkPermissions
     import im.vector.riotx.core.utils.toast
    -import im.vector.riotx.features.phonebook.PhoneBookFragment
    -import im.vector.riotx.features.phonebook.PhoneBookViewModel
    +import im.vector.riotx.features.contactsbook.ContactsBookFragment
    +import im.vector.riotx.features.contactsbook.ContactsBookViewModel
     import im.vector.riotx.features.userdirectory.KnownUsersFragment
     import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs
     import im.vector.riotx.features.userdirectory.UserDirectoryFragment
    @@ -60,7 +60,7 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
         private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
         @Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory
         @Inject lateinit var inviteUsersToRoomViewModelFactory: InviteUsersToRoomViewModel.Factory
    -    @Inject lateinit var phoneBookViewModelFactory: PhoneBookViewModel.Factory
    +    @Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory
         @Inject lateinit var errorFormatter: ErrorFormatter
     
         override fun injectWith(injector: ScreenComponent) {
    @@ -107,21 +107,21 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity() {
                             this,
                             PERMISSION_REQUEST_CODE_READ_CONTACTS,
                             0)) {
    -            addFragmentToBackstack(R.id.container, PhoneBookFragment::class.java)
    +            addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java)
             }
         }
     
         override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
             if (allGranted(grantResults)) {
                 if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
    -                addFragmentToBackstack(R.id.container, PhoneBookFragment::class.java)
    +                addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java)
                 }
             }
         }
     
         private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) {
             if (action.itemId == R.id.action_invite_users_to_room_invite) {
    -            viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.selectedUsers))
    +            viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.invitees))
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewModel.kt
    index 78a9961884..2769dc56bb 100644
    --- a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewModel.kt
    @@ -53,30 +53,30 @@ class InviteUsersToRoomViewModel @AssistedInject constructor(@Assisted
     
         override fun handle(action: InviteUsersToRoomAction) {
             when (action) {
    -            is InviteUsersToRoomAction.InviteSelectedUsers -> inviteUsersToRoom(action.selectedUsers)
    +            is InviteUsersToRoomAction.InviteSelectedUsers -> inviteUsersToRoom(action.invitees)
             }
         }
     
    -    private fun inviteUsersToRoom(selectedUsers: Set) {
    +    private fun inviteUsersToRoom(invitees: Set) {
             _viewEvents.post(InviteUsersToRoomViewEvents.Loading)
     
    -        Observable.fromIterable(selectedUsers).flatMapCompletable { user ->
    +        Observable.fromIterable(invitees).flatMapCompletable { user ->
                 when (user) {
                     is PendingInvitee.UserPendingInvitee     -> room.rx().invite(user.user.userId, null)
                     is PendingInvitee.ThreePidPendingInvitee -> room.rx().invite3pid(user.threePid)
                 }
             }.subscribe(
                     {
    -                    val successMessage = when (selectedUsers.size) {
    +                    val successMessage = when (invitees.size) {
                             1    -> stringProvider.getString(R.string.invitation_sent_to_one_user,
    -                                selectedUsers.first().getBestName())
    +                                invitees.first().getBestName())
                             2    -> stringProvider.getString(R.string.invitations_sent_to_two_users,
    -                                selectedUsers.first().getBestName(),
    -                                selectedUsers.last().getBestName())
    +                                invitees.first().getBestName(),
    +                                invitees.last().getBestName())
                             else -> stringProvider.getQuantityString(R.plurals.invitations_sent_to_one_and_more_users,
    -                                selectedUsers.size - 1,
    -                                selectedUsers.first().getBestName(),
    -                                selectedUsers.size - 1)
    +                                invitees.size - 1,
    +                                invitees.first().getBestName(),
    +                                invitees.size - 1)
                         }
                         _viewEvents.post(InviteUsersToRoomViewEvents.Success(successMessage))
                     },
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt
    index 5367ec270c..671c0b0ee1 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt
    @@ -65,7 +65,7 @@ class KnownUsersFragment @Inject constructor(
             setupAddByMatrixIdView()
             setupAddFromPhoneBookView()
             setupCloseView()
    -        viewModel.selectSubscribe(this, UserDirectoryViewState::selectedUsers) {
    +        viewModel.selectSubscribe(this, UserDirectoryViewState::pendingInvitees) {
                 renderSelectedUsers(it)
             }
         }
    @@ -78,7 +78,7 @@ class KnownUsersFragment @Inject constructor(
     
         override fun onPrepareOptionsMenu(menu: Menu) {
             withState(viewModel) {
    -            val showMenuItem = it.selectedUsers.isNotEmpty()
    +            val showMenuItem = it.pendingInvitees.isNotEmpty()
                 menu.forEach { menuItem ->
                     menuItem.isVisible = showMenuItem
                 }
    @@ -87,7 +87,7 @@ class KnownUsersFragment @Inject constructor(
         }
     
         override fun onOptionsItemSelected(item: MenuItem): Boolean = withState(viewModel) {
    -        sharedActionViewModel.post(UserDirectorySharedAction.OnMenuItemSelected(item.itemId, it.selectedUsers))
    +        sharedActionViewModel.post(UserDirectorySharedAction.OnMenuItemSelected(item.itemId, it.pendingInvitees))
             return@withState true
         }
     
    @@ -139,14 +139,14 @@ class KnownUsersFragment @Inject constructor(
             knownUsersController.setData(it)
         }
     
    -    private fun renderSelectedUsers(selectedUsers: Set) {
    +    private fun renderSelectedUsers(invitees: Set) {
             invalidateOptionsMenu()
     
             val currentNumberOfChips = chipGroup.childCount
    -        val newNumberOfChips = selectedUsers.size
    +        val newNumberOfChips = invitees.size
     
             chipGroup.removeAllViews()
    -        selectedUsers.forEach { addChipToGroup(it) }
    +        invitees.forEach { addChipToGroup(it) }
     
             // Scroll to the bottom when adding chips. When removing chips, do not scroll
             if (newNumberOfChips >= currentNumberOfChips) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedAction.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedAction.kt
    index 7506b97be3..14270f31a7 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedAction.kt
    @@ -23,5 +23,5 @@ sealed class UserDirectorySharedAction : VectorSharedAction {
         object OpenPhoneBook : UserDirectorySharedAction()
         object Close : UserDirectorySharedAction()
         object GoBack : UserDirectorySharedAction()
    -    data class OnMenuItemSelected(val itemId: Int, val selectedUsers: Set) : UserDirectorySharedAction()
    +    data class OnMenuItemSelected(val itemId: Int, val invitees: Set) : UserDirectorySharedAction()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewModel.kt
    index d7fb800aa4..57ebe408c7 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewModel.kt
    @@ -86,15 +86,15 @@ class UserDirectoryViewModel @AssistedInject constructor(@Assisted
         }
     
         private fun handleRemoveSelectedUser(action: UserDirectoryAction.RemovePendingInvitee) = withState { state ->
    -        val selectedUsers = state.selectedUsers.minus(action.pendingInvitee)
    -        setState { copy(selectedUsers = selectedUsers) }
    +        val selectedUsers = state.pendingInvitees.minus(action.pendingInvitee)
    +        setState { copy(pendingInvitees = selectedUsers) }
         }
     
         private fun handleSelectUser(action: UserDirectoryAction.SelectPendingInvitee) = withState { state ->
             // Reset the filter asap
             directoryUsersSearch.accept("")
    -        val selectedUsers = state.selectedUsers.toggle(action.pendingInvitee)
    -        setState { copy(selectedUsers = selectedUsers) }
    +        val selectedUsers = state.pendingInvitees.toggle(action.pendingInvitee)
    +        setState { copy(pendingInvitees = selectedUsers) }
         }
     
         private fun observeDirectoryUsers() = withState { state ->
    diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewState.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewState.kt
    index 4dee0fe264..4d99a75fde 100644
    --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewState.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewState.kt
    @@ -27,7 +27,7 @@ data class UserDirectoryViewState(
             val excludedUserIds: Set? = null,
             val knownUsers: Async> = Uninitialized,
             val directoryUsers: Async> = Uninitialized,
    -        val selectedUsers: Set = emptySet(),
    +        val pendingInvitees: Set = emptySet(),
             val createAndInviteState: Async = Uninitialized,
             val directorySearchTerm: String = "",
             val filterKnownUsersValue: Option = Option.empty()
    @@ -36,7 +36,7 @@ data class UserDirectoryViewState(
         constructor(args: KnownUsersFragmentArgs) : this(excludedUserIds = args.excludedUserIds)
     
         fun getSelectedMatrixId(): List {
    -        return selectedUsers
    +        return pendingInvitees
                     .mapNotNull {
                         when (it) {
                             is PendingInvitee.UserPendingInvitee     -> it.user.userId
    diff --git a/vector/src/main/res/layout/fragment_phonebook.xml b/vector/src/main/res/layout/fragment_contacts_book.xml
    similarity index 98%
    rename from vector/src/main/res/layout/fragment_phonebook.xml
    rename to vector/src/main/res/layout/fragment_contacts_book.xml
    index 14c44c11f0..13a3142cec 100644
    --- a/vector/src/main/res/layout/fragment_phonebook.xml
    +++ b/vector/src/main/res/layout/fragment_contacts_book.xml
    @@ -45,7 +45,7 @@
                         android:layout_marginEnd="8dp"
                         android:ellipsize="end"
                         android:maxLines="1"
    -                    android:text="@string/phone_book_title"
    +                    android:text="@string/contacts_book_title"
                         android:textColor="?riotx_text_primary"
                         android:textSize="18sp"
                         android:textStyle="bold"
    @@ -97,7 +97,7 @@
                 android:id="@+id/phoneBookFilterDivider"
                 android:layout_width="0dp"
                 android:layout_height="1dp"
    -            android:layout_marginTop="16dp"
    +            android:layout_marginTop="4dp"
                 android:background="?attr/vctr_list_divider_color"
                 app:layout_constraintEnd_toEndOf="parent"
                 app:layout_constraintStart_toStartOf="parent"
    diff --git a/vector/src/main/res/layout/fragment_known_users.xml b/vector/src/main/res/layout/fragment_known_users.xml
    index 16e6858e60..82ddea5323 100644
    --- a/vector/src/main/res/layout/fragment_known_users.xml
    +++ b/vector/src/main/res/layout/fragment_known_users.xml
    @@ -132,7 +132,7 @@
                 android:layout_marginTop="8dp"
                 android:layout_marginBottom="8dp"
                 android:minHeight="@dimen/layout_touch_size"
    -            android:text="@string/add_from_phone_book"
    +            android:text="@string/search_in_my_contacts"
                 android:visibility="visible"
                 app:icon="@drawable/ic_plus_circle"
                 app:iconPadding="13dp"
    diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
    index df596ae2d8..36ec74e028 100644
    --- a/vector/src/main/res/values/strings.xml
    +++ b/vector/src/main/res/values/strings.xml
    @@ -2545,4 +2545,8 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
         Add from my phone book
         Your phone book is empty
         Phone book
    +    Search in my contacts
    +    Retrieving your contacts…
    +    Your contact book is empty
    +    Contacts book
     
    
    From 3142442e5c689d30531649750208df4e5eaff43b Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Thu, 9 Jul 2020 15:49:10 +0200
    Subject: [PATCH 105/122] Load contacts much faster
    
    ---
     .../riotx/core/contacts/ContactsDataSource.kt | 116 ++++++++++--------
     1 file changed, 64 insertions(+), 52 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt b/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt
    index f694a9e3f0..4307d106f9 100644
    --- a/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt
    @@ -31,7 +31,7 @@ class ContactsDataSource @Inject constructor(
     
         @WorkerThread
         fun getContacts(): List {
    -        val result = mutableListOf()
    +        val map = mutableMapOf()
             val contentResolver = context.contentResolver
     
             measureTimeMillis {
    @@ -53,70 +53,82 @@ class ContactsDataSource @Inject constructor(
                                     val id = cursor.getLong(ContactsContract.Contacts._ID) ?: continue
                                     val displayName = cursor.getString(ContactsContract.Contacts.DISPLAY_NAME) ?: continue
     
    -                                val currentContact = MappedContactBuilder(
    +                                val mappedContactBuilder = MappedContactBuilder(
                                             id = id,
                                             displayName = displayName
                                     )
     
                                     cursor.getString(ContactsContract.Data.PHOTO_URI)
                                             ?.let { Uri.parse(it) }
    -                                        ?.let { currentContact.photoURI = it }
    +                                        ?.let { mappedContactBuilder.photoURI = it }
     
    -                                // Get the phone numbers
    -                                contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
    -                                        arrayOf(
    -                                                ContactsContract.CommonDataKinds.Phone.NUMBER
    -                                        ),
    -                                        ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
    -                                        arrayOf(id.toString()),
    -                                        null)
    -                                        ?.use { innerCursor ->
    -                                            while (innerCursor.moveToNext()) {
    -                                                innerCursor.getString(ContactsContract.CommonDataKinds.Phone.NUMBER)
    -                                                        ?.let {
    -                                                            currentContact.msisdns.add(
    -                                                                    MappedMsisdn(
    -                                                                            phoneNumber = it,
    -                                                                            matrixId = null
    -                                                                    )
    -                                                            )
    -                                                        }
    -                                            }
    -                                        }
     
    -                                // Get Emails
    -                                contentResolver.query(
    -                                        ContactsContract.CommonDataKinds.Email.CONTENT_URI,
    -                                        arrayOf(
    -                                                ContactsContract.CommonDataKinds.Email.DATA
    -                                        ),
    -                                        ContactsContract.CommonDataKinds.Email.CONTACT_ID + " = ?",
    -                                        arrayOf(id.toString()),
    -                                        null)
    -                                        ?.use { innerCursor ->
    -                                            while (innerCursor.moveToNext()) {
    -                                                // This would allow you get several email addresses
    -                                                // if the email addresses were stored in an array
    -                                                innerCursor.getString(ContactsContract.CommonDataKinds.Email.DATA)
    -                                                        ?.let {
    -                                                            currentContact.emails.add(
    -                                                                    MappedEmail(
    -                                                                            email = it,
    -                                                                            matrixId = null
    -                                                                    )
    -                                                            )
    -                                                        }
    -                                            }
    -                                        }
    -
    -                                result.add(currentContact.build())
    +                                map[id] = mappedContactBuilder
                                 }
                             }
                         }
    -        }.also { Timber.d("Took ${it}ms to fetch ${result.size} contact(s)") }
     
    -        return result
    +            // Get the phone numbers
    +            contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
    +                    arrayOf(
    +                            ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
    +                            ContactsContract.CommonDataKinds.Phone.NUMBER
    +                    ),
    +                    null,
    +                    null,
    +                    null)
    +                    ?.use { innerCursor ->
    +                        while (innerCursor.moveToNext()) {
    +                            val mappedContactBuilder = innerCursor.getLong(ContactsContract.CommonDataKinds.Phone.CONTACT_ID)
    +                                    ?.let { map[it] }
    +                                    ?: continue
    +                            innerCursor.getString(ContactsContract.CommonDataKinds.Phone.NUMBER)
    +                                    ?.let {
    +                                        mappedContactBuilder.msisdns.add(
    +                                                MappedMsisdn(
    +                                                        phoneNumber = it,
    +                                                        matrixId = null
    +                                                )
    +                                        )
    +                                    }
    +                        }
    +                    }
    +
    +            // Get Emails
    +            contentResolver.query(
    +                    ContactsContract.CommonDataKinds.Email.CONTENT_URI,
    +                    arrayOf(
    +                            ContactsContract.CommonDataKinds.Email.CONTACT_ID,
    +                            ContactsContract.CommonDataKinds.Email.DATA
    +                    ),
    +                    null,
    +                    null,
    +                    null)
    +                    ?.use { innerCursor ->
    +                        while (innerCursor.moveToNext()) {
    +                            // This would allow you get several email addresses
    +                            // if the email addresses were stored in an array
    +                            val mappedContactBuilder = innerCursor.getLong(ContactsContract.CommonDataKinds.Email.CONTACT_ID)
    +                                    ?.let { map[it] }
    +                                    ?: continue
    +                            innerCursor.getString(ContactsContract.CommonDataKinds.Email.DATA)
    +                                    ?.let {
    +                                        mappedContactBuilder.emails.add(
    +                                                MappedEmail(
    +                                                        email = it,
    +                                                        matrixId = null
    +                                                )
    +                                        )
    +                                    }
    +                        }
    +                    }
    +
    +        }.also { Timber.d("Took ${it}ms to fetch ${map.size} contact(s)") }
    +
    +        return map
    +                .values
                     .filter { it.emails.isNotEmpty() || it.msisdns.isNotEmpty() }
    +                .map { it.build() }
         }
     
         private fun Cursor.getString(column: String): String? {
    
    From 863c09142f213edde3ff942975c3df8b8734ffe3 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Thu, 9 Jul 2020 17:28:12 +0200
    Subject: [PATCH 106/122] Display three pid invites in the room members list
     (#548)
    
    ---
     CHANGES.md                                    |  1 +
     .../main/java/im/vector/matrix/rx/RxRoom.kt   |  7 ++++
     .../core/epoxy/profiles/ProfileMatrixItem.kt  |  5 ++-
     .../members/RoomMemberListController.kt       | 34 +++++++++++++++++++
     .../members/RoomMemberListFragment.kt         |  5 +++
     .../members/RoomMemberListViewModel.kt        |  8 +++++
     .../members/RoomMemberListViewState.kt        |  2 ++
     vector/src/main/res/values/strings.xml        |  1 +
     8 files changed, 62 insertions(+), 1 deletion(-)
    
    diff --git a/CHANGES.md b/CHANGES.md
    index 5daced2228..3978038b8d 100644
    --- a/CHANGES.md
    +++ b/CHANGES.md
    @@ -13,6 +13,7 @@ Improvements 🙌:
      - Set up SSSS from security settings (#1567)
      - New lab setting to add 'unread notifications' tab to main screen
      - Render third party invite event (#548)
    + - Display three pid invites in the room members list (#548)
     
     Bugfix 🐛:
      - Integration Manager: Wrong URL to review terms if URL in config contains path (#1606)
    diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt
    index 2e96863d60..e945a52650 100644
    --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt
    +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt
    @@ -72,6 +72,13 @@ class RxRoom(private val room: Room) {
                     }
         }
     
    +    fun liveStateEvents(eventTypes: Set): Observable> {
    +        return room.getStateEventsLive(eventTypes).asObservable()
    +                .startWithCallable {
    +                    room.getStateEvents(eventTypes)
    +                }
    +    }
    +
         fun liveReadMarker(): Observable> {
             return room.getReadMarkerLive().asObservable()
         }
    diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt
    index e9f4dba7a5..d6629c708e 100644
    --- a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt
    @@ -42,7 +42,10 @@ abstract class ProfileMatrixItem : VectorEpoxyModel()
         override fun bind(holder: Holder) {
             super.bind(holder)
             val bestName = matrixItem.getBestName()
    -        val matrixId = matrixItem.id.takeIf { it != bestName }
    +        val matrixId = matrixItem.id
    +                .takeIf { it != bestName }
    +                // Special case for ThreePid fake matrix item
    +                .takeIf { it != "@" }
             holder.view.setOnClickListener(clickListener)
             holder.titleView.text = bestName
             holder.subtitleView.setTextOrHide(matrixId)
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListController.kt
    index d0939e939e..6dcf5a0bd3 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListController.kt
    @@ -17,7 +17,11 @@
     package im.vector.riotx.features.roomprofile.members
     
     import com.airbnb.epoxy.TypedEpoxyController
    +import im.vector.matrix.android.api.session.events.model.Event
    +import im.vector.matrix.android.api.session.events.model.toModel
     import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
    +import im.vector.matrix.android.api.session.room.model.RoomThirdPartyInviteContent
    +import im.vector.matrix.android.api.util.MatrixItem
     import im.vector.matrix.android.api.util.toMatrixItem
     import im.vector.riotx.R
     import im.vector.riotx.core.epoxy.dividerItem
    @@ -37,6 +41,7 @@ class RoomMemberListController @Inject constructor(
     
         interface Callback {
             fun onRoomMemberClicked(roomMember: RoomMemberSummary)
    +        fun onThreePidInvites(event: Event)
         }
     
         private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color)
    @@ -76,5 +81,34 @@ class RoomMemberListController @Inject constructor(
                         }
                 )
             }
    +        buildThreePidInvites(data)
    +    }
    +
    +    private fun buildThreePidInvites(data: RoomMemberListViewState) {
    +        if (data.threePidInvites().isNullOrEmpty()) {
    +            return
    +        }
    +
    +        buildProfileSection(
    +                stringProvider.getString(R.string.room_member_power_level_three_pid_invites)
    +        )
    +
    +        data.threePidInvites()?.forEachIndexed { idx, event ->
    +            val content = event.content.toModel() ?: return@forEachIndexed
    +
    +            profileMatrixItem {
    +                id("3pid_$idx")
    +                matrixItem(content.toMatrixItem())
    +                avatarRenderer(avatarRenderer)
    +                clickListener { _ ->
    +                    callback?.onThreePidInvites(event)
    +                }
    +            }
    +
    +        }
    +    }
    +
    +    private fun RoomThirdPartyInviteContent.toMatrixItem(): MatrixItem {
    +        return MatrixItem.UserItem("@", displayName = displayName)
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt
    index 6bd2b5d0e3..72ec3e8462 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt
    @@ -23,6 +23,7 @@ import android.view.View
     import com.airbnb.mvrx.args
     import com.airbnb.mvrx.fragmentViewModel
     import com.airbnb.mvrx.withState
    +import im.vector.matrix.android.api.session.events.model.Event
     import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
     import im.vector.matrix.android.api.util.toMatrixItem
     import im.vector.riotx.R
    @@ -88,6 +89,10 @@ class RoomMemberListFragment @Inject constructor(
             navigator.openRoomMemberProfile(roomMember.userId, roomId = roomProfileArgs.roomId, context = requireActivity())
         }
     
    +    override fun onThreePidInvites(event: Event) {
    +        // TODO Display a bottom sheet to revoke invite if power level is high enough
    +    }
    +
         private fun renderRoomSummary(state: RoomMemberListViewState) {
             state.roomSummary()?.let {
                 roomSettingsToolbarTitleView.text = it.displayName
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt
    index f177d26725..e412e41b73 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt
    @@ -68,6 +68,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
     
         init {
             observeRoomMemberSummaries()
    +        observeThirdPartyInvites()
             observeRoomSummary()
             observePowerLevel()
         }
    @@ -140,6 +141,13 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
                     }
         }
     
    +    private fun observeThirdPartyInvites() {
    +        room.rx().liveStateEvents(setOf(EventType.STATE_ROOM_THIRD_PARTY_INVITE))
    +                .execute { async ->
    +                    copy(threePidInvites = async)
    +                }
    +    }
    +
         private fun buildRoomMemberSummaries(powerLevelsContent: PowerLevelsContent, roomMembers: List): RoomMemberSummaries {
             val admins = ArrayList()
             val moderators = ArrayList()
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewState.kt
    index ece49a178c..6a8738b450 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewState.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewState.kt
    @@ -21,6 +21,7 @@ import com.airbnb.mvrx.Async
     import com.airbnb.mvrx.MvRxState
     import com.airbnb.mvrx.Uninitialized
     import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
    +import im.vector.matrix.android.api.session.events.model.Event
     import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
     import im.vector.matrix.android.api.session.room.model.RoomSummary
     import im.vector.riotx.R
    @@ -30,6 +31,7 @@ data class RoomMemberListViewState(
             val roomId: String,
             val roomSummary: Async = Uninitialized,
             val roomMemberSummaries: Async = Uninitialized,
    +        val threePidInvites: Async> = Uninitialized,
             val trustLevelMap: Async> = Uninitialized,
             val actionsPermissions: ActionPermissions = ActionPermissions()
     ) : MvRxState {
    diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
    index 36ec74e028..62e27b51c2 100644
    --- a/vector/src/main/res/values/strings.xml
    +++ b/vector/src/main/res/values/strings.xml
    @@ -2148,6 +2148,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
         Custom
         Invites
         Users
    +    Other invites
     
         Admin in %1$s
         Moderator in %1$s
    
    From c78bba803c0771ace846a59b72b5142198f229a0 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Thu, 9 Jul 2020 17:47:41 +0200
    Subject: [PATCH 107/122] Revoke ThreePid invitation (#548)
    
    ---
     .../members/RoomMemberListAction.kt           |  4 ++-
     .../members/RoomMemberListFragment.kt         | 17 ++++++++++++-
     .../members/RoomMemberListViewModel.kt        | 25 ++++++++++++++++++-
     .../members/RoomMemberListViewState.kt        |  3 ++-
     vector/src/main/res/values/strings.xml        |  3 +++
     5 files changed, 48 insertions(+), 4 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListAction.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListAction.kt
    index 01a35b84d3..d6a63197bd 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListAction.kt
    @@ -18,4 +18,6 @@ package im.vector.riotx.features.roomprofile.members
     
     import im.vector.riotx.core.platform.VectorViewModelAction
     
    -sealed class RoomMemberListAction : VectorViewModelAction
    +sealed class RoomMemberListAction : VectorViewModelAction {
    +    data class RevokeThreePidInvite(val stateKey: String) : RoomMemberListAction()
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt
    index 72ec3e8462..6fe1f7ad18 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt
    @@ -20,11 +20,14 @@ import android.os.Bundle
     import android.view.Menu
     import android.view.MenuItem
     import android.view.View
    +import androidx.appcompat.app.AlertDialog
     import com.airbnb.mvrx.args
     import com.airbnb.mvrx.fragmentViewModel
     import com.airbnb.mvrx.withState
     import im.vector.matrix.android.api.session.events.model.Event
    +import im.vector.matrix.android.api.session.events.model.toModel
     import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
    +import im.vector.matrix.android.api.session.room.model.RoomThirdPartyInviteContent
     import im.vector.matrix.android.api.util.toMatrixItem
     import im.vector.riotx.R
     import im.vector.riotx.core.extensions.cleanup
    @@ -90,7 +93,19 @@ class RoomMemberListFragment @Inject constructor(
         }
     
         override fun onThreePidInvites(event: Event) {
    -        // TODO Display a bottom sheet to revoke invite if power level is high enough
    +        // Display a dialog to revoke invite if power level is high enough
    +        val content = event.content.toModel() ?: return
    +        val stateKey = event.stateKey ?: return
    +        if (withState(viewModel) { it.actionsPermissions.canRevokeThreePidInvite }) {
    +            AlertDialog.Builder(requireActivity())
    +                    .setTitle(R.string.three_pid_revoke_invite_dialog_title)
    +                    .setMessage(getString(R.string.three_pid_revoke_invite_dialog_content, content.displayName))
    +                    .setNegativeButton(R.string.cancel, null)
    +                    .setPositiveButton(R.string.revoke) { _, _ ->
    +                        viewModel.handle(RoomMemberListAction.RevokeThreePidInvite(stateKey))
    +                    }
    +                    .show()
    +        }
         }
     
         private fun renderRoomSummary(state: RoomMemberListViewState) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt
    index e412e41b73..23d5e61399 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt
    @@ -16,11 +16,13 @@
     
     package im.vector.riotx.features.roomprofile.members
     
    +import androidx.lifecycle.viewModelScope
     import com.airbnb.mvrx.FragmentViewModelContext
     import com.airbnb.mvrx.MvRxViewModelFactory
     import com.airbnb.mvrx.ViewModelContext
     import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
    +import im.vector.matrix.android.api.NoOpMatrixCallback
     import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
     import im.vector.matrix.android.api.extensions.orFalse
     import im.vector.matrix.android.api.query.QueryStringValue
    @@ -37,12 +39,14 @@ import im.vector.matrix.rx.asObservable
     import im.vector.matrix.rx.mapOptional
     import im.vector.matrix.rx.rx
     import im.vector.matrix.rx.unwrap
    +import im.vector.riotx.core.extensions.exhaustive
     import im.vector.riotx.core.platform.EmptyViewEvents
     import im.vector.riotx.core.platform.VectorViewModel
     import im.vector.riotx.features.powerlevel.PowerLevelsObservableFactory
     import io.reactivex.Observable
     import io.reactivex.android.schedulers.AndroidSchedulers
     import io.reactivex.functions.BiFunction
    +import kotlinx.coroutines.launch
     import timber.log.Timber
     
     class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState: RoomMemberListViewState,
    @@ -125,7 +129,12 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
             PowerLevelsObservableFactory(room).createObservable()
                     .subscribe {
                         val permissions = ActionPermissions(
    -                            canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId)
    +                            canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId),
    +                            canRevokeThreePidInvite = PowerLevelsHelper(it).isUserAllowedToSend(
    +                                    userId = session.myUserId,
    +                                    isState = true,
    +                                    eventType = EventType.STATE_ROOM_THIRD_PARTY_INVITE
    +                            )
                         )
                         setState {
                             copy(actionsPermissions = permissions)
    @@ -177,5 +186,19 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
         }
     
         override fun handle(action: RoomMemberListAction) {
    +        when (action) {
    +            is RoomMemberListAction.RevokeThreePidInvite -> handleRevokeThreePidInvite(action)
    +        }.exhaustive
    +    }
    +
    +    private fun handleRevokeThreePidInvite(action: RoomMemberListAction.RevokeThreePidInvite) {
    +        viewModelScope.launch {
    +            room.sendStateEvent(
    +                    eventType = EventType.STATE_ROOM_THIRD_PARTY_INVITE,
    +                    stateKey = action.stateKey,
    +                    body = emptyMap(),
    +                    callback = NoOpMatrixCallback()
    +            )
    +        }
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewState.kt
    index 6a8738b450..55fb950a8e 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewState.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewState.kt
    @@ -40,7 +40,8 @@ data class RoomMemberListViewState(
     }
     
     data class ActionPermissions(
    -        val canInvite: Boolean = false
    +        val canInvite: Boolean = false,
    +        val canRevokeThreePidInvite: Boolean = false
     )
     
     typealias RoomMemberSummaries = List>>
    diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
    index 62e27b51c2..15b658caeb 100644
    --- a/vector/src/main/res/values/strings.xml
    +++ b/vector/src/main/res/values/strings.xml
    @@ -2550,4 +2550,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
         Retrieving your contacts…
         Your contact book is empty
         Contacts book
    +
    +    Revoke invite
    +    Revoke invite to %1$s?
     
    
    From 3d68b15e60398bad316765a00efca466441621aa Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 10 Jul 2020 09:43:32 +0200
    Subject: [PATCH 108/122] Disable fetching Msisdn, it does not work
    
    ---
     .../riotx/core/contacts/ContactsDataSource.kt | 113 ++++++++++--------
     .../contactsbook/ContactsBookViewModel.kt     |   6 +-
     2 files changed, 67 insertions(+), 52 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt b/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt
    index 4307d106f9..1e487f5a23 100644
    --- a/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt
    @@ -29,8 +29,16 @@ class ContactsDataSource @Inject constructor(
             private val context: Context
     ) {
     
    +    /**
    +     * Will return a list of contact from the contacts book of the device, with at least one email or phone.
    +     * If both param are false, you will get en empty list.
    +     * Note: The return list does not contain any matrixId.
    +     */
         @WorkerThread
    -    fun getContacts(): List {
    +    fun getContacts(
    +            withEmails: Boolean,
    +            withMsisdn: Boolean
    +    ): List {
             val map = mutableMapOf()
             val contentResolver = context.contentResolver
     
    @@ -69,60 +77,63 @@ class ContactsDataSource @Inject constructor(
                         }
     
                 // Get the phone numbers
    -            contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
    -                    arrayOf(
    -                            ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
    -                            ContactsContract.CommonDataKinds.Phone.NUMBER
    -                    ),
    -                    null,
    -                    null,
    -                    null)
    -                    ?.use { innerCursor ->
    -                        while (innerCursor.moveToNext()) {
    -                            val mappedContactBuilder = innerCursor.getLong(ContactsContract.CommonDataKinds.Phone.CONTACT_ID)
    -                                    ?.let { map[it] }
    -                                    ?: continue
    -                            innerCursor.getString(ContactsContract.CommonDataKinds.Phone.NUMBER)
    -                                    ?.let {
    -                                        mappedContactBuilder.msisdns.add(
    -                                                MappedMsisdn(
    -                                                        phoneNumber = it,
    -                                                        matrixId = null
    -                                                )
    -                                        )
    -                                    }
    +            if (withMsisdn) {
    +                contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
    +                        arrayOf(
    +                                ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
    +                                ContactsContract.CommonDataKinds.Phone.NUMBER
    +                        ),
    +                        null,
    +                        null,
    +                        null)
    +                        ?.use { innerCursor ->
    +                            while (innerCursor.moveToNext()) {
    +                                val mappedContactBuilder = innerCursor.getLong(ContactsContract.CommonDataKinds.Phone.CONTACT_ID)
    +                                        ?.let { map[it] }
    +                                        ?: continue
    +                                innerCursor.getString(ContactsContract.CommonDataKinds.Phone.NUMBER)
    +                                        ?.let {
    +                                            mappedContactBuilder.msisdns.add(
    +                                                    MappedMsisdn(
    +                                                            phoneNumber = it,
    +                                                            matrixId = null
    +                                                    )
    +                                            )
    +                                        }
    +                            }
                             }
    -                    }
    +            }
     
                 // Get Emails
    -            contentResolver.query(
    -                    ContactsContract.CommonDataKinds.Email.CONTENT_URI,
    -                    arrayOf(
    -                            ContactsContract.CommonDataKinds.Email.CONTACT_ID,
    -                            ContactsContract.CommonDataKinds.Email.DATA
    -                    ),
    -                    null,
    -                    null,
    -                    null)
    -                    ?.use { innerCursor ->
    -                        while (innerCursor.moveToNext()) {
    -                            // This would allow you get several email addresses
    -                            // if the email addresses were stored in an array
    -                            val mappedContactBuilder = innerCursor.getLong(ContactsContract.CommonDataKinds.Email.CONTACT_ID)
    -                                    ?.let { map[it] }
    -                                    ?: continue
    -                            innerCursor.getString(ContactsContract.CommonDataKinds.Email.DATA)
    -                                    ?.let {
    -                                        mappedContactBuilder.emails.add(
    -                                                MappedEmail(
    -                                                        email = it,
    -                                                        matrixId = null
    -                                                )
    -                                        )
    -                                    }
    +            if (withEmails) {
    +                contentResolver.query(
    +                        ContactsContract.CommonDataKinds.Email.CONTENT_URI,
    +                        arrayOf(
    +                                ContactsContract.CommonDataKinds.Email.CONTACT_ID,
    +                                ContactsContract.CommonDataKinds.Email.DATA
    +                        ),
    +                        null,
    +                        null,
    +                        null)
    +                        ?.use { innerCursor ->
    +                            while (innerCursor.moveToNext()) {
    +                                // This would allow you get several email addresses
    +                                // if the email addresses were stored in an array
    +                                val mappedContactBuilder = innerCursor.getLong(ContactsContract.CommonDataKinds.Email.CONTACT_ID)
    +                                        ?.let { map[it] }
    +                                        ?: continue
    +                                innerCursor.getString(ContactsContract.CommonDataKinds.Email.DATA)
    +                                        ?.let {
    +                                            mappedContactBuilder.emails.add(
    +                                                    MappedEmail(
    +                                                            email = it,
    +                                                            matrixId = null
    +                                                    )
    +                                            )
    +                                        }
    +                            }
                             }
    -                    }
    -
    +            }
             }.also { Timber.d("Took ${it}ms to fetch ${map.size} contact(s)") }
     
             return map
    diff --git a/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookViewModel.kt b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookViewModel.kt
    index c09eac2948..3eb6b165b8 100644
    --- a/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/contactsbook/ContactsBookViewModel.kt
    @@ -90,7 +90,11 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted
             }
     
             viewModelScope.launch(Dispatchers.IO) {
    -            allContacts = contactsDataSource.getContacts()
    +            allContacts = contactsDataSource.getContacts(
    +                    withEmails = true,
    +                    // Do not handle phone numbers for the moment
    +                    withMsisdn = false
    +            )
                 mappedContacts = allContacts
     
                 setState {
    
    From 4ba1a34f387bdd4d2eca54e47897245ebab5661e Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 10 Jul 2020 09:46:34 +0200
    Subject: [PATCH 109/122] Hide right arrow if threepid invite can not be
     revoked
    
    ---
     .../vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt   | 6 +++++-
     .../roomprofile/members/RoomMemberListController.kt         | 1 +
     2 files changed, 6 insertions(+), 1 deletion(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt
    index d6629c708e..b89da07984 100644
    --- a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt
    @@ -20,6 +20,7 @@ package im.vector.riotx.core.epoxy.profiles
     import android.view.View
     import android.widget.ImageView
     import android.widget.TextView
    +import androidx.core.view.isVisible
     import com.airbnb.epoxy.EpoxyAttribute
     import com.airbnb.epoxy.EpoxyModelClass
     import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
    @@ -36,6 +37,7 @@ abstract class ProfileMatrixItem : VectorEpoxyModel()
     
         @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
         @EpoxyAttribute lateinit var matrixItem: MatrixItem
    +    @EpoxyAttribute var editable: Boolean = true
         @EpoxyAttribute var userEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
         @EpoxyAttribute var clickListener: View.OnClickListener? = null
     
    @@ -46,9 +48,10 @@ abstract class ProfileMatrixItem : VectorEpoxyModel()
                     .takeIf { it != bestName }
                     // Special case for ThreePid fake matrix item
                     .takeIf { it != "@" }
    -        holder.view.setOnClickListener(clickListener)
    +        holder.view.setOnClickListener(clickListener?.takeIf { editable })
             holder.titleView.text = bestName
             holder.subtitleView.setTextOrHide(matrixId)
    +        holder.editableView.isVisible = editable
             avatarRenderer.render(matrixItem, holder.avatarImageView)
             holder.avatarDecorationImageView.setImageResource(userEncryptionTrustLevel.toImageRes())
         }
    @@ -58,5 +61,6 @@ abstract class ProfileMatrixItem : VectorEpoxyModel()
             val subtitleView by bind(R.id.matrixItemSubtitle)
             val avatarImageView by bind(R.id.matrixItemAvatar)
             val avatarDecorationImageView by bind(R.id.matrixItemAvatarDecoration)
    +        val editableView by bind(R.id.matrixItemEditable)
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListController.kt
    index 6dcf5a0bd3..495d1164a5 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListController.kt
    @@ -100,6 +100,7 @@ class RoomMemberListController @Inject constructor(
                     id("3pid_$idx")
                     matrixItem(content.toMatrixItem())
                     avatarRenderer(avatarRenderer)
    +                editable(data.actionsPermissions.canRevokeThreePidInvite)
                     clickListener { _ ->
                         callback?.onThreePidInvites(event)
                     }
    
    From a58bb776f3db777f83c8e7840ee5ed6d4af8f11d Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 10 Jul 2020 10:14:02 +0200
    Subject: [PATCH 110/122] Display threePid invite along with the other invite
     (code is a bit dirty)
    
    ---
     .../vector/riotx/core/extensions/Iterable.kt  |  6 +-
     .../members/RoomMemberListController.kt       | 83 +++++++++++++------
     vector/src/main/res/values/strings.xml        |  1 -
     3 files changed, 61 insertions(+), 29 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Iterable.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Iterable.kt
    index 987194ea2f..b9907f8789 100644
    --- a/vector/src/main/java/im/vector/riotx/core/extensions/Iterable.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Iterable.kt
    @@ -38,13 +38,13 @@ inline fun > Iterable.lastMinBy(selector: (T) -> R): T?
     /**
      * Call each for each item, and between between each items
      */
    -inline fun  Collection.join(each: (T) -> Unit, between: (T) -> Unit) {
    +inline fun  Collection.join(each: (Int, T) -> Unit, between: (Int, T) -> Unit) {
         val lastIndex = size - 1
         forEachIndexed { idx, t ->
    -        each(t)
    +        each(idx, t)
     
             if (idx != lastIndex) {
    -            between(t)
    +            between(idx, t)
             }
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListController.kt
    index 495d1164a5..8cf93e8589 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListController.kt
    @@ -54,15 +54,29 @@ class RoomMemberListController @Inject constructor(
     
         override fun buildModels(data: RoomMemberListViewState?) {
             val roomMembersByPowerLevel = data?.roomMemberSummaries?.invoke() ?: return
    +        val threePidInvites = data.threePidInvites().orEmpty()
    +        var threePidInvitesDone = threePidInvites.isEmpty()
    +
             for ((powerLevelCategory, roomMemberList) in roomMembersByPowerLevel) {
                 if (roomMemberList.isEmpty()) {
                     continue
                 }
    +
    +            if (powerLevelCategory == RoomMemberListCategories.USER && !threePidInvitesDone) {
    +                // If there is not regular invite, display threepid invite before the regular user
    +                buildProfileSection(
    +                        stringProvider.getString(RoomMemberListCategories.INVITE.titleRes)
    +                )
    +
    +                buildThreePidInvites(data)
    +                threePidInvitesDone = true
    +            }
    +
                 buildProfileSection(
                         stringProvider.getString(powerLevelCategory.titleRes)
                 )
                 roomMemberList.join(
    -                    each = { roomMember ->
    +                    each = { _, roomMember ->
                             profileMatrixItem {
                                 id(roomMember.userId)
                                 matrixItem(roomMember.toMatrixItem())
    @@ -73,40 +87,59 @@ class RoomMemberListController @Inject constructor(
                                 }
                             }
                         },
    -                    between = { roomMemberBefore ->
    +                    between = { _, roomMemberBefore ->
                             dividerItem {
                                 id("divider_${roomMemberBefore.userId}")
                                 color(dividerColor)
                             }
                         }
                 )
    +            if (powerLevelCategory == RoomMemberListCategories.INVITE) {
    +                // Display the threepid invite after the regular invite
    +                dividerItem {
    +                    id("divider_threepidinvites")
    +                    color(dividerColor)
    +                }
    +                buildThreePidInvites(data)
    +                threePidInvitesDone = true
    +            }
    +        }
    +
    +        if (!threePidInvitesDone) {
    +            // If there is not regular invite and no regular user, finally display threepid invite here
    +            buildProfileSection(
    +                    stringProvider.getString(RoomMemberListCategories.INVITE.titleRes)
    +            )
    +
    +            buildThreePidInvites(data)
             }
    -        buildThreePidInvites(data)
         }
     
         private fun buildThreePidInvites(data: RoomMemberListViewState) {
    -        if (data.threePidInvites().isNullOrEmpty()) {
    -            return
    -        }
    -
    -        buildProfileSection(
    -                stringProvider.getString(R.string.room_member_power_level_three_pid_invites)
    -        )
    -
    -        data.threePidInvites()?.forEachIndexed { idx, event ->
    -            val content = event.content.toModel() ?: return@forEachIndexed
    -
    -            profileMatrixItem {
    -                id("3pid_$idx")
    -                matrixItem(content.toMatrixItem())
    -                avatarRenderer(avatarRenderer)
    -                editable(data.actionsPermissions.canRevokeThreePidInvite)
    -                clickListener { _ ->
    -                    callback?.onThreePidInvites(event)
    -                }
    -            }
    -
    -        }
    +        data.threePidInvites()
    +                ?.filter { it.content.toModel() != null }
    +                ?.join(
    +                        each = { idx, event ->
    +                            event.content.toModel()
    +                                    ?.let { content ->
    +                                        profileMatrixItem {
    +                                            id("3pid_$idx")
    +                                            matrixItem(content.toMatrixItem())
    +                                            avatarRenderer(avatarRenderer)
    +                                            editable(data.actionsPermissions.canRevokeThreePidInvite)
    +                                            clickListener { _ ->
    +                                                callback?.onThreePidInvites(event)
    +                                            }
    +                                        }
    +                                    }
    +                        },
    +                        between = { idx, _ ->
    +                            dividerItem {
    +                                id("divider3_$idx")
    +                                color(dividerColor)
    +                            }
    +                        }
    +                )
         }
     
         private fun RoomThirdPartyInviteContent.toMatrixItem(): MatrixItem {
    diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
    index 15b658caeb..be7602af51 100644
    --- a/vector/src/main/res/values/strings.xml
    +++ b/vector/src/main/res/values/strings.xml
    @@ -2148,7 +2148,6 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
         Custom
         Invites
         Users
    -    Other invites
     
         Admin in %1$s
         Moderator in %1$s
    
    From e8f28d7ce43470f53cf962c0d6e6f14d307ca40e Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 10 Jul 2020 11:49:49 +0200
    Subject: [PATCH 111/122] ktlint
    
    ---
     .../api/session/room/model/RoomThirdPartyInviteContent.kt        | 1 -
     .../java/im/vector/riotx/core/contacts/ContactsDataSource.kt     | 1 -
     2 files changed, 2 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomThirdPartyInviteContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomThirdPartyInviteContent.kt
    index 2b8daa0c5b..fa871d186e 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomThirdPartyInviteContent.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomThirdPartyInviteContent.kt
    @@ -64,4 +64,3 @@ data class PublicKeys(
              */
             @Json(name = "public_key") val publicKey: String
     )
    -
    diff --git a/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt b/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt
    index 1e487f5a23..fd23e495b9 100644
    --- a/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/contacts/ContactsDataSource.kt
    @@ -70,7 +70,6 @@ class ContactsDataSource @Inject constructor(
                                             ?.let { Uri.parse(it) }
                                             ?.let { mappedContactBuilder.photoURI = it }
     
    -
                                     map[id] = mappedContactBuilder
                                 }
                             }
    
    From d8a0a1d38e1737048aa63187595d705625f7f88f Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 10 Jul 2020 11:55:11 +0200
    Subject: [PATCH 112/122] Expose other objects in the builder to create a room
    
    ---
     .../session/room/model/create/CreateRoomParamsBuilder.kt    | 5 +++++
     .../session/room/create/CreateRoomParamsInternalBuilder.kt  | 6 ++----
     2 files changed, 7 insertions(+), 4 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParamsBuilder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParamsBuilder.kt
    index 6637e3bcb2..c6799a956f 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParamsBuilder.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParamsBuilder.kt
    @@ -17,6 +17,7 @@
     package im.vector.matrix.android.api.session.room.model.create
     
     import im.vector.matrix.android.api.session.identity.ThreePid
    +import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
     import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
     import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
     import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
    @@ -47,6 +48,10 @@ class CreateRoomParamsBuilder {
     
         var isDirect: Boolean? = null
     
    +    var creationContent: Any? = null
    +
    +    var powerLevelContentOverride: PowerLevelsContent? = null
    +
         /**
          * Mark as a direct message room.
          */
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParamsInternalBuilder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParamsInternalBuilder.kt
    index 29cf0bbac6..765317005a 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParamsInternalBuilder.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParamsInternalBuilder.kt
    @@ -77,13 +77,11 @@ internal class CreateRoomParamsInternalBuilder @Inject constructor(
                     topic = builder.topic,
                     invitedUserIds = builder.invitedUserIds,
                     invite3pids = invite3pids,
    -                // TODO Support this
    -                creationContent = null,
    +                creationContent = builder.creationContent,
                     initialStates = initialStates,
                     preset = builder.preset,
                     isDirect = builder.isDirect,
    -                // TODO Support this
    -                powerLevelContentOverride = null
    +                powerLevelContentOverride = builder.powerLevelContentOverride
             )
         }
     
    
    From ded8acc83692f07deb67729b440b5c3fe9c8836d Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Sat, 11 Jul 2020 22:13:22 +0200
    Subject: [PATCH 113/122] Rename internal class
    
    ---
     .../im/vector/matrix/android/internal/session/room/RoomAPI.kt | 4 ++--
     .../room/create/{CreateRoomParams.kt => CreateRoomBody.kt}    | 2 +-
     .../session/room/create/CreateRoomParamsInternalBuilder.kt    | 4 ++--
     3 files changed, 5 insertions(+), 5 deletions(-)
     rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/{CreateRoomParams.kt => CreateRoomBody.kt} (99%)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt
    index a82c96f93d..fd16b1891e 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt
    @@ -25,7 +25,7 @@ import im.vector.matrix.android.api.util.JsonDict
     import im.vector.matrix.android.internal.network.NetworkConstants
     import im.vector.matrix.android.internal.session.room.alias.AddRoomAliasBody
     import im.vector.matrix.android.internal.session.room.alias.RoomAliasDescription
    -import im.vector.matrix.android.internal.session.room.create.CreateRoomParams
    +import im.vector.matrix.android.internal.session.room.create.CreateRoomBody
     import im.vector.matrix.android.internal.session.room.create.CreateRoomResponse
     import im.vector.matrix.android.internal.session.room.create.JoinRoomResponse
     import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse
    @@ -80,7 +80,7 @@ internal interface RoomAPI {
          */
         @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
         @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "createRoom")
    -    fun createRoom(@Body param: CreateRoomParams): Call
    +    fun createRoom(@Body param: CreateRoomBody): Call
     
         /**
          * Get a list of messages starting from a reference.
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomBody.kt
    similarity index 99%
    rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParams.kt
    rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomBody.kt
    index 525a0501fc..7a27da3607 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParams.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomBody.kt
    @@ -28,7 +28,7 @@ import im.vector.matrix.android.internal.session.room.membership.threepid.ThreeP
      * Parameter to create a room
      */
     @JsonClass(generateAdapter = true)
    -internal data class CreateRoomParams(
    +internal data class CreateRoomBody(
             /**
              * A public visibility indicates that the room will be shown in the published room list.
              * A private visibility will hide the room from the published room list.
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParamsInternalBuilder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParamsInternalBuilder.kt
    index 765317005a..f7088841b8 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParamsInternalBuilder.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParamsInternalBuilder.kt
    @@ -43,7 +43,7 @@ internal class CreateRoomParamsInternalBuilder @Inject constructor(
             private val accessTokenProvider: AccessTokenProvider
     ) {
     
    -    suspend fun build(builder: CreateRoomParamsBuilder): CreateRoomParams {
    +    suspend fun build(builder: CreateRoomParamsBuilder): CreateRoomBody {
             val invite3pids = builder.invite3pids
                     .takeIf { it.isNotEmpty() }
                     .let {
    @@ -70,7 +70,7 @@ internal class CreateRoomParamsInternalBuilder @Inject constructor(
             )
                     .takeIf { it.isNotEmpty() }
     
    -        return CreateRoomParams(
    +        return CreateRoomBody(
                     visibility = builder.visibility,
                     roomAliasName = builder.roomAliasName,
                     name = builder.name,
    
    From e097bd8117fea84a3e8f47d8ed81d59222449dab Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Sat, 11 Jul 2020 22:15:26 +0200
    Subject: [PATCH 114/122] Rename CreateRoomParamsBuilder to CreateRoomParams
     for clarity
    
    ---
     CHANGES.md                                             |  2 +-
     .../src/main/java/im/vector/matrix/rx/RxSession.kt     |  4 ++--
     .../vector/matrix/android/common/CryptoTestHelper.kt   |  6 +++---
     .../android/internal/crypto/gossiping/KeyShareTests.kt |  4 ++--
     .../matrix/android/api/session/room/RoomService.kt     |  4 ++--
     ...{CreateRoomParamsBuilder.kt => CreateRoomParams.kt} |  2 +-
     .../internal/session/room/DefaultRoomService.kt        |  4 ++--
     .../room/create/CreateRoomParamsInternalBuilder.kt     | 10 +++++-----
     .../internal/session/room/create/CreateRoomTask.kt     |  8 ++++----
     .../features/createdirect/CreateDirectRoomViewModel.kt |  4 ++--
     .../verification/VerificationBottomSheetViewModel.kt   |  4 ++--
     .../roomdirectory/createroom/CreateRoomViewModel.kt    |  4 ++--
     12 files changed, 28 insertions(+), 28 deletions(-)
     rename matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/{CreateRoomParamsBuilder.kt => CreateRoomParams.kt} (98%)
    
    diff --git a/CHANGES.md b/CHANGES.md
    index 3978038b8d..8f9d4f956b 100644
    --- a/CHANGES.md
    +++ b/CHANGES.md
    @@ -29,7 +29,7 @@ Translations 🗣:
      -
     
     SDK API changes ⚠️:
    - - CreateRoomParams has been replaced by CreateRoomParamsBuilder
    + - CreateRoomParams has been updated
     
     Build 🧱:
      - Upgrade some dependencies
    diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt
    index 93e2dcae19..ca0bb46f4b 100644
    --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt
    +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt
    @@ -32,7 +32,7 @@ import im.vector.matrix.android.api.session.pushers.Pusher
     import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
     import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
     import im.vector.matrix.android.api.session.room.model.RoomSummary
    -import im.vector.matrix.android.api.session.room.model.create.CreateRoomParamsBuilder
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
     import im.vector.matrix.android.api.session.sync.SyncState
     import im.vector.matrix.android.api.session.user.model.User
     import im.vector.matrix.android.api.session.widgets.model.Widget
    @@ -110,7 +110,7 @@ class RxSession(private val session: Session) {
                     .startWithCallable { session.getThreePids() }
         }
     
    -    fun createRoom(roomParams: CreateRoomParamsBuilder): Single = singleBuilder {
    +    fun createRoom(roomParams: CreateRoomParams): Single = singleBuilder {
             session.createRoom(roomParams, it)
         }
     
    diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt
    index 7e8410a440..26c425241d 100644
    --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt
    +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt
    @@ -30,7 +30,7 @@ import im.vector.matrix.android.api.session.events.model.toContent
     import im.vector.matrix.android.api.session.room.Room
     import im.vector.matrix.android.api.session.room.model.Membership
     import im.vector.matrix.android.api.session.room.model.RoomSummary
    -import im.vector.matrix.android.api.session.room.model.create.CreateRoomParamsBuilder
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
     import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
     import im.vector.matrix.android.api.session.room.timeline.Timeline
     import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
    @@ -65,7 +65,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
             val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
     
             val roomId = mTestHelper.doSync {
    -            aliceSession.createRoom(CreateRoomParamsBuilder().apply { name = "MyRoom" }, it)
    +            aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" }, it)
             }
     
             if (encryptedRoom) {
    @@ -286,7 +286,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
         fun createDM(alice: Session, bob: Session): String {
             val roomId = mTestHelper.doSync {
                 alice.createRoom(
    -                    CreateRoomParamsBuilder().apply {
    +                    CreateRoomParams().apply {
                             invitedUserIds.add(bob.myUserId)
                             setDirectMessage()
                             enableEncryptionIfInvitedUsersSupportIt = true
    diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt
    index e90822a0c7..a5c0913909 100644
    --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt
    +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt
    @@ -27,7 +27,7 @@ import im.vector.matrix.android.api.session.crypto.verification.VerificationTran
     import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
     import im.vector.matrix.android.api.session.events.model.toModel
     import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
    -import im.vector.matrix.android.api.session.room.model.create.CreateRoomParamsBuilder
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
     import im.vector.matrix.android.common.CommonTestHelper
     import im.vector.matrix.android.common.CryptoTestHelper
     import im.vector.matrix.android.common.SessionTestParams
    @@ -66,7 +66,7 @@ class KeyShareTests : InstrumentedTest {
             // Create an encrypted room and add a message
             val roomId = mTestHelper.doSync {
                 aliceSession.createRoom(
    -                    CreateRoomParamsBuilder().apply {
    +                    CreateRoomParams().apply {
                             visibility = RoomDirectoryVisibility.PRIVATE
                             enableEncryption()
                         },
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt
    index 788a074c65..4e7b973bba 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt
    @@ -20,7 +20,7 @@ import androidx.lifecycle.LiveData
     import im.vector.matrix.android.api.MatrixCallback
     import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
     import im.vector.matrix.android.api.session.room.model.RoomSummary
    -import im.vector.matrix.android.api.session.room.model.create.CreateRoomParamsBuilder
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
     import im.vector.matrix.android.api.util.Cancelable
     import im.vector.matrix.android.api.util.Optional
     
    @@ -32,7 +32,7 @@ interface RoomService {
         /**
          * Create a room asynchronously
          */
    -    fun createRoom(createRoomParams: CreateRoomParamsBuilder,
    +    fun createRoom(createRoomParams: CreateRoomParams,
                        callback: MatrixCallback): Cancelable
     
         /**
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParamsBuilder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt
    similarity index 98%
    rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParamsBuilder.kt
    rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt
    index c6799a956f..8611395071 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParamsBuilder.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt
    @@ -22,7 +22,7 @@ import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
     import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
     import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
     
    -class CreateRoomParamsBuilder {
    +class CreateRoomParams {
         var visibility: RoomDirectoryVisibility? = null
         var roomAliasName: String? = null
         var name: String? = null
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt
    index 7d5b8ac341..b8b4c968b1 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt
    @@ -23,7 +23,7 @@ import im.vector.matrix.android.api.session.room.RoomService
     import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
     import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
     import im.vector.matrix.android.api.session.room.model.RoomSummary
    -import im.vector.matrix.android.api.session.room.model.create.CreateRoomParamsBuilder
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
     import im.vector.matrix.android.api.util.Cancelable
     import im.vector.matrix.android.api.util.Optional
     import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
    @@ -49,7 +49,7 @@ internal class DefaultRoomService @Inject constructor(
             private val taskExecutor: TaskExecutor
     ) : RoomService {
     
    -    override fun createRoom(createRoomParams: CreateRoomParamsBuilder, callback: MatrixCallback): Cancelable {
    +    override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback): Cancelable {
             return createRoomTask
                     .configureWith(createRoomParams) {
                         this.callback = callback
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParamsInternalBuilder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParamsInternalBuilder.kt
    index f7088841b8..afc5066ac9 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParamsInternalBuilder.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParamsInternalBuilder.kt
    @@ -22,7 +22,7 @@ import im.vector.matrix.android.api.session.events.model.EventType
     import im.vector.matrix.android.api.session.events.model.toContent
     import im.vector.matrix.android.api.session.identity.IdentityServiceError
     import im.vector.matrix.android.api.session.identity.toMedium
    -import im.vector.matrix.android.api.session.room.model.create.CreateRoomParamsBuilder
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
     import im.vector.matrix.android.internal.crypto.DeviceListManager
     import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
     import im.vector.matrix.android.internal.di.AuthenticatedIdentity
    @@ -43,7 +43,7 @@ internal class CreateRoomParamsInternalBuilder @Inject constructor(
             private val accessTokenProvider: AccessTokenProvider
     ) {
     
    -    suspend fun build(builder: CreateRoomParamsBuilder): CreateRoomBody {
    +    suspend fun build(builder: CreateRoomParams): CreateRoomBody {
             val invite3pids = builder.invite3pids
                     .takeIf { it.isNotEmpty() }
                     .let {
    @@ -85,7 +85,7 @@ internal class CreateRoomParamsInternalBuilder @Inject constructor(
             )
         }
     
    -    private fun buildHistoryVisibilityEvent(builder: CreateRoomParamsBuilder): Event? {
    +    private fun buildHistoryVisibilityEvent(builder: CreateRoomParams): Event? {
             return builder.historyVisibility
                     ?.let {
                         val contentMap = mapOf("history_visibility" to it)
    @@ -100,7 +100,7 @@ internal class CreateRoomParamsInternalBuilder @Inject constructor(
         /**
          * Add the crypto algorithm to the room creation parameters.
          */
    -    private suspend fun buildEncryptionWithAlgorithmEvent(builder: CreateRoomParamsBuilder): Event? {
    +    private suspend fun buildEncryptionWithAlgorithmEvent(builder: CreateRoomParams): Event? {
             if (builder.algorithm == null
                     && canEnableEncryption(builder)) {
                 // Enable the encryption
    @@ -121,7 +121,7 @@ internal class CreateRoomParamsInternalBuilder @Inject constructor(
                     }
         }
     
    -    private suspend fun canEnableEncryption(builder: CreateRoomParamsBuilder): Boolean {
    +    private suspend fun canEnableEncryption(builder: CreateRoomParams): Boolean {
             return (builder.enableEncryptionIfInvitedUsersSupportIt
                     && crossSigningService.isCrossSigningVerified()
                     && builder.invite3pids.isEmpty())
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt
    index e32f8e39ab..d0b746af7d 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt
    @@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.session.room.create
     
     import com.zhuinden.monarchy.Monarchy
     import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure
    -import im.vector.matrix.android.api.session.room.model.create.CreateRoomParamsBuilder
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
     import im.vector.matrix.android.internal.database.awaitNotEmptyResult
     import im.vector.matrix.android.internal.database.model.RoomEntity
     import im.vector.matrix.android.internal.database.model.RoomEntityFields
    @@ -38,7 +38,7 @@ import org.greenrobot.eventbus.EventBus
     import java.util.concurrent.TimeUnit
     import javax.inject.Inject
     
    -internal interface CreateRoomTask : Task
    +internal interface CreateRoomTask : Task
     
     internal class DefaultCreateRoomTask @Inject constructor(
             private val roomAPI: RoomAPI,
    @@ -52,7 +52,7 @@ internal class DefaultCreateRoomTask @Inject constructor(
             private val eventBus: EventBus
     ) : CreateRoomTask {
     
    -    override suspend fun execute(params: CreateRoomParamsBuilder): String {
    +    override suspend fun execute(params: CreateRoomParams): String {
             val createRoomParams = createRoomParamsInternalBuilder.build(params)
     
             val createRoomResponse = executeRequest(eventBus) {
    @@ -75,7 +75,7 @@ internal class DefaultCreateRoomTask @Inject constructor(
             return roomId
         }
     
    -    private suspend fun handleDirectChatCreation(params: CreateRoomParamsBuilder, roomId: String) {
    +    private suspend fun handleDirectChatCreation(params: CreateRoomParams, roomId: String) {
             val otherUserId = params.getFirstInvitedUserId()
                     ?: throw IllegalStateException("You can't create a direct room without an invitedUser")
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt
    index da81c13747..319671b230 100644
    --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt
    @@ -22,7 +22,7 @@ import com.airbnb.mvrx.ViewModelContext
     import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.session.Session
    -import im.vector.matrix.android.api.session.room.model.create.CreateRoomParamsBuilder
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
     import im.vector.matrix.rx.rx
     import im.vector.riotx.core.extensions.exhaustive
     import im.vector.riotx.core.platform.VectorViewModel
    @@ -54,7 +54,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
         }
     
         private fun createRoomAndInviteSelectedUsers(invitees: Set) {
    -        val roomParams = CreateRoomParamsBuilder()
    +        val roomParams = CreateRoomParams()
                     .apply {
                         invitees.forEach {
                             when (it) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
    index 1833688c35..53c9deb296 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
    @@ -43,7 +43,7 @@ import im.vector.matrix.android.api.session.crypto.verification.VerificationServ
     import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
     import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
     import im.vector.matrix.android.api.session.events.model.LocalEcho
    -import im.vector.matrix.android.api.session.room.model.create.CreateRoomParamsBuilder
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
     import im.vector.matrix.android.api.util.MatrixItem
     import im.vector.matrix.android.api.util.toMatrixItem
     import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
    @@ -235,7 +235,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
                                     pendingRequest = Loading()
                             )
                         }
    -                    val roomParams = CreateRoomParamsBuilder()
    +                    val roomParams = CreateRoomParams()
                                 .apply {
                                     invitedUserIds.add(otherUserId)
                                     setDirectMessage()
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewModel.kt
    index 5cb279c848..b75e9444fe 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewModel.kt
    @@ -28,7 +28,7 @@ import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.MatrixCallback
     import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
    -import im.vector.matrix.android.api.session.room.model.create.CreateRoomParamsBuilder
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
     import im.vector.matrix.android.api.session.room.model.create.CreateRoomPreset
     import im.vector.riotx.core.platform.EmptyViewEvents
     import im.vector.riotx.core.platform.VectorViewModel
    @@ -84,7 +84,7 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
                 copy(asyncCreateRoomRequest = Loading())
             }
     
    -        val createRoomParams = CreateRoomParamsBuilder()
    +        val createRoomParams = CreateRoomParams()
                     .apply {
                         name = state.roomName.takeIf { it.isNotBlank() }
                         // Directory visibility
    
    From a456f4c6a593fecb28dea1d25085f4b9a045b478 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Sat, 11 Jul 2020 22:16:35 +0200
    Subject: [PATCH 115/122] Rename CreateRoomParamsInternalBuilder to
     CreateRoomBodyBuilder for clarity
    
    ---
     ...eRoomParamsInternalBuilder.kt => CreateRoomBodyBuilder.kt} | 2 +-
     .../android/internal/session/room/create/CreateRoomTask.kt    | 4 ++--
     2 files changed, 3 insertions(+), 3 deletions(-)
     rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/{CreateRoomParamsInternalBuilder.kt => CreateRoomBodyBuilder.kt} (98%)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParamsInternalBuilder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomBodyBuilder.kt
    similarity index 98%
    rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParamsInternalBuilder.kt
    rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomBodyBuilder.kt
    index afc5066ac9..442dde8ce2 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomParamsInternalBuilder.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomBodyBuilder.kt
    @@ -34,7 +34,7 @@ import im.vector.matrix.android.internal.session.room.membership.threepid.ThreeP
     import java.security.InvalidParameterException
     import javax.inject.Inject
     
    -internal class CreateRoomParamsInternalBuilder @Inject constructor(
    +internal class CreateRoomBodyBuilder @Inject constructor(
             private val ensureIdentityTokenTask: EnsureIdentityTokenTask,
             private val crossSigningService: CrossSigningService,
             private val deviceListManager: DeviceListManager,
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt
    index d0b746af7d..40cd872287 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt
    @@ -48,12 +48,12 @@ internal class DefaultCreateRoomTask @Inject constructor(
             private val readMarkersTask: SetReadMarkersTask,
             @SessionDatabase
             private val realmConfiguration: RealmConfiguration,
    -        private val createRoomParamsInternalBuilder: CreateRoomParamsInternalBuilder,
    +        private val createRoomBodyBuilder: CreateRoomBodyBuilder,
             private val eventBus: EventBus
     ) : CreateRoomTask {
     
         override suspend fun execute(params: CreateRoomParams): String {
    -        val createRoomParams = createRoomParamsInternalBuilder.build(params)
    +        val createRoomParams = createRoomBodyBuilder.build(params)
     
             val createRoomResponse = executeRequest(eventBus) {
                 apiCall = roomAPI.createRoom(createRoomParams)
    
    From 0f327fc75f8e4f80e02619496114b097c08f9827 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Sat, 11 Jul 2020 22:17:55 +0200
    Subject: [PATCH 116/122] Latest renaming
    
    ---
     .../room/create/CreateRoomBodyBuilder.kt      | 52 +++++++++----------
     .../session/room/create/CreateRoomTask.kt     |  4 +-
     2 files changed, 28 insertions(+), 28 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomBodyBuilder.kt
    index 442dde8ce2..23eb88bea9 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomBodyBuilder.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomBodyBuilder.kt
    @@ -43,8 +43,8 @@ internal class CreateRoomBodyBuilder @Inject constructor(
             private val accessTokenProvider: AccessTokenProvider
     ) {
     
    -    suspend fun build(builder: CreateRoomParams): CreateRoomBody {
    -        val invite3pids = builder.invite3pids
    +    suspend fun build(params: CreateRoomParams): CreateRoomBody {
    +        val invite3pids = params.invite3pids
                     .takeIf { it.isNotEmpty() }
                     .let {
                         // This can throw Exception if Identity server is not configured
    @@ -54,7 +54,7 @@ internal class CreateRoomBodyBuilder @Inject constructor(
                                 ?: throw IdentityServiceError.NoIdentityServerConfigured
                         val identityServerAccessToken = accessTokenProvider.getToken() ?: throw IdentityServiceError.NoIdentityServerConfigured
     
    -                    builder.invite3pids.map {
    +                    params.invite3pids.map {
                             ThreePidInviteBody(
                                     id_server = identityServerUrlWithoutProtocol,
                                     id_access_token = identityServerAccessToken,
    @@ -65,28 +65,28 @@ internal class CreateRoomBodyBuilder @Inject constructor(
                     }
     
             val initialStates = listOfNotNull(
    -                buildEncryptionWithAlgorithmEvent(builder),
    -                buildHistoryVisibilityEvent(builder)
    +                buildEncryptionWithAlgorithmEvent(params),
    +                buildHistoryVisibilityEvent(params)
             )
                     .takeIf { it.isNotEmpty() }
     
             return CreateRoomBody(
    -                visibility = builder.visibility,
    -                roomAliasName = builder.roomAliasName,
    -                name = builder.name,
    -                topic = builder.topic,
    -                invitedUserIds = builder.invitedUserIds,
    +                visibility = params.visibility,
    +                roomAliasName = params.roomAliasName,
    +                name = params.name,
    +                topic = params.topic,
    +                invitedUserIds = params.invitedUserIds,
                     invite3pids = invite3pids,
    -                creationContent = builder.creationContent,
    +                creationContent = params.creationContent,
                     initialStates = initialStates,
    -                preset = builder.preset,
    -                isDirect = builder.isDirect,
    -                powerLevelContentOverride = builder.powerLevelContentOverride
    +                preset = params.preset,
    +                isDirect = params.isDirect,
    +                powerLevelContentOverride = params.powerLevelContentOverride
             )
         }
     
    -    private fun buildHistoryVisibilityEvent(builder: CreateRoomParams): Event? {
    -        return builder.historyVisibility
    +    private fun buildHistoryVisibilityEvent(params: CreateRoomParams): Event? {
    +        return params.historyVisibility
                     ?.let {
                         val contentMap = mapOf("history_visibility" to it)
     
    @@ -100,13 +100,13 @@ internal class CreateRoomBodyBuilder @Inject constructor(
         /**
          * Add the crypto algorithm to the room creation parameters.
          */
    -    private suspend fun buildEncryptionWithAlgorithmEvent(builder: CreateRoomParams): Event? {
    -        if (builder.algorithm == null
    -                && canEnableEncryption(builder)) {
    +    private suspend fun buildEncryptionWithAlgorithmEvent(params: CreateRoomParams): Event? {
    +        if (params.algorithm == null
    +                && canEnableEncryption(params)) {
                 // Enable the encryption
    -            builder.enableEncryption()
    +            params.enableEncryption()
             }
    -        return builder.algorithm
    +        return params.algorithm
                     ?.let {
                         if (it != MXCRYPTO_ALGORITHM_MEGOLM) {
                             throw InvalidParameterException("Unsupported algorithm: $it")
    @@ -121,12 +121,12 @@ internal class CreateRoomBodyBuilder @Inject constructor(
                     }
         }
     
    -    private suspend fun canEnableEncryption(builder: CreateRoomParams): Boolean {
    -        return (builder.enableEncryptionIfInvitedUsersSupportIt
    +    private suspend fun canEnableEncryption(params: CreateRoomParams): Boolean {
    +        return (params.enableEncryptionIfInvitedUsersSupportIt
                     && crossSigningService.isCrossSigningVerified()
    -                && builder.invite3pids.isEmpty())
    -                && builder.invitedUserIds.isNotEmpty()
    -                && builder.invitedUserIds.let { userIds ->
    +                && params.invite3pids.isEmpty())
    +                && params.invitedUserIds.isNotEmpty()
    +                && params.invitedUserIds.let { userIds ->
                 val keys = deviceListManager.downloadKeys(userIds, forceDownload = false)
     
                 userIds.all { userId ->
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt
    index 40cd872287..e293f2068f 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt
    @@ -53,10 +53,10 @@ internal class DefaultCreateRoomTask @Inject constructor(
     ) : CreateRoomTask {
     
         override suspend fun execute(params: CreateRoomParams): String {
    -        val createRoomParams = createRoomBodyBuilder.build(params)
    +        val createRoomBody = createRoomBodyBuilder.build(params)
     
             val createRoomResponse = executeRequest(eventBus) {
    -            apiCall = roomAPI.createRoom(createRoomParams)
    +            apiCall = roomAPI.createRoom(createRoomBody)
             }
             val roomId = createRoomResponse.roomId
             // Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before)
    
    From 75ef491e3ec984e7d339c6c2432f694e1f010234 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Sat, 11 Jul 2020 22:22:21 +0200
    Subject: [PATCH 117/122] Move internal methods to internal task
    
    ---
     .../room/model/create/CreateRoomParams.kt      | 17 -----------------
     .../session/room/create/CreateRoomTask.kt      | 18 ++++++++++++++++++
     2 files changed, 18 insertions(+), 17 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt
    index 8611395071..40a9437f51 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt
    @@ -71,21 +71,4 @@ class CreateRoomParams {
         fun enableEncryption() {
             algorithm = MXCRYPTO_ALGORITHM_MEGOLM
         }
    -
    -    /**
    -     * Tells if the created room can be a direct chat one.
    -     *
    -     * @return true if it is a direct chat
    -     */
    -    fun isDirect(): Boolean {
    -        return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
    -                && isDirect == true
    -    }
    -
    -    /**
    -     * @return the first invited user id
    -     */
    -    fun getFirstInvitedUserId(): String? {
    -        return invitedUserIds.firstOrNull() ?: invite3pids.firstOrNull()?.value
    -    }
     }
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt
    index e293f2068f..791091c549 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt
    @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.room.create
     import com.zhuinden.monarchy.Monarchy
     import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure
     import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomPreset
     import im.vector.matrix.android.internal.database.awaitNotEmptyResult
     import im.vector.matrix.android.internal.database.model.RoomEntity
     import im.vector.matrix.android.internal.database.model.RoomEntityFields
    @@ -93,4 +94,21 @@ internal class DefaultCreateRoomTask @Inject constructor(
             val setReadMarkerParams = SetReadMarkersTask.Params(roomId, forceReadReceipt = true, forceReadMarker = true)
             return readMarkersTask.execute(setReadMarkerParams)
         }
    +
    +    /**
    +     * Tells if the created room can be a direct chat one.
    +     *
    +     * @return true if it is a direct chat
    +     */
    +    private fun CreateRoomParams.isDirect(): Boolean {
    +        return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
    +                && isDirect == true
    +    }
    +
    +    /**
    +     * @return the first invited user id
    +     */
    +    private fun CreateRoomParams.getFirstInvitedUserId(): String? {
    +        return invitedUserIds.firstOrNull() ?: invite3pids.firstOrNull()?.value
    +    }
     }
    
    From 602d67155fbe77320b5fd5f16b7f5dcfabb5040f Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Sat, 11 Jul 2020 22:25:21 +0200
    Subject: [PATCH 118/122] Copy Javadoc to the API class
    
    ---
     .../room/model/create/CreateRoomParams.kt     | 46 ++++++++++++++++++-
     1 file changed, 44 insertions(+), 2 deletions(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt
    index 40a9437f51..5c00e9eccc 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt
    @@ -23,18 +23,41 @@ import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
     import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
     
     class CreateRoomParams {
    +    /**
    +     * A public visibility indicates that the room will be shown in the published room list.
    +     * A private visibility will hide the room from the published room list.
    +     * Rooms default to private visibility if this key is not included.
    +     * NB: This should not be confused with join_rules which also uses the word public. One of: ["public", "private"]
    +     */
         var visibility: RoomDirectoryVisibility? = null
    +
    +    /**
    +     * The desired room alias local part. If this is included, a room alias will be created and mapped to the newly created room.
    +     * The alias will belong on the same homeserver which created the room.
    +     * For example, if this was set to "foo" and sent to the homeserver "example.com" the complete room alias would be #foo:example.com.
    +     */
         var roomAliasName: String? = null
    +
    +    /**
    +     * If this is not null, an m.room.name event will be sent into the room to indicate the name of the room.
    +     * See Room Events for more information on m.room.name.
    +     */
         var name: String? = null
    +
    +    /**
    +     * If this is not null, an m.room.topic event will be sent into the room to indicate the topic for the room.
    +     * See Room Events for more information on m.room.topic.
    +     */
         var topic: String? = null
     
         /**
    -     * UserIds to invite
    +     * A list of user IDs to invite to the room.
    +     * This will tell the server to invite everyone in the list to the newly created room.
          */
         val invitedUserIds = mutableListOf()
     
         /**
    -     * ThreePids to invite
    +     * A list of objects representing third party IDs to invite into the room.
          */
         val invite3pids = mutableListOf()
     
    @@ -44,12 +67,31 @@ class CreateRoomParams {
          */
         var enableEncryptionIfInvitedUsersSupportIt: Boolean = false
     
    +    /**
    +     * Convenience parameter for setting various default state events based on a preset. Must be either:
    +     * private_chat => join_rules is set to invite. history_visibility is set to shared.
    +     * trusted_private_chat => join_rules is set to invite. history_visibility is set to shared. All invitees are given the same power level as the
    +     * room creator.
    +     * public_chat: => join_rules is set to public. history_visibility is set to shared.
    +     */
         var preset: CreateRoomPreset? = null
     
    +    /**
    +     * This flag makes the server set the is_direct flag on the m.room.member events sent to the users in invite and invite_3pid.
    +     * See Direct Messaging for more information.
    +     */
         var isDirect: Boolean? = null
     
    +    /**
    +     * Extra keys to be added to the content of the m.room.create.
    +     * The server will clobber the following keys: creator.
    +     * Future versions of the specification may allow the server to clobber other keys.
    +     */
         var creationContent: Any? = null
     
    +    /**
    +     * The power level content to override in the default power level event
    +     */
         var powerLevelContentOverride: PowerLevelsContent? = null
     
         /**
    
    From ece9fbd3bbab05ff91236e6272acfe652317ac9b Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Sat, 11 Jul 2020 22:26:21 +0200
    Subject: [PATCH 119/122] Add TODO
    
    ---
     .../android/api/session/room/model/create/CreateRoomParams.kt    | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt
    index 5c00e9eccc..f89558801d 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt
    @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
     import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
     import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
     
    +// TODO Give a way to include other initial states
     class CreateRoomParams {
         /**
          * A public visibility indicates that the room will be shown in the published room list.
    
    From 68d475dc55910c1897dd816c726591229569a457 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Sat, 11 Jul 2020 22:45:03 +0200
    Subject: [PATCH 120/122] Fix crash after rebase
    
    ---
     vector/src/main/res/layout/fragment_contacts_book.xml | 2 +-
     vector/src/main/res/layout/item_contact_detail.xml    | 4 ++--
     vector/src/main/res/layout/item_contact_main.xml      | 2 +-
     3 files changed, 4 insertions(+), 4 deletions(-)
    
    diff --git a/vector/src/main/res/layout/fragment_contacts_book.xml b/vector/src/main/res/layout/fragment_contacts_book.xml
    index 13a3142cec..eb90da1bbe 100644
    --- a/vector/src/main/res/layout/fragment_contacts_book.xml
    +++ b/vector/src/main/res/layout/fragment_contacts_book.xml
    @@ -37,7 +37,7 @@
                         app:layout_constraintStart_toStartOf="parent"
                         app:layout_constraintTop_toTopOf="parent" />
     
    -                
     
    -    
     
    -    
     
    -    
    Date: Sat, 11 Jul 2020 22:49:29 +0200
    Subject: [PATCH 121/122] Fix test compilation issue
    
    ---
     .../java/im/vector/matrix/android/common/CryptoTestHelper.kt    | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt
    index 26c425241d..08c24227be 100644
    --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt
    +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt
    @@ -175,7 +175,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
             }
     
             mTestHelper.doSync {
    -            samSession.joinRoom(room.roomId, null, it)
    +            samSession.joinRoom(room.roomId, null, emptyList(), it)
             }
     
             return samSession
    
    From f4445958455ff30e387d08bf613f9aeedf90d2da Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Sat, 11 Jul 2020 22:58:34 +0200
    Subject: [PATCH 122/122] Prepare release 0.91.5
    
    ---
     CHANGES.md | 7 ++-----
     1 file changed, 2 insertions(+), 5 deletions(-)
    
    diff --git a/CHANGES.md b/CHANGES.md
    index 8f9d4f956b..db9b472ad9 100644
    --- a/CHANGES.md
    +++ b/CHANGES.md
    @@ -1,8 +1,8 @@
    -Changes in Riot.imX 0.91.5 (2020-XX-XX)
    +Changes in Riot.imX 0.91.5 (2020-07-11)
     ===================================================
     
     Features ✨:
    - -
    + - 3pid invite: it is now possible to invite people by email. An Identity Server has to be configured (#548)
     
     Improvements 🙌:
      - Cleaning chunks with lots of events as long as a threshold has been exceeded (35_000 events in DB) (#1634)
    @@ -25,9 +25,6 @@ Bugfix 🐛:
      - verification issues on transition (#1555)
      - Fix issue when restoring keys backup using recovery key
     
    -Translations 🗣:
    - -
    -
     SDK API changes ⚠️:
      - CreateRoomParams has been updated