From a2367ef14f6c9b371d95f41d0758494e3f99fe14 Mon Sep 17 00:00:00 2001 From: unclejay <2KPKNc#6RaKl> Date: Mon, 16 Mar 2020 21:12:15 +0100 Subject: [PATCH 001/191] added network proxy configuration --- CHANGES.md | 2 +- .../im/vector/matrix/android/api/Matrix.kt | 4 +++- .../android/api/config/ProxyConfiguration.kt | 24 +++++++++++++++++++ .../android/internal/di/NetworkModule.kt | 9 ++++++- 4 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/config/ProxyConfiguration.kt diff --git a/CHANGES.md b/CHANGES.md index d26237fd13..b1034f9ad6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,7 +14,7 @@ Translations 🗣: - SDK API changes ⚠️: - - + - initialize with proxy configuration Build 🧱: - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt index 22ac0324cf..4ebbeced8c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt @@ -23,6 +23,7 @@ import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.api.auth.AuthenticationService +import im.vector.matrix.android.api.config.ProxyConfiguration import im.vector.matrix.android.api.crypto.MXCryptoConfig import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt @@ -37,7 +38,8 @@ import javax.inject.Inject data class MatrixConfiguration( val applicationFlavor: String = "Default-application-flavor", - val cryptoConfig: MXCryptoConfig = MXCryptoConfig() + val cryptoConfig: MXCryptoConfig = MXCryptoConfig(), + val proxyConfig: ProxyConfiguration? = null ) { interface Provider { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/config/ProxyConfiguration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/config/ProxyConfiguration.kt new file mode 100644 index 0000000000..b23ffa82f9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/config/ProxyConfiguration.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.config + +import java.net.Proxy + +/** + * This is the configuration to use a proxy to connect to the matrix servers + */ +data class ProxyConfiguration(val hostname: String, val port: Int, val proxyType: Proxy.Type) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt index 4d6c66b7ed..a3ebeb5e11 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt @@ -21,6 +21,7 @@ import com.squareup.moshi.Moshi import dagger.Module import dagger.Provides import im.vector.matrix.android.BuildConfig +import im.vector.matrix.android.api.MatrixConfiguration import im.vector.matrix.android.internal.network.TimeOutInterceptor import im.vector.matrix.android.internal.network.UserAgentInterceptor import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor @@ -28,6 +29,8 @@ import im.vector.matrix.android.internal.network.interceptors.FormattedJsonHttpL import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import okreplay.OkReplayInterceptor +import java.net.InetSocketAddress +import java.net.Proxy import java.util.concurrent.TimeUnit @Module @@ -64,7 +67,8 @@ internal object NetworkModule { @Provides @JvmStatic @Unauthenticated - fun providesOkHttpClient(stethoInterceptor: StethoInterceptor, + fun providesOkHttpClient(matrixConfiguration: MatrixConfiguration, + stethoInterceptor: StethoInterceptor, timeoutInterceptor: TimeOutInterceptor, userAgentInterceptor: UserAgentInterceptor, httpLoggingInterceptor: HttpLoggingInterceptor, @@ -82,6 +86,9 @@ internal object NetworkModule { if (BuildConfig.LOG_PRIVATE_DATA) { addInterceptor(curlLoggingInterceptor) } + matrixConfiguration.proxyConfig?.let { + proxy(Proxy(it.proxyType, InetSocketAddress(it.hostname, it.port))) + } } .addInterceptor(okReplayInterceptor) .build() From aa16ba88ae36a763a26b166d2595e7eb1be87a43 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 27 Apr 2020 12:41:47 +0200 Subject: [PATCH 002/191] Add hint to translators --- vector/src/main/res/values/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 370b7cf8f4..93638420e7 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2124,6 +2124,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Verify this session Other users may not trust it + Complete Security Use an existing session to verify this one, granting it access to encrypted messages. From 2c47fe9f0d959f2d9e97af6d7b8c2b7f69635669 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 29 Apr 2020 11:47:33 +0200 Subject: [PATCH 003/191] typo --- .../session/room/timeline/DefaultTimeline.kt | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 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 f2bee734ce..82729f092d 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 @@ -383,7 +383,7 @@ internal class DefaultTimeline( } /** - * This has to be called on TimelineThread as it access realm live results + * This has to be called on TimelineThread as it accesses realm live results * @return true if createSnapshot should be posted */ private fun paginateInternal(startDisplayIndex: Int?, @@ -446,7 +446,7 @@ internal class DefaultTimeline( } /** - * This has to be called on TimelineThread as it access realm live results + * This has to be called on TimelineThread as it accesses realm live results */ private fun handleInitialLoad() { var shouldFetchInitialEvent = false @@ -478,7 +478,7 @@ internal class DefaultTimeline( } /** - * This has to be called on TimelineThread as it access realm live results + * This has to be called on TimelineThread as it accesses realm live results */ private fun handleUpdates(results: RealmResults, changeSet: OrderedCollectionChangeSet) { // If changeSet has deletion we are having a gap, so we clear everything @@ -516,7 +516,7 @@ internal class DefaultTimeline( } /** - * This has to be called on TimelineThread as it access realm live results + * This has to be called on TimelineThread as it accesses realm live results */ private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { val token = getTokenLive(direction) @@ -560,23 +560,22 @@ internal class DefaultTimeline( } /** - * This has to be called on TimelineThread as it access realm live results + * This has to be called on TimelineThread as it accesses realm live results */ - private fun getTokenLive(direction: Timeline.Direction): String? { val chunkEntity = getLiveChunk() ?: return null return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken } /** - * This has to be called on TimelineThread as it access realm live results + * This has to be called on TimelineThread as it accesses realm live results */ private fun getLiveChunk(): ChunkEntity? { return nonFilteredEvents.firstOrNull()?.chunk?.firstOrNull() } /** - * This has to be called on TimelineThread as it access realm live results + * This has to be called on TimelineThread as it accesses realm live results * @return number of items who have been added */ private fun buildTimelineEvents(startDisplayIndex: Int?, @@ -628,7 +627,7 @@ internal class DefaultTimeline( ) /** - * This has to be called on TimelineThread as it access realm live results + * This has to be called on TimelineThread as it accesses realm live results */ private fun getOffsetResults(startDisplayIndex: Int, direction: Timeline.Direction, From 2697800deb7a60000ae13707710a170ebd9c7127 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 29 Apr 2020 11:48:40 +0200 Subject: [PATCH 004/191] Doc and cleanup --- .../android/internal/database/helper/ChunkEntityHelper.kt | 7 +++---- .../matrix/android/internal/database/model/ChunkEntity.kt | 2 ++ 2 files changed, 5 insertions(+), 4 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 80376fb6ee..d86151e694 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 @@ -60,10 +60,9 @@ internal fun ChunkEntity.merge(roomId: String, chunkToMerge: ChunkEntity, direct chunkToMerge.stateEvents.forEach { stateEvent -> addStateEvent(roomId, stateEvent, direction) } - return eventsToMerge - .forEach { - addTimelineEventFromMerge(localRealm, it, direction) - } + eventsToMerge.forEach { + addTimelineEventFromMerge(localRealm, it, direction) + } } internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity, direction: PaginationDirection) { 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 2d294e6783..9c6bf5f757 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 @@ -23,9 +23,11 @@ import io.realm.annotations.Index import io.realm.annotations.LinkingObjects internal open class ChunkEntity(@Index var prevToken: String? = null, + // Because of gaps we can have several chunks with nextToken == null @Index var nextToken: String? = null, var stateEvents: RealmList = RealmList(), var timelineEvents: RealmList = RealmList(), + // Only one chunk will have isLastForward == true @Index var isLastForward: Boolean = false, @Index var isLastBackward: Boolean = false ) : RealmObject() { From 7e955ef0e4e1b1f0cad74e580dbbe6de33b0452a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 29 Apr 2020 11:49:08 +0200 Subject: [PATCH 005/191] Add possibility to create clear room --- .../matrix/android/common/CryptoTestHelper.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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 9278bed918..e4aa7872aa 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 @@ -53,17 +53,19 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { /** * @return alice session */ - fun doE2ETestWithAliceInARoom(): CryptoTestData { + fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true): CryptoTestData { val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams) val roomId = mTestHelper.doSync { aliceSession.createRoom(CreateRoomParams(name = "MyRoom"), it) } - val room = aliceSession.getRoom(roomId)!! + if (encryptedRoom) { + val room = aliceSession.getRoom(roomId)!! - mTestHelper.doSync { - room.enableEncryption(callback = it) + mTestHelper.doSync { + room.enableEncryption(callback = it) + } } return CryptoTestData(aliceSession, roomId) @@ -72,8 +74,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { /** * @return alice and bob sessions */ - fun doE2ETestWithAliceAndBobInARoom(): CryptoTestData { - val cryptoTestData = doE2ETestWithAliceInARoom() + fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true): CryptoTestData { + val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom) val aliceSession = cryptoTestData.firstSession val aliceRoomId = cryptoTestData.roomId From bfd847179f8b55e77ad7759ac0eff85b56f460eb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 29 Apr 2020 11:49:42 +0200 Subject: [PATCH 006/191] Wait more --- .../java/im/vector/matrix/android/common/CommonTestHelper.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt index 5bc8653f3d..ad12fb6bba 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt @@ -143,7 +143,8 @@ class CommonTestHelper(context: Context) { for (i in 0 until nbOfMessages) { room.sendTextMessage(message + " #" + (i + 1)) } - await(latch) + // Wait 3 second more per message + await(latch, timeout = TestConstants.timeOutMillis + 3_000L * nbOfMessages) timeline.removeListener(timelineListener) timeline.dispose() From 20b726819ff8136f1dd2aa5e3ffce099166dc85f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 Apr 2020 17:35:48 +0200 Subject: [PATCH 007/191] Rename "LastLive" -> "LastForward" --- .../android/internal/database/query/ChunkEntityQueries.kt | 2 +- .../matrix/android/internal/database/query/ReadQueries.kt | 2 +- .../internal/database/query/TimelineEventEntityQueries.kt | 2 +- .../session/room/timeline/TokenChunkEventPersistor.kt | 8 ++++---- .../android/internal/session/sync/RoomSyncHandler.kt | 5 +++-- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt index 009ee4b7fe..5efb84a105 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt @@ -41,7 +41,7 @@ internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken: return query.findFirst() } -internal fun ChunkEntity.Companion.findLastLiveChunkFromRoom(realm: Realm, roomId: String): ChunkEntity? { +internal fun ChunkEntity.Companion.findLastForwardChunkOfRoom(realm: Realm, roomId: String): ChunkEntity? { return where(realm, roomId) .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true) .findFirst() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt index 1b83577a8c..9c73dff1dd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt @@ -36,7 +36,7 @@ internal fun isEventRead(monarchy: Monarchy, var isEventRead = false monarchy.doWithRealm { realm -> - val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return@doWithRealm + val liveChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: return@doWithRealm val eventToCheck = liveChunk.timelineEvents.find(eventId) isEventRead = if (eventToCheck == null || eventToCheck.root?.sender == userId) { true diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt index 5168d0728e..c3e9a8288b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt @@ -59,7 +59,7 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm, filterTypes: List = emptyList()): TimelineEventEntity? { val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null val sendingTimelineEvents = roomEntity.sendingTimelineEvents.where().filterTypes(filterTypes) - val liveEvents = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)?.timelineEvents?.where()?.filterTypes(filterTypes) + val liveEvents = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.where()?.filterTypes(filterTypes) if (filterContentRelation) { liveEvents ?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.EDIT_TYPE) 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 164626224b..f6fd88f816 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 @@ -35,7 +35,7 @@ import im.vector.matrix.android.internal.database.query.copyToRealmOrIgnore import im.vector.matrix.android.internal.database.query.create import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findAllIncludingEvents -import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom +import im.vector.matrix.android.internal.database.query.findLastForwardChunkOfRoom import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.where @@ -169,10 +169,10 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy private fun handleReachEnd(realm: Realm, roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) { Timber.v("Reach end of $roomId") if (direction == PaginationDirection.FORWARDS) { - val currentLiveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) - if (currentChunk != currentLiveChunk) { + val currentLastForwardChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) + if (currentChunk != currentLastForwardChunk) { currentChunk.isLastForward = true - currentLiveChunk?.deleteOnCascade() + currentLastForwardChunk?.deleteOnCascade() RoomSummaryEntity.where(realm, roomId).findFirst()?.apply { 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 70c1e39334..8c21d23a8c 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 @@ -36,7 +36,7 @@ import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity 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.find -import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom +import im.vector.matrix.android.internal.database.query.findLastForwardChunkOfRoom import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.getOrNull import im.vector.matrix.android.internal.database.query.where @@ -220,12 +220,13 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle prevToken: String? = null, isLimited: Boolean = true, syncLocalTimestampMillis: Long): ChunkEntity { - val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId) + val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId) val chunkEntity = if (!isLimited && lastChunk != null) { lastChunk } else { realm.createObject().apply { this.prevToken = prevToken } } + // Only one chunk has isLastForward set to true lastChunk?.isLastForward = false chunkEntity.isLastForward = true From a61434ae0877d829d51eb72582606a25517415e9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 Apr 2020 17:37:58 +0200 Subject: [PATCH 008/191] doc --- .../vector/matrix/android/api/session/room/timeline/Timeline.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/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt index d7d6682046..19ff65dbe2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -58,7 +58,7 @@ interface Timeline { /** * Check if the timeline can be enriched by paginating. - * @param the direction to check in + * @param direction the direction to check in * @return true if timeline can be enriched */ fun hasMoreToLoad(direction: Direction): Boolean From becc5a7b541680d65ff2b4acf6de4def0cba199f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 Apr 2020 17:39:01 +0200 Subject: [PATCH 009/191] Add assertion in debug --- .../android/api/session/room/timeline/TimelineEvent.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index 0c8a04db36..7adc438e20 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.api.session.room.timeline +import im.vector.matrix.android.BuildConfig 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.RelationType @@ -45,6 +46,12 @@ data class TimelineEvent( val readReceipts: List = emptyList() ) { + init { + if (BuildConfig.DEBUG) { + assert(eventId == root.eventId) + } + } + val metadata = HashMap() /** From 8966e249256d47fd0e81767df9a883d3637b7420 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 Apr 2020 18:23:18 +0200 Subject: [PATCH 010/191] Create a debug method to send x times the same event --- .../session/room/send/DefaultSendService.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 1037b7c79c..eee1c01295 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -74,6 +74,19 @@ internal class DefaultSendService @AssistedInject constructor( return sendEvent(event) } + // For test only + private fun sendTextMessages(text: CharSequence, msgType: String, autoMarkdown: Boolean, times: Int): Cancelable { + return CancelableBag().apply { + // Send the event several times + repeat(times) { i -> + val event = localEchoEventFactory.createTextEvent(roomId, msgType, "$text - $i", autoMarkdown).also { + createLocalEcho(it) + } + add(sendEvent(event)) + } + } + } + override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable { val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType).also { createLocalEcho(it) From f3c3c07d468e04820cc1e465aec1bc5410aef40f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 Apr 2020 18:36:43 +0200 Subject: [PATCH 011/191] Kotlin sugar --- .../session/room/send/DefaultSendService.kt | 77 +++++++++---------- 1 file changed, 36 insertions(+), 41 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index eee1c01295..9c8723af05 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -68,10 +68,9 @@ internal class DefaultSendService @AssistedInject constructor( private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor() override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable { - val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also { - createLocalEcho(it) - } - return sendEvent(event) + return localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown) + .also { createLocalEcho(it) } + .let { sendEvent(it) } } // For test only @@ -79,33 +78,30 @@ internal class DefaultSendService @AssistedInject constructor( return CancelableBag().apply { // Send the event several times repeat(times) { i -> - val event = localEchoEventFactory.createTextEvent(roomId, msgType, "$text - $i", autoMarkdown).also { - createLocalEcho(it) - } - add(sendEvent(event)) + localEchoEventFactory.createTextEvent(roomId, msgType, "$text - $i", autoMarkdown) + .also { createLocalEcho(it) } + .let { sendEvent(it) } + .also { add(it) } } } } override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable { - val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType).also { - createLocalEcho(it) - } - return sendEvent(event) + return localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType) + .also { createLocalEcho(it) } + .let { sendEvent(it) } } override fun sendPoll(question: String, options: List): Cancelable { - val event = localEchoEventFactory.createPollEvent(roomId, question, options).also { - createLocalEcho(it) - } - return sendEvent(event) + return localEchoEventFactory.createPollEvent(roomId, question, options) + .also { createLocalEcho(it) } + .let { sendEvent(it) } } override fun sendOptionsReply(pollEventId: String, optionIndex: Int, optionValue: String): Cancelable { - val event = localEchoEventFactory.createOptionsReplyEvent(roomId, pollEventId, optionIndex, optionValue).also { - createLocalEcho(it) - } - return sendEvent(event) + return localEchoEventFactory.createOptionsReplyEvent(roomId, pollEventId, optionIndex, optionValue) + .also { createLocalEcho(it) } + .let { sendEvent(it) } } private fun sendEvent(event: Event): Cancelable { @@ -132,8 +128,8 @@ internal class DefaultSendService @AssistedInject constructor( override fun redactEvent(event: Event, reason: String?): Cancelable { // TODO manage media/attachements? - val redactWork = createRedactEventWork(event, reason) - return timelineSendEventWorkCommon.postWork(roomId, redactWork) + return createRedactEventWork(event, reason) + .let { timelineSendEventWorkCommon.postWork(roomId, it) } } override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? { @@ -276,31 +272,30 @@ internal class DefaultSendService @AssistedInject constructor( private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { // Same parameter - val params = EncryptEventWorker.Params(sessionId, event) - val sendWorkData = WorkerParamsFactory.toData(params) - - return workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) - .setInputData(sendWorkData) - .startChain(startChain) - .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) - .build() + return EncryptEventWorker.Params(sessionId, event) + .let { WorkerParamsFactory.toData(it) } + .let { + workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(it) + .startChain(startChain) + .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } } private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { - val sendContentWorkerParams = SendEventWorker.Params(sessionId, event) - val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - - return timelineSendEventWorkCommon.createWork(sendWorkData, startChain) + return SendEventWorker.Params(sessionId, event) + .let { WorkerParamsFactory.toData(it) } + .let { timelineSendEventWorkCommon.createWork(it, startChain) } } private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest { - val redactEvent = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason).also { - createLocalEcho(it) - } - val sendContentWorkerParams = RedactEventWorker.Params(sessionId, redactEvent.eventId!!, roomId, event.eventId, reason) - val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - return timelineSendEventWorkCommon.createWork(redactWorkData, true) + return localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason) + .also { createLocalEcho(it) } + .let { RedactEventWorker.Params(sessionId, it.eventId!!, roomId, event.eventId, reason) } + .let { WorkerParamsFactory.toData(it) } + .let { timelineSendEventWorkCommon.createWork(it, true) } } private fun createUploadMediaWork(allLocalEchos: List, From 86fba283131ab9b5315694ece8af42ba9f6bde78 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 5 May 2020 02:40:50 +0200 Subject: [PATCH 012/191] After jump to unread, newer messages are never loaded (#1008) --- CHANGES.md | 2 +- .../internal/database/model/ChunkEntity.kt | 3 +++ .../session/room/timeline/DefaultTimeline.kt | 27 ++++++++++++++++++- .../room/timeline/TokenChunkEventPersistor.kt | 9 ++++++- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f0cf30af2d..bc04df2e39 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,7 @@ Improvements 🙌: - Bugfix 🐛: - - + - After jump to unread, newer messages are never loaded (#1008) Translations 🗣: - 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 9c6bf5f757..19bf72970c 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 @@ -34,6 +34,9 @@ internal open class ChunkEntity(@Index var prevToken: String? = null, fun identifier() = "${prevToken}_$nextToken" + // If true, then this chunk was previously a last forward chunk + fun hasBeenALastForwardChunk() = nextToken == null && !isLastForward + @LinkingObjects("chunks") val room: RealmResults? = null 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 82729f092d..8cfd498cc8 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 @@ -559,6 +559,28 @@ internal class DefaultTimeline( .executeBy(taskExecutor) } + // For debug purpose only + private fun dumpAndLogChunks() { + val liveChunk = getLiveChunk() + Timber.w("Live chunk: $liveChunk") + + Realm.getInstance(realmConfiguration).use { realm -> + ChunkEntity.where(realm, roomId).findAll() + .also { Timber.w("Found ${it.size} chunks") } + .forEach { + Timber.w("") + Timber.w("ChunkEntity: $it") + Timber.w("prevToken: ${it.prevToken}") + Timber.w("nextToken: ${it.nextToken}") + Timber.w("isLastBackward: ${it.isLastBackward}") + Timber.w("isLastForward: ${it.isLastForward}") + it.timelineEvents.forEach { tle -> + Timber.w(" TLE: ${tle.root?.content}") + } + } + } + } + /** * This has to be called on TimelineThread as it accesses realm live results */ @@ -569,6 +591,7 @@ internal class DefaultTimeline( /** * This has to be called on TimelineThread as it accesses realm live results + * Return the current Chunk */ private fun getLiveChunk(): ChunkEntity? { return nonFilteredEvents.firstOrNull()?.chunk?.firstOrNull() @@ -576,7 +599,7 @@ internal class DefaultTimeline( /** * This has to be called on TimelineThread as it accesses realm live results - * @return number of items who have been added + * @return the number of items who have been added */ private fun buildTimelineEvents(startDisplayIndex: Int?, direction: Timeline.Direction, @@ -617,6 +640,8 @@ internal class DefaultTimeline( } val time = System.currentTimeMillis() - start Timber.v("Built ${offsetResults.size} items from db in $time ms") + // For the case where wo reach the lastForward chunk + updateLoadingStates(filteredEvents) return offsetResults.size } 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 f6fd88f816..6161e8e5fb 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 @@ -224,11 +224,18 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser) } + // Find all the chunks which contain at least one event from the list of eventIds val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds) + Timber.d("Found ${chunks.size} chunks containing at least one of the eventIds") val chunksToDelete = ArrayList() chunks.forEach { if (it != currentChunk) { - currentChunk.merge(roomId, it, direction) + if (direction == PaginationDirection.FORWARDS && it.hasBeenALastForwardChunk()) { + Timber.d("Do not merge $it") + } else { + Timber.d("Merge $it") + currentChunk.merge(roomId, it, direction) + } chunksToDelete.add(it) } } From 697eaec197b338e735b48095c7b5424e9df67908 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 1 May 2020 00:36:01 +0200 Subject: [PATCH 013/191] TI: After jump to unread, newer messages are never loaded (#1008) --- .../matrix/android/common/CommonTestHelper.kt | 24 ++- .../timeline/TimelineForwardPaginationTest.kt | 175 ++++++++++++++++++ 2 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineForwardPaginationTest.kt diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt index ad12fb6bba..2529be9547 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt @@ -28,10 +28,10 @@ import im.vector.matrix.android.api.auth.data.LoginFlowResult import im.vector.matrix.android.api.auth.registration.RegistrationResult 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.events.model.LocalEcho import im.vector.matrix.android.api.session.events.model.toModel 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.send.SendState 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.TimelineSettings @@ -116,7 +116,7 @@ class CommonTestHelper(context: Context) { */ fun sendTextMessage(room: Room, message: String, nbOfMessages: Int): List { val sentEvents = ArrayList(nbOfMessages) - val latch = CountDownLatch(nbOfMessages) + val latch = CountDownLatch(1) val timelineListener = object : Timeline.Listener { override fun onTimelineFailure(throwable: Throwable) { } @@ -127,7 +127,7 @@ class CommonTestHelper(context: Context) { override fun onTimelineUpdated(snapshot: List) { val newMessages = snapshot - .filter { LocalEcho.isLocalEchoId(it.eventId).not() } + .filter { it.root.sendState == SendState.SYNCED } .filter { it.root.getClearType() == EventType.MESSAGE } .filter { it.root.getClearContent().toModel()?.body?.startsWith(message) == true } @@ -292,6 +292,24 @@ class CommonTestHelper(context: Context) { return requestFailure!! } + fun createEventListener(latch: CountDownLatch, predicate: (List) -> Boolean): Timeline.Listener { + return object : Timeline.Listener { + override fun onTimelineFailure(throwable: Throwable) { + // noop + } + + override fun onNewTimelineEvents(eventIds: List) { + // noop + } + + override fun onTimelineUpdated(snapshot: List) { + if (predicate(snapshot)) { + latch.countDown() + } + } + } + } + /** * Await for a latch and ensure the result is true * diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineForwardPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineForwardPaginationTest.kt new file mode 100644 index 0000000000..1540f9f99b --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineForwardPaginationTest.kt @@ -0,0 +1,175 @@ +/* + * 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.session.room.timeline + +import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.room.timeline.Timeline +import im.vector.matrix.android.api.session.room.timeline.TimelineSettings +import im.vector.matrix.android.common.CommonTestHelper +import im.vector.matrix.android.common.CryptoTestHelper +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeTrue +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import timber.log.Timber +import java.util.concurrent.CountDownLatch + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class TimelineForwardPaginationTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + /** + * This test ensure that if we click to permalink, we will be able to go back to the live + */ + @Test + fun forwardPaginationTest() { + val numberOfMessagesToSend = 90 + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false) + + val aliceSession = cryptoTestData.firstSession + val aliceRoomId = cryptoTestData.roomId + + aliceSession.cryptoService().setWarnOnUnknownDevices(false) + + val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! + + // Alice sends X messages + val sentMessages = commonTestHelper.sendTextMessage( + roomFromAlicePOV, + "Message from Alice, long enough to observe the problem, if it is not long enough, there is not always the problem", + numberOfMessagesToSend) + + // Alice clear the cache + commonTestHelper.doSync { + aliceSession.clearCache(it) + } + + aliceSession.startSync(true) + + val aliceTimeline = roomFromAlicePOV.createTimeline(null, TimelineSettings(30)) + aliceTimeline.start() + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Alice timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root.content}") + } + + // Ok, we have the 10 first messages of the initial sync + snapshot.size == 10 + } + + // Open the timeline at last sent message + aliceTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + aliceTimeline.removeAllListeners() + + aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + run { + val lock = CountDownLatch(1) + val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Alice timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root.content}") + } + + // The event is not in db, so it is fetch alone + snapshot.size == 1 + } + + aliceTimeline.addListener(aliceEventsListener) + + // Restart the timeline to the first sent event + aliceTimeline.restartWithEventId(sentMessages.last().eventId) + + commonTestHelper.await(lock) + aliceTimeline.removeAllListeners() + + aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() + aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + } + + run { + val lock = CountDownLatch(1) + val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Alice timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root.content}") + } + + // Alice can see the first event of the room (so Back pagination has worked) + snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE + // 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination + && snapshot.size == 6 + 1 + 50 + } + + aliceTimeline.addListener(aliceEventsListener) + + // Restart the timeline to the first sent event + // We ask to load event backward and forward + aliceTimeline.paginate(Timeline.Direction.BACKWARDS, 50) + aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50) + + commonTestHelper.await(lock) + aliceTimeline.removeAllListeners() + + aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() + aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + + run { + val lock = CountDownLatch(1) + val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Alice timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root.content}") + } + + // 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room) + snapshot.size == 6 + numberOfMessagesToSend + } + + aliceTimeline.addListener(aliceEventsListener) + + // Ask for a forward pagination + aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50) + + commonTestHelper.await(lock) + aliceTimeline.removeAllListeners() + + // The timeline is fully loaded + aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + + aliceTimeline.dispose() + + cryptoTestData.cleanUp(commonTestHelper) + } +} From 92befcde5d747a5f22c63a0e1ecf9af3920b5d1f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 1 May 2020 01:45:06 +0200 Subject: [PATCH 014/191] Add test to cover previous last forward case (passing) --- .../TimelinePreviousLastForwardTest.kt | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelinePreviousLastForwardTest.kt diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelinePreviousLastForwardTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelinePreviousLastForwardTest.kt new file mode 100644 index 0000000000..7ae307ec8b --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelinePreviousLastForwardTest.kt @@ -0,0 +1,236 @@ +/* + * 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.session.room.timeline + +import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.api.extensions.orFalse +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.timeline.Timeline +import im.vector.matrix.android.api.session.room.timeline.TimelineSettings +import im.vector.matrix.android.common.CommonTestHelper +import im.vector.matrix.android.common.CryptoTestHelper +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeTrue +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import timber.log.Timber +import java.util.concurrent.CountDownLatch + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class TimelinePreviousLastForwardTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + /** + * This test ensure that if we have a chunk in the timeline which is due to a sync, and we click to permalink, we will be able to go back to the live + */ + @Test + fun previousLastForwardTest() { + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession!! + val aliceRoomId = cryptoTestData.roomId + + aliceSession.cryptoService().setWarnOnUnknownDevices(false) + bobSession.cryptoService().setWarnOnUnknownDevices(false) + + val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! + val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! + + val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30)) + bobTimeline.start() + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Ok, we have the 8 first messages of the initial sync (room creation and bob join event) + snapshot.size == 8 + } + + bobTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Bob stop to sync + bobSession.stopSync() + + // Alice sends 30 messages + val firstMessageFromAliceId = commonTestHelper.sendTextMessage( + roomFromAlicePOV, + "First messages from Alice", + 30) + .last() + .eventId + + // Bob start to sync + bobSession.startSync(true) + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Ok, we have the 10 last messages from Alice. This will be our future previous lastForward chunk + snapshot.size == 10 + && snapshot.all { it.root.content.toModel()?.body?.startsWith("First messages from Alice").orFalse() } + } + + bobTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Bob stop to sync + bobSession.stopSync() + + // Alice sends again 30 messages + commonTestHelper.sendTextMessage( + roomFromAlicePOV, + "Second messages from Alice", + 30) + + // Bob start to sync + bobSession.startSync(true) + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Ok, we have the 10 last messages from Alice. This will be our future previous lastForward chunk + snapshot.size == 10 + && snapshot.all { it.root.content.toModel()?.body?.startsWith("Second messages from Alice").orFalse() } + } + + bobTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Bob navigate to the first message sent from Alice + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // The event is not in db, so it is fetch + snapshot.size == 1 + } + + bobTimeline.addListener(eventsListener) + + // Restart the timeline to the first sent event, and paginate in both direction + bobTimeline.restartWithEventId(firstMessageFromAliceId) + bobTimeline.paginate(Timeline.Direction.BACKWARDS, 50) + bobTimeline.paginate(Timeline.Direction.FORWARDS, 50) + + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + } + + // Paginate in both direction + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + snapshot.size == 8 + 1 + 35 + } + + bobTimeline.addListener(eventsListener) + + // Paginate in both direction + bobTimeline.paginate(Timeline.Direction.BACKWARDS, 50) + // Ensure the chunk in the middle is included in the next pagination + bobTimeline.paginate(Timeline.Direction.FORWARDS, 35) + + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + + // Bob scroll to the future, till the live + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Bob can see the first event of the room (so Back pagination has worked) + snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE + // 8 for room creation item 60 message from Alice + && snapshot.size == 8 + 60 + } + + bobTimeline.addListener(eventsListener) + + bobTimeline.paginate(Timeline.Direction.FORWARDS, 50) + + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + + bobTimeline.dispose() + + cryptoTestData.cleanUp(commonTestHelper) + } +} From 2b9d3960b384279662d43665d6ba189750eeb8a2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 4 May 2020 15:02:09 +0200 Subject: [PATCH 015/191] Improve tests --- .../timeline/TimelineForwardPaginationTest.kt | 17 +++++++++++++++-- .../timeline/TimelinePreviousLastForwardTest.kt | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineForwardPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineForwardPaginationTest.kt index 1540f9f99b..ae6a9f8d42 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineForwardPaginationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineForwardPaginationTest.kt @@ -17,7 +17,10 @@ package im.vector.matrix.android.session.room.timeline import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.api.extensions.orFalse 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.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.common.CommonTestHelper @@ -57,7 +60,7 @@ class TimelineForwardPaginationTest : InstrumentedTest { // Alice sends X messages val sentMessages = commonTestHelper.sendTextMessage( roomFromAlicePOV, - "Message from Alice, long enough to observe the problem, if it is not long enough, there is not always the problem", + "Message from Alice", numberOfMessagesToSend) // Alice clear the cache @@ -65,11 +68,13 @@ class TimelineForwardPaginationTest : InstrumentedTest { aliceSession.clearCache(it) } + // And restarts the sync aliceSession.startSync(true) val aliceTimeline = roomFromAlicePOV.createTimeline(null, TimelineSettings(30)) aliceTimeline.start() + // Alice sees the 10 last message of the room, and can only navigate BACKWARD run { val lock = CountDownLatch(1) val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> @@ -78,8 +83,9 @@ class TimelineForwardPaginationTest : InstrumentedTest { Timber.w(" event ${it.root.content}") } - // Ok, we have the 10 first messages of the initial sync + // Ok, we have the 10 last messages of the initial sync snapshot.size == 10 + && snapshot.all { it.root.content.toModel()?.body?.startsWith("Message from Alice").orFalse() } } // Open the timeline at last sent message @@ -91,6 +97,8 @@ class TimelineForwardPaginationTest : InstrumentedTest { aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() } + // Alice navigates to the first message of the room, which is not in its database. A GET /context is performed + // Then she can paginate BACKWARD and FORWARD run { val lock = CountDownLatch(1) val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot -> @@ -101,6 +109,7 @@ class TimelineForwardPaginationTest : InstrumentedTest { // The event is not in db, so it is fetch alone snapshot.size == 1 + && snapshot.all { it.root.content.toModel()?.body?.startsWith("Message from Alice").orFalse() } } aliceTimeline.addListener(aliceEventsListener) @@ -115,6 +124,8 @@ class TimelineForwardPaginationTest : InstrumentedTest { aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() } + // Alice paginates BACKWARD and FORWARD of 50 events each + // Then she can only navigate FORWARD run { val lock = CountDownLatch(1) val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot -> @@ -143,6 +154,8 @@ class TimelineForwardPaginationTest : InstrumentedTest { aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() } + // Alice paginates once again FORWARD for 50 events + // All the timeline is retrieved, she cannot paginate anymore in both direction run { val lock = CountDownLatch(1) val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot -> diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelinePreviousLastForwardTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelinePreviousLastForwardTest.kt index 7ae307ec8b..0ca5cfdda0 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelinePreviousLastForwardTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelinePreviousLastForwardTest.kt @@ -70,7 +70,7 @@ class TimelinePreviousLastForwardTest : InstrumentedTest { Timber.w(" event ${it.root}") } - // Ok, we have the 8 first messages of the initial sync (room creation and bob join event) + // Ok, we have the 8 first messages of the initial sync (room creation and bob invite and join events) snapshot.size == 8 } From 53583c691f9e4da12aab493b943df1f2ed801c60 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 4 May 2020 17:15:01 +0200 Subject: [PATCH 016/191] Add some logs --- .../internal/crypto/store/db/RealmCryptoStoreMigration.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt index c1897c76d9..6ff8a49e28 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -196,6 +196,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi } private fun migrateTo3(realm: DynamicRealm) { + Timber.d("Step 2 -> 3") Timber.d("Updating CryptoMetadataEntity table") realm.schema.get("CryptoMetadataEntity") ?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY, String::class.java) @@ -203,6 +204,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi } private fun migrateTo4(realm: DynamicRealm) { + Timber.d("Step 3 -> 4") Timber.d("Updating KeyInfoEntity table") val keyInfoEntities = realm.where("KeyInfoEntity").findAll() try { @@ -217,6 +219,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi } private fun migrateTo5(realm: DynamicRealm) { + Timber.d("Step 4 -> 5") realm.schema.create("MyDeviceLastSeenInfoEntity") .addField(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, String::class.java) .addPrimaryKey(MyDeviceLastSeenInfoEntityFields.DEVICE_ID) From 17ddb5ce43fedebe818e5e15e0fa60063ca65451 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 4 May 2020 18:19:49 +0200 Subject: [PATCH 017/191] if all events are rendered in the timeline (developer mode), render the room creation event. --- .../src/main/res/values/strings.xml | 1 + .../timeline/factory/RoomCreateItemFactory.kt | 24 ++++++++++++------- .../timeline/factory/TimelineItemFactory.kt | 2 +- .../timeline/format/NoticeEventFormatter.kt | 8 +++++++ 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml index 50169fd982..69907e5835 100644 --- a/matrix-sdk-android/src/main/res/values/strings.xml +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -5,6 +5,7 @@ %1$s sent a sticker. %s\'s invitation + %1$s created the room %1$s invited %2$s %1$s invited you %1$s joined the room diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt index bf3b82ab4d..d5471d7f4f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt @@ -21,21 +21,21 @@ 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.api.session.room.timeline.TimelineEvent import im.vector.riotx.R -import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.resources.UserPreferencesProvider import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController -import im.vector.riotx.features.home.room.detail.timeline.item.RoomCreateItem import im.vector.riotx.features.home.room.detail.timeline.item.RoomCreateItem_ import me.gujun.android.span.span import javax.inject.Inject -class RoomCreateItemFactory @Inject constructor(private val colorProvider: ColorProvider, - private val stringProvider: StringProvider) { +class RoomCreateItemFactory @Inject constructor(private val stringProvider: StringProvider, + private val userPreferencesProvider: UserPreferencesProvider, + private val noticeItemFactory: NoticeItemFactory) { - fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): RoomCreateItem? { - val createRoomContent = event.root.getClearContent().toModel() - ?: return null - val predecessorId = createRoomContent.predecessor?.roomId ?: return null + fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { + val createRoomContent = event.root.getClearContent().toModel() ?: return null + val predecessorId = createRoomContent.predecessor?.roomId ?: return defaultRendering(event, callback) val roomLink = PermalinkFactory.createPermalink(predecessorId) ?: return null val text = span { +stringProvider.getString(R.string.room_tombstone_continuation_description) @@ -48,4 +48,12 @@ class RoomCreateItemFactory @Inject constructor(private val colorProvider: Color return RoomCreateItem_() .text(text) } + + private fun defaultRendering(event: TimelineEvent, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { + return if (userPreferencesProvider.shouldShowHiddenEvents()) { + noticeItemFactory.create(event, false, callback) + } else { + null + } + } } 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 7e6c387934..f2ac7018aa 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 @@ -58,7 +58,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.CALL_HANGUP, EventType.CALL_ANSWER, EventType.REACTION, - EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback) + EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback) EventType.STATE_ROOM_ENCRYPTION -> { encryptionItemFactory.create(event, highlight, callback) } 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 39e17b7c35..f29bd72e0a 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.RoomMemberContent import im.vector.matrix.android.api.session.room.model.RoomNameContent 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 import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent @@ -47,6 +48,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active fun format(timelineEvent: TimelineEvent): CharSequence? { return when (val type = timelineEvent.root.getClearType()) { EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root) EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) @@ -98,6 +100,12 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active return "{ \"type\": ${event.getClearType()} }" } + private fun formatRoomCreateEvent(event: Event): CharSequence? { + return event.getClearContent().toModel() + ?.takeIf { it.creator.isNullOrBlank().not() } + ?.let { sp.getString(R.string.notice_room_created, it.creator) } + } + private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? { val content = event.getClearContent().toModel() ?: return null return if (content.name.isNullOrBlank()) { From fcee85a6829557ac4169f04b137718493e6e7a47 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 5 May 2020 00:19:40 +0200 Subject: [PATCH 018/191] Cleanup and doc --- .../riotx/core/extensions/Collections.kt | 20 +++++++++++++++++++ .../timeline/TimelineEventController.kt | 2 +- .../factory/MergedHeaderItemFactory.kt | 14 ++++++------- .../helper/TimelineDisplayableEvents.kt | 8 -------- 4 files changed, 28 insertions(+), 16 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/core/extensions/Collections.kt diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Collections.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Collections.kt new file mode 100644 index 0000000000..af5d5babb6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Collections.kt @@ -0,0 +1,20 @@ +/* + * 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.extensions + +inline fun List.nextOrNull(index: Int) = getOrNull(index + 1) +inline fun List.prevOrNull(index: Int) = getOrNull(index - 1) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index addbfab43c..e074af1da6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -35,6 +35,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.epoxy.LoadingItem_ import im.vector.riotx.core.extensions.localDateTime +import im.vector.riotx.core.extensions.nextOrNull import im.vector.riotx.features.home.room.detail.RoomDetailAction import im.vector.riotx.features.home.room.detail.RoomDetailViewState import im.vector.riotx.features.home.room.detail.UnreadState @@ -45,7 +46,6 @@ import im.vector.riotx.features.home.room.detail.timeline.helper.ReadMarkerVisib import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider -import im.vector.riotx.features.home.room.detail.timeline.helper.nextOrNull import im.vector.riotx.features.home.room.detail.timeline.item.BaseEventItem import im.vector.riotx.features.home.room.detail.timeline.item.BasedMergedItem import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 03c273800a..ec6a975178 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -22,7 +22,7 @@ import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent -import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.extensions.prevOrNull import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider @@ -37,15 +37,15 @@ import im.vector.riotx.features.home.room.detail.timeline.item.MergedRoomCreatio import im.vector.riotx.features.home.room.detail.timeline.item.MergedRoomCreationItem_ import javax.inject.Inject -class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: ActiveSessionHolder, - private val avatarRenderer: AvatarRenderer, +class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer, private val avatarSizeProvider: AvatarSizeProvider) { private val collapsedEventIds = linkedSetOf() private val mergeItemCollapseStates = HashMap() /** - * Note: nextEvent is an older event than event + * @param nextEvent is an older event than event + * @param items all known items, sorted from newer event to oldest event */ fun create(event: TimelineEvent, nextEvent: TimelineEvent?, @@ -127,9 +127,9 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act eventIdToHighlight: String?, requestModelBuild: () -> Unit, callback: TimelineEventController.Callback?): MergedRoomCreationItem_? { - var prevEvent = if (currentPosition > 0) items[currentPosition - 1] else null + var prevEvent = items.prevOrNull(currentPosition) var tmpPos = currentPosition - 1 - val mergedEvents = ArrayList().also { it.add(event) } + val mergedEvents = mutableListOf(event) var hasEncryption = false var encryptionAlgorithm: String? = null while (prevEvent != null && prevEvent.isRoomConfiguration(null)) { @@ -139,7 +139,7 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act } mergedEvents.add(prevEvent) tmpPos-- - prevEvent = if (tmpPos >= 0) items[tmpPos] else null + prevEvent = items.getOrNull(tmpPos) } return if (mergedEvents.size > 2) { var highlighted = false diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index f1106d276e..daf0100bbb 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -106,11 +106,3 @@ fun List.prevSameTypeEvents(index: Int, minSize: Int): List.nextOrNull(index: Int): TimelineEvent? { - return if (index >= size - 1) { - null - } else { - subList(index + 1, this.size).firstOrNull() - } -} From db77e7b8178dcc3aab09b29b10dd9c31cf50e357 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 5 May 2020 00:30:49 +0200 Subject: [PATCH 019/191] Create a fun --- .../factory/MergedHeaderItemFactory.kt | 111 ++++++++++-------- 1 file changed, 60 insertions(+), 51 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index ec6a975178..9529693e6b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -64,60 +64,69 @@ class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: Av } else if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) { null } else { - val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2) - if (prevSameTypeEvents.isEmpty()) { - null - } else { - var highlighted = false - val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed() - val mergedData = ArrayList(mergedEvents.size) - mergedEvents.forEach { mergedEvent -> - if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) { - highlighted = true - } - val senderAvatar = mergedEvent.senderAvatar - val senderName = mergedEvent.getDisambiguatedDisplayName() - val data = BasedMergedItem.Data( - userId = mergedEvent.root.senderId ?: "", - avatarUrl = senderAvatar, - memberName = senderName, - localId = mergedEvent.localId, - eventId = mergedEvent.root.eventId ?: "" - ) - mergedData.add(data) + buildMembershipEventsMergedSummary(currentPosition, items, event, eventIdToHighlight, requestModelBuild, callback) + } + } + + private fun buildMembershipEventsMergedSummary(currentPosition: Int, + items: List, + event: TimelineEvent, + eventIdToHighlight: String?, + requestModelBuild: () -> Unit, + callback: TimelineEventController.Callback?): MergedMembershipEventsItem_? { + val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2) + return if (prevSameTypeEvents.isEmpty()) { + null + } else { + var highlighted = false + val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed() + val mergedData = ArrayList(mergedEvents.size) + mergedEvents.forEach { mergedEvent -> + if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) { + highlighted = true } - val mergedEventIds = mergedEvents.map { it.localId } - // We try to find if one of the item id were used as mergeItemCollapseStates key - // => handle case where paginating from mergeable events and we get more - val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull() - val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) - ?: true - val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState } - if (isCollapsed) { - collapsedEventIds.addAll(mergedEventIds) - } else { - collapsedEventIds.removeAll(mergedEventIds) - } - val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() } - val attributes = MergedMembershipEventsItem.Attributes( - isCollapsed = isCollapsed, - mergeData = mergedData, - avatarRenderer = avatarRenderer, - onCollapsedStateChanged = { - mergeItemCollapseStates[event.localId] = it - requestModelBuild() - }, - readReceiptsCallback = callback + val senderAvatar = mergedEvent.senderAvatar + val senderName = mergedEvent.getDisambiguatedDisplayName() + val data = BasedMergedItem.Data( + userId = mergedEvent.root.senderId ?: "", + avatarUrl = senderAvatar, + memberName = senderName, + localId = mergedEvent.localId, + eventId = mergedEvent.root.eventId ?: "" ) - MergedMembershipEventsItem_() - .id(mergeId) - .leftGuideline(avatarSizeProvider.leftGuideline) - .highlighted(isCollapsed && highlighted) - .attributes(attributes) - .also { - it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents)) - } + mergedData.add(data) } + val mergedEventIds = mergedEvents.map { it.localId } + // We try to find if one of the item id were used as mergeItemCollapseStates key + // => handle case where paginating from mergeable events and we get more + val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull() + val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) + ?: true + val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState } + if (isCollapsed) { + collapsedEventIds.addAll(mergedEventIds) + } else { + collapsedEventIds.removeAll(mergedEventIds) + } + val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() } + val attributes = MergedMembershipEventsItem.Attributes( + isCollapsed = isCollapsed, + mergeData = mergedData, + avatarRenderer = avatarRenderer, + onCollapsedStateChanged = { + mergeItemCollapseStates[event.localId] = it + requestModelBuild() + }, + readReceiptsCallback = callback + ) + MergedMembershipEventsItem_() + .id(mergeId) + .leftGuideline(avatarSizeProvider.leftGuideline) + .highlighted(isCollapsed && highlighted) + .attributes(attributes) + .also { + it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents)) + } } } From ffeae7ec83ffbed426ef3982e0b84501f7528ece Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 5 May 2020 02:38:30 +0200 Subject: [PATCH 020/191] Fix timeline navigation when opening an event in a previous lastForward chunk. In this case, we do not have a nextToken, but there are more event to load. So we perform a GET /context on the last known event. Not sure it is correct to do that though... --- .../TimelineBackToPreviousLastForwardTest.kt | 206 ++++++++++++++++++ .../session/room/timeline/DefaultTimeline.kt | 6 + .../room/timeline/TokenChunkEventPersistor.kt | 16 +- 3 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt new file mode 100644 index 0000000000..d55087a8c7 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt @@ -0,0 +1,206 @@ +/* + * 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.session.room.timeline + +import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.api.extensions.orFalse +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.timeline.Timeline +import im.vector.matrix.android.api.session.room.timeline.TimelineSettings +import im.vector.matrix.android.common.CommonTestHelper +import im.vector.matrix.android.common.CryptoTestHelper +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeTrue +import org.junit.Assert.assertTrue +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import timber.log.Timber +import java.util.concurrent.CountDownLatch + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class TimelineBackToPreviousLastForwardTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + /** + * This test ensure that if we have a chunk in the timeline which is due to a sync, and we click to permalink of an + * even contained in a previous lastForward chunk, we will be able to go back to the live + */ + @Test + fun backToPreviousLastForwardTest() { + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession!! + val aliceRoomId = cryptoTestData.roomId + + aliceSession.cryptoService().setWarnOnUnknownDevices(false) + bobSession.cryptoService().setWarnOnUnknownDevices(false) + + val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! + val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! + + val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30)) + bobTimeline.start() + + var roomCreationEventId: String? = null + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + roomCreationEventId = snapshot.lastOrNull()?.root?.eventId + // Ok, we have the 8 first messages of the initial sync (room creation and bob join event) + snapshot.size == 8 + } + + bobTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Bob stop to sync + bobSession.stopSync() + + // Alice sends 30 messages + commonTestHelper.sendTextMessage( + roomFromAlicePOV, + "First messages from Alice", + 30) + + // Bob start to sync + bobSession.startSync(true) + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Ok, we have the 10 last messages from Alice. + snapshot.size == 10 + && snapshot.all { it.root.content.toModel()?.body?.startsWith("First messages from Alice").orFalse() } + } + + bobTimeline.addListener(eventsListener) + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + } + + // Bob navigate to the first event (room creation event), so inside the previous last forward chunk + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // The event is in db, so it is fetch and auto pagination occurs, half of the number of events we have for this chunk (?) + snapshot.size == 4 + } + + bobTimeline.addListener(eventsListener) + + // Restart the timeline to the first sent event, which is already in the database, so pagination should start automatically + assertTrue(roomFromBobPOV.getTimeLineEvent(roomCreationEventId!!) != null) + + bobTimeline.restartWithEventId(roomCreationEventId) + + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + + // Bob scroll to the future + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Bob can see the first event of the room (so Back pagination has worked) + snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE + // 8 for room creation item, and 30 for the forward pagination + && snapshot.size == 8 + } + + bobTimeline.addListener(eventsListener) + + bobTimeline.paginate(Timeline.Direction.FORWARDS, 50) + + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + + // Do it again, now we should have a next token, so we can paginate FORWARD + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + Timber.e("Bob timeline updated: with ${snapshot.size} events:") + snapshot.forEach { + Timber.w(" event ${it.root}") + } + + // Bob can see the first event of the room (so Back pagination has worked) + snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE + // 8 for room creation item, and 30 for the forward pagination + && snapshot.size == 8 + 30 + } + + bobTimeline.addListener(eventsListener) + + bobTimeline.paginate(Timeline.Direction.FORWARDS, 50) + + commonTestHelper.await(lock) + bobTimeline.removeAllListeners() + + bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() + bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + } + + bobTimeline.dispose() + + cryptoTestData.cleanUp(commonTestHelper) + } +} 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 8cfd498cc8..ba46b10228 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 @@ -521,6 +521,12 @@ internal class DefaultTimeline( private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { val token = getTokenLive(direction) if (token == null) { + val currentChunk = getLiveChunk() + if (direction == Timeline.Direction.FORWARDS && currentChunk?.hasBeenALastForwardChunk() == true) { + // 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 + currentChunk.timelineEvents.lastOrNull()?.eventId?.let { fetchEvent(it) } + } updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } return } 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 6161e8e5fb..3f5e5ccf06 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 @@ -231,12 +231,20 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy chunks.forEach { if (it != currentChunk) { if (direction == PaginationDirection.FORWARDS && it.hasBeenALastForwardChunk()) { - Timber.d("Do not merge $it") + // Maybe it was a trick to get a nextToken + if (receivedChunk.events.size == 1) { + Timber.d("Receiving a new nextToken") + it.nextToken = receivedChunk.end + chunksToDelete.add(currentChunk) + } else { + Timber.d("Do not merge $it") + chunksToDelete.add(it) + } } else { Timber.d("Merge $it") currentChunk.merge(roomId, it, direction) + chunksToDelete.add(it) } - chunksToDelete.add(it) } } val shouldUpdateSummary = chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS @@ -253,6 +261,8 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy ) roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent } - RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk) + if (currentChunk.isValid) { + RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk) + } } } From 48e58967b20b6a4e1990b156aaa48ab20bfe3f54 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 May 2020 15:48:15 +0200 Subject: [PATCH 021/191] Version++ --- CHANGES.md | 24 ++++++++++++++++++++++++ vector/build.gradle | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 869b034f45..099e4cf5f4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,27 @@ +Changes in RiotX 0.21.0 (2020-XX-XX) +=================================================== + +Features ✨: + - + +Improvements 🙌: + - + +Bugfix 🐛: + - + +Translations 🗣: + - + +SDK API changes ⚠️: + - + +Build 🧱: + - + +Other changes: + - + Changes in RiotX 0.20.0 (2020-05-15) =================================================== diff --git a/vector/build.gradle b/vector/build.gradle index 459b297fd6..74fc96a425 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -15,7 +15,7 @@ androidExtensions { } ext.versionMajor = 0 -ext.versionMinor = 20 +ext.versionMinor = 21 ext.versionPatch = 0 static def getGitTimestamp() { From 5fa247a0c5c02e34ea2caf6b68e458b115eb21ce Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 May 2020 15:50:15 +0200 Subject: [PATCH 022/191] Remove temporary tool and strings_riotX.xml temporary files --- .../src/main/res/values/strings_RiotX.xml | 32 ----- tools/import_from_riot.sh | 122 ------------------ vector/src/main/res/values/strings.xml | 8 ++ vector/src/main/res/values/strings_riotX.xml | 68 ---------- 4 files changed, 8 insertions(+), 222 deletions(-) delete mode 100644 matrix-sdk-android/src/main/res/values/strings_RiotX.xml delete mode 100755 tools/import_from_riot.sh delete mode 100644 vector/src/main/res/values/strings_riotX.xml diff --git a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml deleted file mode 100644 index 6eb46fd7df..0000000000 --- a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - diff --git a/tools/import_from_riot.sh b/tools/import_from_riot.sh deleted file mode 100755 index a2b68a347c..0000000000 --- a/tools/import_from_riot.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env bash - -# -# Copyright 2018 New Vector Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -# Exit on any error -set -e - -echo -echo "Copy strings to SDK" - -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values/strings.xml ./matrix-sdk-android/src/main/res/values/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-ar/strings.xml ./matrix-sdk-android/src/main/res/values-ar/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-az/strings.xml ./matrix-sdk-android/src/main/res/values-az/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-bg/strings.xml ./matrix-sdk-android/src/main/res/values-bg/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-bs/strings.xml ./matrix-sdk-android/src/main/res/values-bs/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-ca/strings.xml ./matrix-sdk-android/src/main/res/values-ca/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-cs/strings.xml ./matrix-sdk-android/src/main/res/values-cs/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-da/strings.xml ./matrix-sdk-android/src/main/res/values-da/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-de/strings.xml ./matrix-sdk-android/src/main/res/values-de/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-el/strings.xml ./matrix-sdk-android/src/main/res/values-el/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-eo/strings.xml ./matrix-sdk-android/src/main/res/values-eo/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-en-rGB/strings.xml ./matrix-sdk-android/src/main/res/values-en-rGB/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-es/strings.xml ./matrix-sdk-android/src/main/res/values-es/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-es-rMX/strings.xml ./matrix-sdk-android/src/main/res/values-es-rMX/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-eu/strings.xml ./matrix-sdk-android/src/main/res/values-eu/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-fa/strings.xml ./matrix-sdk-android/src/main/res/values-fa/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-fi/strings.xml ./matrix-sdk-android/src/main/res/values-fi/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-fr/strings.xml ./matrix-sdk-android/src/main/res/values-fr/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-gl/strings.xml ./matrix-sdk-android/src/main/res/values-gl/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-hu/strings.xml ./matrix-sdk-android/src/main/res/values-hu/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-id/strings.xml ./matrix-sdk-android/src/main/res/values-id/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-in/strings.xml ./matrix-sdk-android/src/main/res/values-in/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-is/strings.xml ./matrix-sdk-android/src/main/res/values-is/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-it/strings.xml ./matrix-sdk-android/src/main/res/values-it/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-ja/strings.xml ./matrix-sdk-android/src/main/res/values-ja/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-ko/strings.xml ./matrix-sdk-android/src/main/res/values-ko/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-lv/strings.xml ./matrix-sdk-android/src/main/res/values-lv/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-nl/strings.xml ./matrix-sdk-android/src/main/res/values-nl/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-nn/strings.xml ./matrix-sdk-android/src/main/res/values-nn/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-pl/strings.xml ./matrix-sdk-android/src/main/res/values-pl/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-pt/strings.xml ./matrix-sdk-android/src/main/res/values-pt/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-pt-rBR/strings.xml ./matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-ru/strings.xml ./matrix-sdk-android/src/main/res/values-ru/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-sk/strings.xml ./matrix-sdk-android/src/main/res/values-sk/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-sq/strings.xml ./matrix-sdk-android/src/main/res/values-sq/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-te/strings.xml ./matrix-sdk-android/src/main/res/values-te/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-th/strings.xml ./matrix-sdk-android/src/main/res/values-th/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-uk/strings.xml ./matrix-sdk-android/src/main/res/values-uk/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-vls/strings.xml ./matrix-sdk-android/src/main/res/values-vls/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-zh-rCN/strings.xml ./matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-zh-rTW/strings.xml ./matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml - -echo -echo "Copy strings to RiotX" - -cp ../riot-android/vector/src/main/res/values/strings.xml ./vector/src/main/res/values/strings.xml -cp ../riot-android/vector/src/main/res/values-ar/strings.xml ./vector/src/main/res/values-ar/strings.xml -cp ../riot-android/vector/src/main/res/values-az/strings.xml ./vector/src/main/res/values-az/strings.xml -cp ../riot-android/vector/src/main/res/values-b+sr+Latn/strings.xml ./vector/src/main/res/values-b+sr+Latn/strings.xml -cp ../riot-android/vector/src/main/res/values-bg/strings.xml ./vector/src/main/res/values-bg/strings.xml -cp ../riot-android/vector/src/main/res/values-bn-rIN/strings.xml ./vector/src/main/res/values-bn-rIN/strings.xml -cp ../riot-android/vector/src/main/res/values-bs/strings.xml ./vector/src/main/res/values-bs/strings.xml -cp ../riot-android/vector/src/main/res/values-ca/strings.xml ./vector/src/main/res/values-ca/strings.xml -cp ../riot-android/vector/src/main/res/values-cs/strings.xml ./vector/src/main/res/values-cs/strings.xml -cp ../riot-android/vector/src/main/res/values-cy/strings.xml ./vector/src/main/res/values-cy/strings.xml -cp ../riot-android/vector/src/main/res/values-da/strings.xml ./vector/src/main/res/values-da/strings.xml -cp ../riot-android/vector/src/main/res/values-de/strings.xml ./vector/src/main/res/values-de/strings.xml -cp ../riot-android/vector/src/main/res/values-el/strings.xml ./vector/src/main/res/values-el/strings.xml -cp ../riot-android/vector/src/main/res/values-eo/strings.xml ./vector/src/main/res/values-eo/strings.xml -cp ../riot-android/vector/src/main/res/values-es/strings.xml ./vector/src/main/res/values-es/strings.xml -cp ../riot-android/vector/src/main/res/values-es-rMX/strings.xml ./vector/src/main/res/values-es-rMX/strings.xml -cp ../riot-android/vector/src/main/res/values-eu/strings.xml ./vector/src/main/res/values-eu/strings.xml -cp ../riot-android/vector/src/main/res/values-fa/strings.xml ./vector/src/main/res/values-fa/strings.xml -cp ../riot-android/vector/src/main/res/values-fi/strings.xml ./vector/src/main/res/values-fi/strings.xml -cp ../riot-android/vector/src/main/res/values-fy/strings.xml ./vector/src/main/res/values-fy/strings.xml -cp ../riot-android/vector/src/main/res/values-fr/strings.xml ./vector/src/main/res/values-fr/strings.xml -cp ../riot-android/vector/src/main/res/values-fr-rCA/strings.xml ./vector/src/main/res/values-fr-rCA/strings.xml -cp ../riot-android/vector/src/main/res/values-gl/strings.xml ./vector/src/main/res/values-gl/strings.xml -cp ../riot-android/vector/src/main/res/values-hu/strings.xml ./vector/src/main/res/values-hu/strings.xml -cp ../riot-android/vector/src/main/res/values-id/strings.xml ./vector/src/main/res/values-id/strings.xml -cp ../riot-android/vector/src/main/res/values-in/strings.xml ./vector/src/main/res/values-in/strings.xml -cp ../riot-android/vector/src/main/res/values-is/strings.xml ./vector/src/main/res/values-is/strings.xml -cp ../riot-android/vector/src/main/res/values-it/strings.xml ./vector/src/main/res/values-it/strings.xml -cp ../riot-android/vector/src/main/res/values-ja/strings.xml ./vector/src/main/res/values-ja/strings.xml -cp ../riot-android/vector/src/main/res/values-ko/strings.xml ./vector/src/main/res/values-ko/strings.xml -cp ../riot-android/vector/src/main/res/values-lv/strings.xml ./vector/src/main/res/values-lv/strings.xml -cp ../riot-android/vector/src/main/res/values-nb-rNO/strings.xml ./vector/src/main/res/values-nb-rNO/strings.xml -cp ../riot-android/vector/src/main/res/values-nl/strings.xml ./vector/src/main/res/values-nl/strings.xml -cp ../riot-android/vector/src/main/res/values-nn/strings.xml ./vector/src/main/res/values-nn/strings.xml -cp ../riot-android/vector/src/main/res/values-pl/strings.xml ./vector/src/main/res/values-pl/strings.xml -cp ../riot-android/vector/src/main/res/values-pt/strings.xml ./vector/src/main/res/values-pt/strings.xml -cp ../riot-android/vector/src/main/res/values-pt-rBR/strings.xml ./vector/src/main/res/values-pt-rBR/strings.xml -cp ../riot-android/vector/src/main/res/values-ro/strings.xml ./vector/src/main/res/values-ro/strings.xml -cp ../riot-android/vector/src/main/res/values-ru/strings.xml ./vector/src/main/res/values-ru/strings.xml -cp ../riot-android/vector/src/main/res/values-sk/strings.xml ./vector/src/main/res/values-sk/strings.xml -cp ../riot-android/vector/src/main/res/values-sq/strings.xml ./vector/src/main/res/values-sq/strings.xml -cp ../riot-android/vector/src/main/res/values-sr/strings.xml ./vector/src/main/res/values-sr/strings.xml -cp ../riot-android/vector/src/main/res/values-te/strings.xml ./vector/src/main/res/values-te/strings.xml -cp ../riot-android/vector/src/main/res/values-th/strings.xml ./vector/src/main/res/values-th/strings.xml -cp ../riot-android/vector/src/main/res/values-tlh/strings.xml ./vector/src/main/res/values-tlh/strings.xml -cp ../riot-android/vector/src/main/res/values-tr/strings.xml ./vector/src/main/res/values-tr/strings.xml -cp ../riot-android/vector/src/main/res/values-uk/strings.xml ./vector/src/main/res/values-uk/strings.xml -cp ../riot-android/vector/src/main/res/values-vls/strings.xml ./vector/src/main/res/values-vls/strings.xml -cp ../riot-android/vector/src/main/res/values-zh-rCN/strings.xml ./vector/src/main/res/values-zh-rCN/strings.xml -cp ../riot-android/vector/src/main/res/values-zh-rTW/strings.xml ./vector/src/main/res/values-zh-rTW/strings.xml - -echo -echo "Success!" diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index ff821f5b95..89cbc9a856 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1939,6 +1939,14 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Too many requests have been sent. You can retry in %1$d seconds… + Alternatively, if you already have an account and you know your Matrix identifier and your password, you can use this method: + Sign in with my Matrix identifier + Sign in + Enter your identifier and your password + User identifier + This is not a valid user identifier. Expected format: \'@user:homeserver.org\' + Unable to find a valid homeserver. Please check your identifier + Seen by You’re signed out diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml deleted file mode 100644 index dd1043819d..0000000000 --- a/vector/src/main/res/values/strings_riotX.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Alternatively, if you already have an account and you know your Matrix identifier and your password, you can use this method: - Sign in with my Matrix identifier - Sign in - Enter your identifier and your password - User identifier - This is not a valid user identifier. Expected format: \'@user:homeserver.org\' - Unable to find a valid homeserver. Please check your identifier - - From 458e3ee5e8b9b6a56371f0bd373a56000bb119ec Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 15 May 2020 20:18:07 +0200 Subject: [PATCH 023/191] Timeline: fetch next token with the help of getContext when required --- .../internal/session/room/RoomModule.kt | 5 + .../session/room/timeline/DefaultTimeline.kt | 100 +++++++++++------- .../room/timeline/DefaultTimelineService.kt | 4 +- .../timeline/FetchNextTokenAndPaginateTask.kt | 66 ++++++++++++ .../room/timeline/TokenChunkEventPersistor.kt | 18 +--- 5 files changed, 138 insertions(+), 55 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/FetchNextTokenAndPaginateTask.kt 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 6b003b5ba2..b0a60480e3 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 @@ -56,8 +56,10 @@ import im.vector.matrix.android.internal.session.room.reporting.DefaultReportCon import im.vector.matrix.android.internal.session.room.reporting.ReportContentTask import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask import im.vector.matrix.android.internal.session.room.state.SendStateTask +import im.vector.matrix.android.internal.session.room.timeline.DefaultFetchNextTokenAndPaginateTask 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.GetContextOfEventTask import im.vector.matrix.android.internal.session.room.timeline.PaginationTask import im.vector.matrix.android.internal.session.room.typing.DefaultSendTypingTask @@ -143,6 +145,9 @@ internal abstract class RoomModule { @Binds abstract fun bindPaginationTask(task: DefaultPaginationTask): PaginationTask + @Binds + abstract fun bindFetchNextTokenAndPaginateTask(task: DefaultFetchNextTokenAndPaginateTask): FetchNextTokenAndPaginateTask + @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 ba46b10228..76cdf8c485 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 @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.session.room.timeline import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.extensions.orFalse import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.toModel @@ -71,6 +72,7 @@ internal class DefaultTimeline( private val realmConfiguration: RealmConfiguration, private val taskExecutor: TaskExecutor, private val contextOfEventTask: GetContextOfEventTask, + private val fetchNextTokenAndPaginateTask: FetchNextTokenAndPaginateTask, private val paginationTask: PaginationTask, private val timelineEventMapper: TimelineEventMapper, private val settings: TimelineSettings, @@ -519,50 +521,44 @@ internal class DefaultTimeline( * This has to be called on TimelineThread as it accesses realm live results */ private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { - val token = getTokenLive(direction) + val currentChunk = getLiveChunk() + val token = if (direction == Timeline.Direction.BACKWARDS) currentChunk?.prevToken else currentChunk?.nextToken if (token == null) { - val currentChunk = getLiveChunk() - if (direction == Timeline.Direction.FORWARDS && currentChunk?.hasBeenALastForwardChunk() == true) { + 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 - currentChunk.timelineEvents.lastOrNull()?.eventId?.let { fetchEvent(it) } - } - updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } - return - } - val params = PaginationTask.Params(roomId = roomId, - from = token, - direction = direction.toPaginationDirection(), - limit = limit) - - Timber.v("Should fetch $limit items $direction") - cancelableBag += paginationTask - .configureWith(params) { - this.callback = object : MatrixCallback { - override fun onSuccess(data: TokenChunkEventPersistor.Result) { - when (data) { - TokenChunkEventPersistor.Result.SUCCESS -> { - Timber.v("Success fetching $limit items $direction from pagination request") - } - TokenChunkEventPersistor.Result.REACHED_END -> { - postSnapshot() - } - TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> - // Database won't be updated, so we force pagination request - BACKGROUND_HANDLER.post { - executePaginationTask(direction, limit) - } + val lastKnownEventId = nonFilteredEvents.firstOrNull()?.eventId + if (lastKnownEventId == null) { + updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } + } else { + val params = FetchNextTokenAndPaginateTask.Params( + roomId = roomId, + limit = limit, + lastKnownEventId = lastKnownEventId + ) + cancelableBag += fetchNextTokenAndPaginateTask + .configureWith(params) { + this.callback = createPaginationCallback(limit, direction) } - } - - override fun onFailure(failure: Throwable) { - updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } - postSnapshot() - Timber.v("Failure fetching $limit items $direction from pagination request") - } - } + .executeBy(taskExecutor) } - .executeBy(taskExecutor) + } else { + updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } + } + } else { + val params = PaginationTask.Params( + roomId = roomId, + from = token, + direction = direction.toPaginationDirection(), + limit = limit + ) + Timber.v("Should fetch $limit items $direction") + cancelableBag += paginationTask + .configureWith(params) { + this.callback = createPaginationCallback(limit, direction) + } + .executeBy(taskExecutor) + } } // For debug purpose only @@ -743,6 +739,32 @@ internal class DefaultTimeline( forwardsState.set(State()) } + private fun createPaginationCallback(limit: Int, direction: Timeline.Direction): MatrixCallback { + return object : MatrixCallback { + override fun onSuccess(data: TokenChunkEventPersistor.Result) { + when (data) { + TokenChunkEventPersistor.Result.SUCCESS -> { + Timber.v("Success fetching $limit items $direction from pagination request") + } + TokenChunkEventPersistor.Result.REACHED_END -> { + postSnapshot() + } + TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> + // Database won't be updated, so we force pagination request + BACKGROUND_HANDLER.post { + executePaginationTask(direction, limit) + } + } + } + + override fun onFailure(failure: Throwable) { + updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } + postSnapshot() + Timber.v("Failure fetching $limit items $direction from pagination request") + } + } + } + // Extension methods *************************************************************************** private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { 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 c02bb915ef..ffa282d088 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 @@ -42,6 +42,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 timelineEventMapper: TimelineEventMapper, private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper ) : TimelineService { @@ -63,7 +64,8 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv settings = settings, hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings), eventBus = eventBus, - eventDecryptor = eventDecryptor + eventDecryptor = eventDecryptor, + fetchNextTokenAndPaginateTask = fetchNextTokenAndPaginateTask ) } 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/FetchNextTokenAndPaginateTask.kt new file mode 100644 index 0000000000..1189e627c4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/FetchNextTokenAndPaginateTask.kt @@ -0,0 +1,66 @@ +/* + * 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.timeline + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.query.findIncludingEvent +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.filter.FilterRepository +import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.awaitTransaction +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface FetchNextTokenAndPaginateTask : Task { + + data class Params( + val roomId: String, + val lastKnownEventId: String, + val limit: Int + ) +} + +internal class DefaultFetchNextTokenAndPaginateTask @Inject constructor( + private val roomAPI: RoomAPI, + private val monarchy: Monarchy, + private val filterRepository: FilterRepository, + private val paginationTask: PaginationTask, + private val eventBus: EventBus +) : FetchNextTokenAndPaginateTask { + + override suspend fun execute(params: FetchNextTokenAndPaginateTask.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") + } + monarchy.awaitTransaction { + ChunkEntity.findIncludingEvent(it, params.lastKnownEventId)?.nextToken = response.end + } + val paginationParams = PaginationTask.Params( + roomId = params.roomId, + from = response.end, + direction = PaginationDirection.FORWARDS, + limit = params.limit + ) + return paginationTask.execute(paginationParams) + } +} 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 3f5e5ccf06..f7411b3bf1 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 @@ -230,21 +230,9 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy val chunksToDelete = ArrayList() chunks.forEach { if (it != currentChunk) { - if (direction == PaginationDirection.FORWARDS && it.hasBeenALastForwardChunk()) { - // Maybe it was a trick to get a nextToken - if (receivedChunk.events.size == 1) { - Timber.d("Receiving a new nextToken") - it.nextToken = receivedChunk.end - chunksToDelete.add(currentChunk) - } else { - Timber.d("Do not merge $it") - chunksToDelete.add(it) - } - } else { - Timber.d("Merge $it") - currentChunk.merge(roomId, it, direction) - chunksToDelete.add(it) - } + Timber.d("Merge $it") + currentChunk.merge(roomId, it, direction) + chunksToDelete.add(it) } } val shouldUpdateSummary = chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS From f361fd73557416987d7e44d863c37f09548371f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 15 May 2020 21:33:36 +0000 Subject: [PATCH 024/191] Added translation using Weblate (Croatian) --- vector/src/main/res/values-hr/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 vector/src/main/res/values-hr/strings.xml diff --git a/vector/src/main/res/values-hr/strings.xml b/vector/src/main/res/values-hr/strings.xml new file mode 100644 index 0000000000..a6b3daec93 --- /dev/null +++ b/vector/src/main/res/values-hr/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From e84fd408bebf968ba1f3a659cadda229146eb3a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 15 May 2020 21:36:12 +0000 Subject: [PATCH 025/191] Translated using Weblate (Croatian) Currently translated at 0.5% (8 of 1682 strings) Translation: Riot Android/RiotX application Translate-URL: https://translate.riot.im/projects/riot-android/riotx-application/hr/ --- vector/src/main/res/values-hr/strings.xml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/vector/src/main/res/values-hr/strings.xml b/vector/src/main/res/values-hr/strings.xml index a6b3daec93..a3e6c53e5b 100644 --- a/vector/src/main/res/values-hr/strings.xml +++ b/vector/src/main/res/values-hr/strings.xml @@ -1,2 +1,12 @@ - - \ No newline at end of file + +HR + Latn + + Svijetla tema + Tamna tema + Crna tema + Tema Status.im-a + + Inicijalizacija servisa + Sinkronizacija u tijeku… + From 3fe2f2876a2edd946138b1e494793ea7d5e6c9ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Fri, 15 May 2020 17:39:14 +0000 Subject: [PATCH 026/191] Translated using Weblate (Estonian) Currently translated at 1.6% (27 of 1682 strings) Translation: Riot Android/RiotX application Translate-URL: https://translate.riot.im/projects/riot-android/riotx-application/et/ --- vector/src/main/res/values-et/strings.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vector/src/main/res/values-et/strings.xml b/vector/src/main/res/values-et/strings.xml index 3ec6bf684a..549b1a1f7a 100644 --- a/vector/src/main/res/values-et/strings.xml +++ b/vector/src/main/res/values-et/strings.xml @@ -30,4 +30,6 @@ Võtmete varundus pole veel valmis, oota natuke… Krüptitud sõnum - +Kui sa logid nüüd välja, siis sa kaotad ligipääsu kõikidele krüptitud sõnumitele + Parasjagu varundan võtmeid. Kui sa logid nüüd välja, siis sa kaotad ligipääsu kõikidele oma krüptitud sõnumitele. + From 860921217dccdf91114b84318caafb4803119a6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20C?= Date: Sat, 16 May 2020 06:53:04 +0000 Subject: [PATCH 027/191] Translated using Weblate (French) Currently translated at 100.0% (1682 of 1682 strings) Translation: Riot Android/RiotX application Translate-URL: https://translate.riot.im/projects/riot-android/riotx-application/fr/ --- vector/src/main/res/values-fr/strings.xml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml index 13f3808328..ddd2c3c263 100644 --- a/vector/src/main/res/values-fr/strings.xml +++ b/vector/src/main/res/values-fr/strings.xml @@ -589,7 +589,7 @@ Veuillez noter que cette action redémarrera l’application et pourra prendre u Le chiffrement est activé sur ce salon. Le chiffrement est désactivé sur ce salon. Activer le chiffrement -\n(attention : ne peut pas être désactivé ensuite !) +\n(attention : ne peut plus être désactivé par la suite !) %s a essayé de charger un point précis dans l’historique du salon mais ne l’a pas trouvé. @@ -2393,4 +2393,19 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq Vérifiez toutes les sessions pour vous assurer que votre compte et vos messages sont en sécurité Vérifiez la nouvelle connexion accédant à votre compte : %1$s +%1$s : %2$s + %1$s : %2$s %3$s + + Ajouter des membres + INVITER + Invitation des utilisateurs… + Inviter des utilisateurs + Invitation envoyée à %1$s + Invitations envoyées à %1$s et %2$s + + Invitations envoyées à %1$s et un·e autre + Invitations envoyées à %1$s et %2$d autres + + Nous n’avons pas pu inviter les utilisateurs. Vérifiez les utilisateurs que vous souhaitez inviter et réessayez. + From c105d820275a8681d3a5a052b878f5a1dd775886 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Fri, 15 May 2020 15:56:56 +0000 Subject: [PATCH 028/191] Translated using Weblate (Hungarian) Currently translated at 100.0% (1682 of 1682 strings) Translation: Riot Android/RiotX application Translate-URL: https://translate.riot.im/projects/riot-android/riotx-application/hu/ --- vector/src/main/res/values-hu/strings.xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/vector/src/main/res/values-hu/strings.xml b/vector/src/main/res/values-hu/strings.xml index acdb4bf5b2..538af2f1a7 100644 --- a/vector/src/main/res/values-hu/strings.xml +++ b/vector/src/main/res/values-hu/strings.xml @@ -2388,4 +2388,19 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró \n \nFolytatod\? +%1$s: %2$s + %1$s: %2$s %3$s + + Tag hozzáadása + MEGHÍV + Felhasználók meghívása… + Felhasználók meghívása + Meghívó elküldve neki: %1$s + Meghívó elküldve nekik: %1$s, %2$s + + Meghívó elküldve neki: %1$s és még egy valakinek + Meghívó elküldve neki: %1$s és még %2$d helyre + + Felhasználókat nem tudtuk meghívni. Ellenőrizd azokat a felhasználókat akiket meg szeretnél hívni és próbáld újra. + From 18e804d17420b77520841bf900bee48d3e3e22f0 Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Fri, 15 May 2020 21:49:25 +0000 Subject: [PATCH 029/191] Translated using Weblate (Swedish) Currently translated at 14.3% (241 of 1682 strings) Translation: Riot Android/RiotX application Translate-URL: https://translate.riot.im/projects/riot-android/riotx-application/sv/ --- vector/src/main/res/values-sv/strings.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vector/src/main/res/values-sv/strings.xml b/vector/src/main/res/values-sv/strings.xml index 40e2df9870..981109b660 100644 --- a/vector/src/main/res/values-sv/strings.xml +++ b/vector/src/main/res/values-sv/strings.xml @@ -279,4 +279,7 @@ Felformaterad JSON Innehöll inte giltig JSON För många förfrågningar har skickats - +Det här användarnamnet är upptaget + E-postlänken har inte klickats på än + + From 3b62f50f7b159226ce6f2eca02af14e3bc20c788 Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Sat, 16 May 2020 07:44:05 +0000 Subject: [PATCH 030/191] Translated using Weblate (Swedish) Currently translated at 14.4% (242 of 1682 strings) Translation: Riot Android/RiotX application Translate-URL: https://translate.riot.im/projects/riot-android/riotx-application/sv/ --- vector/src/main/res/values-sv/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vector/src/main/res/values-sv/strings.xml b/vector/src/main/res/values-sv/strings.xml index 981109b660..daeb20e67d 100644 --- a/vector/src/main/res/values-sv/strings.xml +++ b/vector/src/main/res/values-sv/strings.xml @@ -282,4 +282,8 @@ Det här användarnamnet är upptaget E-postlänken har inte klickats på än + Du behöver logga in igen för att generera nycklar för ände-till-ände-kryptering för den här sessionen och skicka den publika nyckeln till hemservern. +\nDet här behövs bara en gång. +\nUrsäkta för besväret. + From 5a834619c022bc7a2fa9a7d5eab092471ea2550d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Fri, 15 May 2020 17:31:58 +0000 Subject: [PATCH 031/191] Translated using Weblate (Estonian) Currently translated at 97.5% (159 of 163 strings) Translation: Riot Android/RiotX Matrix SDK Translate-URL: https://translate.riot.im/projects/riot-android/riotx-matrix-sdk/et/ --- .../src/main/res/values-et/strings.xml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/matrix-sdk-android/src/main/res/values-et/strings.xml b/matrix-sdk-android/src/main/res/values-et/strings.xml index 1d52c2a7a1..fc92786a8d 100644 --- a/matrix-sdk-android/src/main/res/values-et/strings.xml +++ b/matrix-sdk-android/src/main/res/values-et/strings.xml @@ -185,4 +185,24 @@ %1$s lülitas sisse läbiva krüptimise. %1$s lülitas sisse läbiva krüptimise (tundmatu algoritm %2$s). + + %1$s lisas %2$s selle jututoa aadressiks. + %1$s lisas %2$s selle jututoa aadressideks. + + + + %1$s eemaldas %2$s kui selle jututoa aadressi. + %1$s eemaldas %2$s selle jututoa aadresside hulgast. + + + %1$s lisas %2$s ja eemaldas %3$s selle jututoa aadresside loendist. + + %1$s seadistas selle jututoa põhiaadressiks %2$s. + %1$s eemaldas selle jututoa põhiaadressi. + + %1$s lubas külalistel selle jututoaga liituda. + %1$s seadistas, et külalised ei või selle jututoaga liituda. + + %s soovib verifitseerida sinu võtmeid, kuid sinu kasutatav klient ei oska vestluse-sisest verifitseerimist teha. Sa pead kasutama traditsioonilist verifitseerimislahendust. + From 9c22c0952cf26909dc9c1372a7d059f2b630ec78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Sat, 16 May 2020 12:43:03 +0000 Subject: [PATCH 032/191] Translated using Weblate (Estonian) Currently translated at 5.9% (100 of 1682 strings) Translation: Riot Android/RiotX application Translate-URL: https://translate.riot.im/projects/riot-android/riotx-application/et/ --- vector/src/main/res/values-et/strings.xml | 79 +++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/vector/src/main/res/values-et/strings.xml b/vector/src/main/res/values-et/strings.xml index 549b1a1f7a..770f540f5e 100644 --- a/vector/src/main/res/values-et/strings.xml +++ b/vector/src/main/res/values-et/strings.xml @@ -32,4 +32,83 @@ Kui sa logid nüüd välja, siis sa kaotad ligipääsu kõikidele krüptitud sõnumitele Parasjagu varundan võtmeid. Kui sa logid nüüd välja, siis sa kaotad ligipääsu kõikidele oma krüptitud sõnumitele. + Turvaline võtmete varundus peaks olema aktiivne kõikides sinu sessioonides, sest see hoiab ära krüptitud sõnumitele ligipääsu kadumise. + Ma ei vaja oma krüptitud sõnumeid + Varundan võtmeid… + Kasuta varundatud võtmeid + Kas sa oled kindel\? + Varunda + Kui sa enne väljalogimist ei varunda oma võtmeid, siis sa kaotad ligipääsu oma krüptitud sõnumitele. + + Kolmandate osapoolte litsentsid + + Laeme… + + Sobib + Tühista + Salvesta + Lahku + Jää + Saada + Kopeeri + Saada uuesti + Eemalda + Tsiteeri + Lae alla + Jaga + Räägi + Eemalda + Hiljem + Edasta + Püsiviide + Lähtekood + Näita dekrüptitud lähtekoodi + Kustuta + Muuda nime + Ei midagi + Tunnista kehtetuks + Katkesta ühendus + Teata kahtlasest sisust + Kõne on käsil + Konverentsikõne on käsil. +\nLiitu kas %1$s või %2$s + häälkõnega + videokõnega + Kõne alustamine ei õnnestunud, palun proovi hiljem uuesti + Puuduolevate õiguste tõttu mõned funktsionaalsused ei toimi… + Puuduolevate õiguste tõttu seda toimingut ei saa teha. + Konverentsikõne alustamiseks selles jututoas on teil vaja õigusi + Kõne algatamine ei õnnestu + Sessiooniteave + Konverentsikõned ei ole krüptitud jututubades toetatud + Helista siiski + Saada ikkagi + või + Kutsu + Võrgust väljas + Võta vastu + Jäta vahele + Valmis + Katkesta + Eira + Vaata üle + Keeldu + + Välju + Tegevused + Logi välja + Kas sa oled kindel et soovid välja logida\? + Häälkõne + Videokõne + Üldine otsing + Märgi kõik loetuks + Ajalugu + Kiirvastus + Märgi loetuks + Ava + Sulge + Kopeeritud lõikelauale + Lülita välja + + Kinnitus From cfb615f9721b997879beb3c6f56e1f8d8c2007bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BC=D1=91=D0=B1=D0=B0?= Date: Sat, 16 May 2020 09:45:59 +0000 Subject: [PATCH 033/191] Translated using Weblate (Russian) Currently translated at 82.1% (1381 of 1682 strings) Translation: Riot Android/RiotX application Translate-URL: https://translate.riot.im/projects/riot-android/riotx-application/ru/ --- vector/src/main/res/values-ru/strings.xml | 35 ++++++++++++----------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/vector/src/main/res/values-ru/strings.xml b/vector/src/main/res/values-ru/strings.xml index 5837fa39ad..898401db08 100644 --- a/vector/src/main/res/values-ru/strings.xml +++ b/vector/src/main/res/values-ru/strings.xml @@ -96,7 +96,7 @@ %d пользователь %d пользователя %d пользователей - + Отправить логи @@ -838,13 +838,13 @@ %d комната %d комнаты %d комнат - + %d комната %d комнаты %d комнат - + %1$s в %2$s @@ -852,7 +852,7 @@ %d активный виджет %d активных виджета %d активных виджетов - + @@ -862,45 +862,45 @@ %d активный участник %d активных участника %d активных участников - + %d участник %d участника %d участников - + %d новое сообщение %d новых сообщения %d новых сообщений - + %1$s комната найдена для %2$s %1$s комнаты найдено для %2$s %1$s комнат найдено для %2$s - + %d изменение членства %d изменения членства %d изменений членства - + %d непрочитанное уведомление %d непрочитанных уведомления %d непрочитанных уведомлений - + %d непрочитанное уведомление %d непрочитанных уведомления %d непрочитанных уведомлений - + Получить аватар Заметка аватара @@ -1038,20 +1038,20 @@ %d выбран %d выбрано %d выбраны - + %d участник %d участника %d участников - + %d комната %d комнаты %d комнат - + Системные оповещения @@ -1339,7 +1339,7 @@ %d новый ключ был добавлен к этому устройству. %d новых ключа были добавлены к этому устройству. %d новых ключей были добавлены к этому устройству. - + @@ -1391,7 +1391,7 @@ Резервное копирование %d ключа… Резервное копирование %d ключей… Резервное копирование %d ключей… - + Все ключи сохранены @@ -2021,4 +2021,5 @@ Интерактивная проверка по отсебятинам Пожалуйста, выберите имя пользователя. Пожалуйста, выберите пароль. - +Скрыть… + From c2906d9b06b0fa32edc3a91416d2a9fc4888a909 Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Sat, 16 May 2020 09:59:01 +0000 Subject: [PATCH 034/191] Translated using Weblate (Swedish) Currently translated at 14.6% (245 of 1682 strings) Translation: Riot Android/RiotX application Translate-URL: https://translate.riot.im/projects/riot-android/riotx-application/sv/ --- vector/src/main/res/values-sv/strings.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/vector/src/main/res/values-sv/strings.xml b/vector/src/main/res/values-sv/strings.xml index daeb20e67d..e949a90027 100644 --- a/vector/src/main/res/values-sv/strings.xml +++ b/vector/src/main/res/values-sv/strings.xml @@ -286,4 +286,9 @@ \nDet här behövs bara en gång. \nUrsäkta för besväret. + Efterfråga krypteringsnycklarna igen från dina andra sessioner. + + Nyckelförfrågan har skickats. + + Förfrågan har skickats From 3185b88fe525bc20a7bb86614bbba38b46d4acd9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 May 2020 09:34:14 +0200 Subject: [PATCH 035/191] Better connectivity lost indicator when airplane mode is on --- CHANGES.md | 2 +- .../java/im/vector/riotx/core/utils/SystemUtils.kt | 4 ++++ .../riotx/features/sync/widget/SyncStateView.kt | 14 ++++++++++---- vector/src/main/res/layout/view_sync_state.xml | 13 +++++++++++++ vector/src/main/res/values/strings.xml | 1 + 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 099e4cf5f4..37cdfba1f5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ Features ✨: - Improvements 🙌: - - + - Better connectivity lost indicator when airplane mode is on Bugfix 🐛: - 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 d82134caf5..23f3cbc875 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 @@ -53,6 +53,10 @@ fun isIgnoringBatteryOptimizations(context: Context): Boolean { || (context.getSystemService(Context.POWER_SERVICE) as PowerManager?)?.isIgnoringBatteryOptimizations(context.packageName) == true } +fun isAirplaneModeOn(context: Context): Boolean { + return Settings.Global.getInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) != 0 +} + /** * display the system dialog for granting this permission. If previously granted, the * system will not show it (so you should call this method). diff --git a/vector/src/main/java/im/vector/riotx/features/sync/widget/SyncStateView.kt b/vector/src/main/java/im/vector/riotx/features/sync/widget/SyncStateView.kt index b8856dddb1..fa392a10ad 100755 --- a/vector/src/main/java/im/vector/riotx/features/sync/widget/SyncStateView.kt +++ b/vector/src/main/java/im/vector/riotx/features/sync/widget/SyncStateView.kt @@ -23,6 +23,7 @@ import android.widget.FrameLayout import androidx.core.view.isVisible import im.vector.matrix.android.api.session.sync.SyncState import im.vector.riotx.R +import im.vector.riotx.core.utils.isAirplaneModeOn import kotlinx.android.synthetic.main.view_sync_state.view.* class SyncStateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) @@ -33,10 +34,15 @@ class SyncStateView @JvmOverloads constructor(context: Context, attrs: Attribute } fun render(newState: SyncState) { - syncStateProgressBar.visibility = when (newState) { - is SyncState.Running -> if (newState.afterPause) View.VISIBLE else View.GONE - else -> View.GONE + syncStateProgressBar.isVisible = newState is SyncState.Running && newState.afterPause + + if (newState == SyncState.NoNetwork) { + val isAirplaneModeOn = isAirplaneModeOn(context) + syncStateNoNetwork.isVisible = isAirplaneModeOn.not() + syncStateNoNetworkAirplane.isVisible = isAirplaneModeOn + } else { + syncStateNoNetwork.isVisible = false + syncStateNoNetworkAirplane.isVisible = false } - syncStateNoNetwork.isVisible = newState == SyncState.NoNetwork } } diff --git a/vector/src/main/res/layout/view_sync_state.xml b/vector/src/main/res/layout/view_sync_state.xml index bc828045fe..0e7ddabc21 100644 --- a/vector/src/main/res/layout/view_sync_state.xml +++ b/vector/src/main/res/layout/view_sync_state.xml @@ -33,6 +33,19 @@ android:text="@string/no_connectivity_to_the_server_indicator" android:textColor="@color/white" android:visibility="gone" + tools:layout_marginTop="10dp" + tools:visibility="visible" /> + + \ 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 89cbc9a856..0158c61fcc 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2163,6 +2163,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming No Connectivity to the server has been lost + Airplane mode is on Dev Tools Account Data From 9fed62e4a7aa80a34846fdd289056bc1e9288041 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 May 2020 01:31:15 +0200 Subject: [PATCH 036/191] Improve template --- .../RiotXFeature/root/src/app_package/ViewModel.kt.ftl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/templates/RiotXFeature/root/src/app_package/ViewModel.kt.ftl b/tools/templates/RiotXFeature/root/src/app_package/ViewModel.kt.ftl index f4090b40e6..1d2ec0a069 100644 --- a/tools/templates/RiotXFeature/root/src/app_package/ViewModel.kt.ftl +++ b/tools/templates/RiotXFeature/root/src/app_package/ViewModel.kt.ftl @@ -6,6 +6,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.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorViewModel <#if createViewEvents> @@ -38,7 +39,8 @@ class ${viewModelClass} @AssistedInject constructor(@Assisted initialState: ${vi } override fun handle(action: ${actionClass}) { - //TODO - } + when (action) { + }.exhaustive + } } From f45040c4051f602ef8207fdcf69ee2135e37ad9e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 May 2020 02:14:12 +0200 Subject: [PATCH 037/191] Locale: support locale change --- CHANGES.md | 2 +- .../im/vector/riotx/core/di/FragmentModule.kt | 10 ++- .../vector/riotx/core/extensions/Activity.kt | 6 ++ .../configuration/VectorConfiguration.kt | 2 - .../riotx/features/settings/VectorLocale.kt | 41 ++++++---- .../VectorSettingsPreferencesFragment.kt | 26 ------ .../features/settings/locale/LocaleItem.kt | 48 +++++++++++ .../settings/locale/LocalePickerAction.kt | 24 ++++++ .../settings/locale/LocalePickerController.kt | 82 +++++++++++++++++++ .../settings/locale/LocalePickerFragment.kt | 80 ++++++++++++++++++ .../settings/locale/LocalePickerViewEvents.kt | 23 ++++++ .../settings/locale/LocalePickerViewModel.kt | 68 +++++++++++++++ .../settings/locale/LocalePickerViewState.kt | 25 ++++++ .../res/layout/fragment_locale_picker.xml | 8 ++ vector/src/main/res/layout/item_locale.xml | 41 ++++++++++ vector/src/main/res/values/strings.xml | 3 + .../res/xml/vector_settings_preferences.xml | 5 +- 17 files changed, 444 insertions(+), 50 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/locale/LocaleItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerAction.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerController.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewEvents.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewState.kt create mode 100644 vector/src/main/res/layout/fragment_locale_picker.xml create mode 100644 vector/src/main/res/layout/item_locale.xml diff --git a/CHANGES.md b/CHANGES.md index 37cdfba1f5..0ff37419cd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,7 @@ Changes in RiotX 0.21.0 (2020-XX-XX) =================================================== Features ✨: - - + - Switch language support (#41) Improvements 🙌: - Better connectivity lost indicator when airplane mode is on 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 01709efcac..26a5142961 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 @@ -61,8 +61,6 @@ import im.vector.riotx.features.login.LoginSplashFragment import im.vector.riotx.features.login.LoginWaitForEmailFragment import im.vector.riotx.features.login.LoginWebFragment import im.vector.riotx.features.login.terms.LoginTermsFragment -import im.vector.riotx.features.userdirectory.KnownUsersFragment -import im.vector.riotx.features.userdirectory.UserDirectoryFragment import im.vector.riotx.features.qrcode.QrCodeScannerFragment import im.vector.riotx.features.reactions.EmojiChooserFragment import im.vector.riotx.features.reactions.EmojiSearchResultFragment @@ -92,9 +90,12 @@ import im.vector.riotx.features.settings.devtools.IncomingKeyRequestListFragment import im.vector.riotx.features.settings.devtools.KeyRequestsFragment import im.vector.riotx.features.settings.devtools.OutgoingKeyRequestListFragment import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment +import im.vector.riotx.features.settings.locale.LocalePickerFragment import im.vector.riotx.features.settings.push.PushGatewaysFragment import im.vector.riotx.features.share.IncomingShareFragment import im.vector.riotx.features.signout.soft.SoftLogoutFragment +import im.vector.riotx.features.userdirectory.KnownUsersFragment +import im.vector.riotx.features.userdirectory.UserDirectoryFragment @Module interface FragmentModule { @@ -109,6 +110,11 @@ interface FragmentModule { @FragmentKey(RoomListFragment::class) fun bindRoomListFragment(fragment: RoomListFragment): Fragment + @Binds + @IntoMap + @FragmentKey(LocalePickerFragment::class) + fun bindLocalePickerFragment(fragment: LocalePickerFragment): Fragment + @Binds @IntoMap @FragmentKey(GroupListFragment::class) 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 f9f5d3b3d2..b74f143e17 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 @@ -16,6 +16,7 @@ package im.vector.riotx.core.extensions +import android.app.Activity import android.os.Parcelable import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentTransaction @@ -59,3 +60,8 @@ fun VectorBaseActivity.addFragmentToBackstack(frameId: Int, fun VectorBaseActivity.hideKeyboard() { currentFocus?.hideKeyboard() } + +fun Activity.restart() { + startActivity(intent) + finish() +} diff --git a/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt b/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt index a4b7ca263d..8ea17fecf6 100644 --- a/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt +++ b/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt @@ -30,7 +30,6 @@ import javax.inject.Inject /** * Handle locale configuration change, such as theme, font size and locale chosen by the user */ - class VectorConfiguration @Inject constructor(private val context: Context) { // TODO Import mLanguageReceiver From Riot? @@ -98,7 +97,6 @@ class VectorConfiguration @Inject constructor(private val context: Context) { * * @param locale */ - // TODO Call from LanguagePickerActivity fun updateApplicationLocale(locale: Locale) { updateApplicationSettings(locale, FontScale.getFontScalePrefValue(context), ThemeUtils.getApplicationTheme(context)) } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt index e1a89ab3c4..4d78a30718 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt @@ -19,9 +19,8 @@ package im.vector.riotx.features.settings import android.content.Context import android.content.res.Configuration import android.os.Build -import androidx.preference.PreferenceManager import androidx.core.content.edit -import im.vector.riotx.BuildConfig +import androidx.preference.PreferenceManager import im.vector.riotx.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -75,7 +74,7 @@ object VectorLocale { saveApplicationLocale(context, applicationLocale) } - // init the known locales in background, using kotlin coroutines + // init the known locales in background GlobalScope.launch(Dispatchers.IO) { initApplicationLocales(context) } @@ -144,6 +143,7 @@ object VectorLocale { } else { val resources = context.resources val conf = resources.configuration + @Suppress("DEPRECATION") val savedLocale = conf.locale @Suppress("DEPRECATION") @@ -235,22 +235,29 @@ object VectorLocale { append(locale.getDisplayCountry(locale)) append(")") } + } + } - // In debug mode, also display information about the locale in the current locale. - if (BuildConfig.DEBUG) { - append("\n[") - append(locale.displayLanguage) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && locale.script != "Latn") { - append(" - ") - append(locale.displayScript) - } - if (locale.displayCountry.isNotEmpty()) { - append(" (") - append(locale.displayCountry) - append(")") - } - append("]") + /** + * Information about the locale in the current locale + * + * @param locale the locale to get info from + * @return the string + */ + fun localeToLocalisedStringInfo(locale: Locale): String { + return buildString { + append("[") + append(locale.displayLanguage) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && locale.script != "Latn") { + append(" - ") + append(locale.displayScript) } + if (locale.displayCountry.isNotEmpty()) { + append(" (") + append(locale.displayCountry) + append(")") + } + append("]") } } } 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 9c240ad093..a8ba7bcbe6 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 @@ -18,7 +18,6 @@ package im.vector.riotx.features.settings import android.app.Activity import android.content.Context -import android.content.Intent import android.widget.CheckedTextView import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog @@ -129,21 +128,6 @@ class VectorSettingsPreferencesFragment @Inject constructor( } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - - if (resultCode == Activity.RESULT_OK) { - when (requestCode) { - REQUEST_LOCALE -> { - activity?.let { - startActivity(it.intent) - it.finish() - } - } - } - } - } - // ============================================================================================================== // user interface management // ============================================================================================================== @@ -152,12 +136,6 @@ class VectorSettingsPreferencesFragment @Inject constructor( // Selected language selectedLanguagePreference.summary = VectorLocale.localeToLocalisedString(VectorLocale.applicationLocale) - selectedLanguagePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { - notImplemented() - // TODO startActivityForResult(LanguagePickerActivity.getIntent(activity), REQUEST_LOCALE) - true - } - // Text size textSizePreference.summary = FontScale.getFontScaleDescription(activity!!) @@ -199,8 +177,4 @@ class VectorSettingsPreferencesFragment @Inject constructor( } } } - - companion object { - private const val REQUEST_LOCALE = 777 - } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocaleItem.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocaleItem.kt new file mode 100644 index 0000000000..18c5cb2aae --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocaleItem.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.settings.locale + +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_locale) +abstract class LocaleItem : VectorEpoxyModel() { + + @EpoxyAttribute var title: String? = null + @EpoxyAttribute var subtitle: String? = null + @EpoxyAttribute var clickListener: ClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + + holder.view.onClick { clickListener?.invoke() } + holder.titleView.setTextOrHide(title) + holder.subtitleView.setTextOrHide(subtitle) + } + + class Holder : VectorEpoxyHolder() { + val titleView by bind(R.id.localeTitle) + val subtitleView by bind(R.id.localeSubtitle) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerAction.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerAction.kt new file mode 100644 index 0000000000..0bfc203159 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerAction.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.riotx.features.settings.locale + +import im.vector.riotx.core.platform.VectorViewModelAction +import java.util.Locale + +sealed class LocalePickerAction : VectorViewModelAction { + data class SelectLocale(val locale: Locale) : LocalePickerAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerController.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerController.kt new file mode 100644 index 0000000000..74c5adf98c --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerController.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.settings.locale + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.noResultItem +import im.vector.riotx.core.epoxy.profiles.profileSectionItem +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.settings.VectorLocale +import im.vector.riotx.features.settings.VectorPreferences +import java.util.Locale +import javax.inject.Inject + +class LocalePickerController @Inject constructor( + private val vectorPreferences: VectorPreferences, + private val stringProvider: StringProvider +) : TypedEpoxyController() { + + var listener: Listener? = null + + @ExperimentalStdlibApi + override fun buildModels(data: LocalePickerViewState?) { + val list = data?.locales ?: return + + if (list.isEmpty()) { + noResultItem { + id("noResult") + text(stringProvider.getString(R.string.no_result_placeholder)) + } + } else { + profileSectionItem { + id("currentTitle") + title(stringProvider.getString(R.string.choose_locale_current_locale_title)) + } + localeItem { + id(data.currentLocale.toString()) + title(VectorLocale.localeToLocalisedString(data.currentLocale).capitalize(data.currentLocale)) + if (vectorPreferences.developerMode()) { + subtitle(VectorLocale.localeToLocalisedStringInfo(data.currentLocale)) + } + clickListener { listener?.onUseCurrentClicked() } + } + profileSectionItem { + id("otherTitle") + title(stringProvider.getString(R.string.choose_locale_other_locales_title)) + } + list + .filter { it != data.currentLocale } + .sortedBy { VectorLocale.localeToLocalisedString(it).toLowerCase(it) } + .forEach { + localeItem { + id(it.toString()) + title(VectorLocale.localeToLocalisedString(it).capitalize(it)) + if (vectorPreferences.developerMode()) { + subtitle(VectorLocale.localeToLocalisedStringInfo(it)) + } + clickListener { listener?.onLocaleClicked(it) } + } + } + } + } + + interface Listener { + fun onUseCurrentClicked() + fun onLocaleClicked(locale: Locale) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerFragment.kt new file mode 100644 index 0000000000..75d758aafa --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerFragment.kt @@ -0,0 +1,80 @@ +/* + * 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.settings.locale + +import android.os.Bundle +import android.view.View +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +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.exhaustive +import im.vector.riotx.core.extensions.restart +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.platform.VectorBaseFragment +import kotlinx.android.synthetic.main.fragment_locale_picker.* +import java.util.Locale +import javax.inject.Inject + +class LocalePickerFragment @Inject constructor( + private val viewModelFactory: LocalePickerViewModel.Factory, + private val controller: LocalePickerController +) : VectorBaseFragment(), LocalePickerViewModel.Factory by viewModelFactory, LocalePickerController.Listener { + + private val viewModel: LocalePickerViewModel by fragmentViewModel() + + override fun getLayoutResId() = R.layout.fragment_locale_picker + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + localeRecyclerView.configureWith(controller) + controller.listener = this + + viewModel.observeViewEvents { + when (it) { + LocalePickerViewEvents.RestartActivity -> { + activity?.restart() + } + }.exhaustive + } + } + + override fun onDestroyView() { + super.onDestroyView() + localeRecyclerView.cleanup() + controller.listener = null + } + + override fun invalidate() = withState(viewModel) { state -> + controller.setData(state) + } + + override fun onUseCurrentClicked() { + // Just close the fragment + parentFragmentManager.popBackStack() + } + + override fun onLocaleClicked(locale: Locale) { + viewModel.handle(LocalePickerAction.SelectLocale(locale)) + } + + override fun onResume() { + super.onResume() + (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_select_language) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewEvents.kt new file mode 100644 index 0000000000..e007f5f036 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewEvents.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.settings.locale + +import im.vector.riotx.core.platform.VectorViewEvents + +sealed class LocalePickerViewEvents : VectorViewEvents { + object RestartActivity : LocalePickerViewEvents() +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewModel.kt new file mode 100644 index 0000000000..1adcad5086 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewModel.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.features.settings.locale + +import com.airbnb.mvrx.ActivityViewModelContext +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.riotx.core.extensions.exhaustive +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.features.configuration.VectorConfiguration +import im.vector.riotx.features.settings.VectorLocale + +class LocalePickerViewModel @AssistedInject constructor( + @Assisted initialState: LocalePickerViewState, + private val vectorConfiguration: VectorConfiguration +) : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: LocalePickerViewState): LocalePickerViewModel + } + + companion object : MvRxViewModelFactory { + + override fun initialState(viewModelContext: ViewModelContext): LocalePickerViewState? { + return LocalePickerViewState( + locales = VectorLocale.supportedLocales + ) + } + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: LocalePickerViewState): LocalePickerViewModel? { + 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") + } + } + + override fun handle(action: LocalePickerAction) { + when (action) { + is LocalePickerAction.SelectLocale -> handleSelectLocale(action) + }.exhaustive + } + + private fun handleSelectLocale(action: LocalePickerAction.SelectLocale) { + vectorConfiguration.updateApplicationLocale(action.locale) + _viewEvents.post(LocalePickerViewEvents.RestartActivity) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewState.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewState.kt new file mode 100644 index 0000000000..6a0f39ab66 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewState.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.settings.locale + +import com.airbnb.mvrx.MvRxState +import java.util.Locale + +data class LocalePickerViewState( + val currentLocale: Locale = Locale.getDefault(), + val locales: List = emptyList() +) : MvRxState diff --git a/vector/src/main/res/layout/fragment_locale_picker.xml b/vector/src/main/res/layout/fragment_locale_picker.xml new file mode 100644 index 0000000000..1d7977d19a --- /dev/null +++ b/vector/src/main/res/layout/fragment_locale_picker.xml @@ -0,0 +1,8 @@ + + diff --git a/vector/src/main/res/layout/item_locale.xml b/vector/src/main/res/layout/item_locale.xml new file mode 100644 index 0000000000..a446f23e78 --- /dev/null +++ b/vector/src/main/res/layout/item_locale.xml @@ -0,0 +1,41 @@ + + + + + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 0158c61fcc..f2e5ea4edc 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2393,4 +2393,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming We could not invite users. Please check the users you want to invite and try again. + Current language + Other available languages + \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index e7217b7394..dde967a283 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -7,9 +7,10 @@ android:title="@string/settings_user_interface"> + android:persistent="false" + android:title="@string/settings_interface_language" + app:fragment="im.vector.riotx.features.settings.locale.LocalePickerFragment" /> Date: Thu, 14 May 2020 11:22:44 +0200 Subject: [PATCH 038/191] FontSize: rework by creating FontScaleValue data class. --- CHANGES.md | 2 +- .../configuration/VectorConfiguration.kt | 24 ++-- .../riotx/features/settings/FontScale.kt | 135 +++++------------- .../VectorSettingsPreferencesFragment.kt | 12 +- .../riotx/features/themes/ThemeUtils.kt | 2 +- .../res/xml/vector_settings_preferences.xml | 1 + 6 files changed, 56 insertions(+), 120 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0ff37419cd..991078e704 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,7 @@ Improvements 🙌: - Better connectivity lost indicator when airplane mode is on Bugfix 🐛: - - + - Fix issues with FontScale switch (#69, #645) Translations 🗣: - diff --git a/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt b/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt index 8ea17fecf6..d4dfd297b7 100644 --- a/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt +++ b/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt @@ -38,20 +38,20 @@ class VectorConfiguration @Inject constructor(private val context: Context) { Timber.v("## onConfigurationChanged(): the locale has been updated to ${Locale.getDefault()}") Timber.v("## onConfigurationChanged(): restore the expected value ${VectorLocale.applicationLocale}") updateApplicationSettings(VectorLocale.applicationLocale, - FontScale.getFontScalePrefValue(context), - ThemeUtils.getApplicationTheme(context)) + FontScale.getFontScaleValue(context), + ThemeUtils.getApplicationTheme(context)) } } - private fun updateApplicationSettings(locale: Locale, textSize: String, theme: String) { + private fun updateApplicationSettings(locale: Locale, fontScaleValue: FontScale.FontScaleValue, theme: String) { VectorLocale.saveApplicationLocale(context, locale) - FontScale.saveFontScale(context, textSize) + FontScale.saveFontScaleValue(context, fontScaleValue) Locale.setDefault(locale) val config = Configuration(context.resources.configuration) @Suppress("DEPRECATION") config.locale = locale - config.fontScale = FontScale.getFontScale(context) + config.fontScale = FontScale.getFontScaleValue(context).scale @Suppress("DEPRECATION") context.resources.updateConfiguration(config, context.resources.displayMetrics) @@ -67,8 +67,8 @@ class VectorConfiguration @Inject constructor(private val context: Context) { fun updateApplicationTheme(theme: String) { ThemeUtils.setApplicationTheme(context, theme) updateApplicationSettings(VectorLocale.applicationLocale, - FontScale.getFontScalePrefValue(context), - theme) + FontScale.getFontScaleValue(context), + theme) } /** @@ -77,14 +77,14 @@ class VectorConfiguration @Inject constructor(private val context: Context) { fun initConfiguration() { VectorLocale.init(context) val locale = VectorLocale.applicationLocale - val fontScale = FontScale.getFontScale(context) + val fontScale = FontScale.getFontScaleValue(context) val theme = ThemeUtils.getApplicationTheme(context) Locale.setDefault(locale) val config = Configuration(context.resources.configuration) @Suppress("DEPRECATION") config.locale = locale - config.fontScale = fontScale + config.fontScale = fontScale.scale @Suppress("DEPRECATION") context.resources.updateConfiguration(config, context.resources.displayMetrics) @@ -98,7 +98,7 @@ class VectorConfiguration @Inject constructor(private val context: Context) { * @param locale */ fun updateApplicationLocale(locale: Locale) { - updateApplicationSettings(locale, FontScale.getFontScalePrefValue(context), ThemeUtils.getApplicationTheme(context)) + updateApplicationSettings(locale, FontScale.getFontScaleValue(context), ThemeUtils.getApplicationTheme(context)) } /** @@ -113,7 +113,7 @@ class VectorConfiguration @Inject constructor(private val context: Context) { val resources = context.resources val locale = VectorLocale.applicationLocale val configuration = resources.configuration - configuration.fontScale = FontScale.getFontScale(context) + configuration.fontScale = FontScale.getFontScaleValue(context).scale if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { configuration.setLocale(locale) @@ -143,7 +143,7 @@ class VectorConfiguration @Inject constructor(private val context: Context) { // TODO Create data class for this fun getHash(): String { return (VectorLocale.applicationLocale.toString() - + "_" + FontScale.getFontScalePrefValue(context) + + "_" + FontScale.getFontScaleValue(context).preferenceValue + "_" + ThemeUtils.getApplicationTheme(context)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/FontScale.kt b/vector/src/main/java/im/vector/riotx/features/settings/FontScale.kt index a9e797ba7a..c538a1ac0c 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/FontScale.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/FontScale.kt @@ -17,7 +17,7 @@ package im.vector.riotx.features.settings import android.content.Context -import android.content.res.Configuration +import androidx.annotation.StringRes import androidx.core.content.edit import androidx.preference.PreferenceManager import im.vector.riotx.R @@ -29,124 +29,59 @@ object FontScale { // Key for the SharedPrefs private const val APPLICATION_FONT_SCALE_KEY = "APPLICATION_FONT_SCALE_KEY" - // Possible values for the SharedPrefs - private const val FONT_SCALE_TINY = "FONT_SCALE_TINY" - private const val FONT_SCALE_SMALL = "FONT_SCALE_SMALL" - private const val FONT_SCALE_NORMAL = "FONT_SCALE_NORMAL" - private const val FONT_SCALE_LARGE = "FONT_SCALE_LARGE" - private const val FONT_SCALE_LARGER = "FONT_SCALE_LARGER" - private const val FONT_SCALE_LARGEST = "FONT_SCALE_LARGEST" - private const val FONT_SCALE_HUGE = "FONT_SCALE_HUGE" - - private val fontScaleToPrefValue = mapOf( - 0.70f to FONT_SCALE_TINY, - 0.85f to FONT_SCALE_SMALL, - 1.00f to FONT_SCALE_NORMAL, - 1.15f to FONT_SCALE_LARGE, - 1.30f to FONT_SCALE_LARGER, - 1.45f to FONT_SCALE_LARGEST, - 1.60f to FONT_SCALE_HUGE + data class FontScaleValue( + val index: Int, + // Possible values for the SharedPrefs + val preferenceValue: String, + val scale: Float, + @StringRes + val nameResId: Int ) - private val prefValueToNameResId = mapOf( - FONT_SCALE_TINY to R.string.tiny, - FONT_SCALE_SMALL to R.string.small, - FONT_SCALE_NORMAL to R.string.normal, - FONT_SCALE_LARGE to R.string.large, - FONT_SCALE_LARGER to R.string.larger, - FONT_SCALE_LARGEST to R.string.largest, - FONT_SCALE_HUGE to R.string.huge + private val fontScaleValues = listOf( + FontScaleValue(0, "FONT_SCALE_TINY", 0.70f, R.string.tiny), + FontScaleValue(1, "FONT_SCALE_SMALL", 0.85f, R.string.small), + FontScaleValue(2, "FONT_SCALE_NORMAL", 1.00f, R.string.normal), + FontScaleValue(3, "FONT_SCALE_LARGE", 1.15f, R.string.large), + FontScaleValue(4, "FONT_SCALE_LARGER", 1.30f, R.string.larger), + FontScaleValue(5, "FONT_SCALE_LARGEST", 1.45f, R.string.largest), + FontScaleValue(6, "FONT_SCALE_HUGE", 1.60f, R.string.huge) ) + private val normalFontScaleValue = fontScaleValues[2] + /** * Get the font scale value from SharedPrefs. Init the SharedPrefs if necessary * - * @return the font scale + * @return the font scale value */ - fun getFontScalePrefValue(context: Context): String { + fun getFontScaleValue(context: Context): FontScaleValue { val preferences = PreferenceManager.getDefaultSharedPreferences(context) - var scalePreferenceValue: String - if (APPLICATION_FONT_SCALE_KEY !in preferences) { + return if (APPLICATION_FONT_SCALE_KEY !in preferences) { val fontScale = context.resources.configuration.fontScale - scalePreferenceValue = FONT_SCALE_NORMAL - - if (fontScaleToPrefValue.containsKey(fontScale)) { - scalePreferenceValue = fontScaleToPrefValue[fontScale] as String - } - - preferences.edit { - putString(APPLICATION_FONT_SCALE_KEY, scalePreferenceValue) - } + (fontScaleValues.firstOrNull { it.scale == fontScale } ?: normalFontScaleValue) + .also { preferences.edit { putString(APPLICATION_FONT_SCALE_KEY, it.preferenceValue) } } } else { - scalePreferenceValue = preferences.getString(APPLICATION_FONT_SCALE_KEY, FONT_SCALE_NORMAL)!! + val pref = preferences.getString(APPLICATION_FONT_SCALE_KEY, null) + fontScaleValues.firstOrNull { it.preferenceValue == pref } ?: normalFontScaleValue } + } - return scalePreferenceValue + fun updateFontScale(context: Context, index: Int) { + fontScaleValues.getOrNull(index)?.let { + saveFontScaleValue(context, it) + } } /** - * Provides the font scale value + * Store the font scale vale * - * @return the font scale + * @param fontScaleValue the font scale value to store */ - fun getFontScale(context: Context): Float { - val fontScale = getFontScalePrefValue(context) - - if (fontScaleToPrefValue.containsValue(fontScale)) { - for ((key, value) in fontScaleToPrefValue) { - if (value == fontScale) { - return key - } - } - } - - return 1.0f - } - - /** - * Provides the font scale description - * - * @return the font description - */ - fun getFontScaleDescription(context: Context): String { - val fontScale = getFontScalePrefValue(context) - - return if (prefValueToNameResId.containsKey(fontScale)) { - context.getString(prefValueToNameResId[fontScale] as Int) - } else context.getString(R.string.normal) - } - - /** - * Update the font size from the locale description. - * - * @param fontScaleDescription the font scale description - */ - fun updateFontScale(context: Context, fontScaleDescription: String) { - for ((key, value) in prefValueToNameResId) { - if (context.getString(value) == fontScaleDescription) { - saveFontScale(context, key) - } - } - - val config = Configuration(context.resources.configuration) - config.fontScale = getFontScale(context) - @Suppress("DEPRECATION") - context.resources.updateConfiguration(config, context.resources.displayMetrics) - } - - /** - * Save the new font scale - * - * @param scaleValue the text scale - */ - fun saveFontScale(context: Context, scaleValue: String) { - if (scaleValue.isNotEmpty()) { - PreferenceManager.getDefaultSharedPreferences(context) - .edit { - putString(APPLICATION_FONT_SCALE_KEY, scaleValue) - } - } + fun saveFontScaleValue(context: Context, fontScaleValue: FontScaleValue) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit { putString(APPLICATION_FONT_SCALE_KEY, fontScaleValue.preferenceValue) } } } 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 a8ba7bcbe6..86a256a2b4 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 @@ -24,6 +24,7 @@ import androidx.appcompat.app.AlertDialog import androidx.preference.Preference import androidx.preference.SwitchPreference import im.vector.riotx.R +import im.vector.riotx.core.extensions.restart import im.vector.riotx.core.preference.VectorListPreference import im.vector.riotx.core.preference.VectorPreference import im.vector.riotx.features.configuration.VectorConfiguration @@ -137,7 +138,7 @@ class VectorSettingsPreferencesFragment @Inject constructor( selectedLanguagePreference.summary = VectorLocale.localeToLocalisedString(VectorLocale.applicationLocale) // Text size - textSizePreference.summary = FontScale.getFontScaleDescription(activity!!) + textSizePreference.summary = getString(FontScale.getFontScaleValue(activity!!).nameResId) textSizePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { activity?.let { displayTextSizeSelection(it) } @@ -160,19 +161,18 @@ class VectorSettingsPreferencesFragment @Inject constructor( val childCount = linearLayout.childCount - val scaleText = FontScale.getFontScaleDescription(activity) + val index = FontScale.getFontScaleValue(activity).index for (i in 0 until childCount) { val v = linearLayout.getChildAt(i) if (v is CheckedTextView) { - v.isChecked = v.text == scaleText + v.isChecked = i == index v.setOnClickListener { dialog.dismiss() - FontScale.updateFontScale(activity, v.text.toString()) - activity.startActivity(activity.intent) - activity.finish() + FontScale.updateFontScale(activity, i) + activity.restart() } } } diff --git a/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt b/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt index 1f835164db..45e64465d6 100644 --- a/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt +++ b/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt @@ -52,7 +52,7 @@ object ThemeUtils { */ fun getApplicationTheme(context: Context): String { return PreferenceManager.getDefaultSharedPreferences(context) - .getString(APPLICATION_THEME_KEY, THEME_LIGHT_VALUE)!! + .getString(APPLICATION_THEME_KEY, THEME_LIGHT_VALUE) ?: THEME_LIGHT_VALUE } /** diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index dde967a283..b1bd6ea5e6 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -24,6 +24,7 @@ From 19d655ec4149cce0f8bbbf6b23706bec5273cbc4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 May 2020 11:41:42 +0200 Subject: [PATCH 039/191] Locales: improve algo --- .../vector/riotx/features/settings/VectorLocale.kt | 13 ++++++------- .../settings/locale/LocalePickerController.kt | 1 - 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt index 4d78a30718..13e68d83df 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt @@ -42,8 +42,7 @@ object VectorLocale { /** * The supported application languages */ - var supportedLocales = ArrayList() - private set + val supportedLocales = mutableListOf() /** * Provides the current application locale @@ -195,9 +194,7 @@ object VectorLocale { ) } - supportedLocales.clear() - - knownLocalesSet.mapTo(supportedLocales) { (language, country, script) -> + val list = knownLocalesSet.map { (language, country, script) -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { Locale.Builder() .setLanguage(language) @@ -208,9 +205,11 @@ object VectorLocale { Locale(language, country) } } + // sort by human display names + .sortedBy { localeToLocalisedString(it).toLowerCase(it) } - // sort by human display names - supportedLocales.sortWith(Comparator { lhs, rhs -> localeToLocalisedString(lhs).compareTo(localeToLocalisedString(rhs)) }) + supportedLocales.clear() + supportedLocales.addAll(list) } /** diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerController.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerController.kt index 74c5adf98c..ecaeac31c1 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerController.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerController.kt @@ -61,7 +61,6 @@ class LocalePickerController @Inject constructor( } list .filter { it != data.currentLocale } - .sortedBy { VectorLocale.localeToLocalisedString(it).toLowerCase(it) } .forEach { localeItem { id(it.toString()) From 8ac2cb0530f714409575110c587dfc5bfab844e7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 May 2020 21:10:03 +0200 Subject: [PATCH 040/191] Cleanup --- .../vector/riotx/core/di/VectorComponent.kt | 4 +- .../riotx/core/platform/VectorBaseActivity.kt | 2 +- .../configuration/VectorConfiguration.kt | 43 +------------------ .../setup/KeysBackupSetupStep2Fragment.kt | 8 ++-- .../riotx/features/settings/FontScale.kt | 2 +- .../riotx/features/settings/VectorLocale.kt | 7 ++- .../VectorSettingsPreferencesFragment.kt | 8 +--- .../settings/locale/LocalePickerViewModel.kt | 16 +++---- 8 files changed, 24 insertions(+), 66 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt index 6f864c7f5b..5c5052cf2b 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt @@ -130,9 +130,9 @@ interface VectorComponent { fun emojiDataSource(): EmojiDataSource - fun alertManager() : PopupAlertManager + fun alertManager(): PopupAlertManager - fun reAuthHelper() : ReAuthHelper + fun reAuthHelper(): ReAuthHelper @Component.Factory interface Factory { 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 08cf8e57e1..191f5dbc4c 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 @@ -179,7 +179,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { } }) - sessionListener = getVectorComponent().sessionListener() + sessionListener = vectorComponent.sessionListener() sessionListener.globalErrorLiveData.observeEvent(this) { handleGlobalError(it) } diff --git a/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt b/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt index d4dfd297b7..2ef69890ed 100644 --- a/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt +++ b/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt @@ -32,45 +32,14 @@ import javax.inject.Inject */ class VectorConfiguration @Inject constructor(private val context: Context) { - // TODO Import mLanguageReceiver From Riot? fun onConfigurationChanged() { if (Locale.getDefault().toString() != VectorLocale.applicationLocale.toString()) { Timber.v("## onConfigurationChanged(): the locale has been updated to ${Locale.getDefault()}") Timber.v("## onConfigurationChanged(): restore the expected value ${VectorLocale.applicationLocale}") - updateApplicationSettings(VectorLocale.applicationLocale, - FontScale.getFontScaleValue(context), - ThemeUtils.getApplicationTheme(context)) + Locale.setDefault(VectorLocale.applicationLocale) } } - private fun updateApplicationSettings(locale: Locale, fontScaleValue: FontScale.FontScaleValue, theme: String) { - VectorLocale.saveApplicationLocale(context, locale) - FontScale.saveFontScaleValue(context, fontScaleValue) - Locale.setDefault(locale) - - val config = Configuration(context.resources.configuration) - @Suppress("DEPRECATION") - config.locale = locale - config.fontScale = FontScale.getFontScaleValue(context).scale - @Suppress("DEPRECATION") - context.resources.updateConfiguration(config, context.resources.displayMetrics) - - ThemeUtils.setApplicationTheme(context, theme) - // TODO PhoneNumberUtils.onLocaleUpdate() - } - - /** - * Update the application theme - * - * @param theme the new theme - */ - fun updateApplicationTheme(theme: String) { - ThemeUtils.setApplicationTheme(context, theme) - updateApplicationSettings(VectorLocale.applicationLocale, - FontScale.getFontScaleValue(context), - theme) - } - /** * Init the configuration from the saved one */ @@ -92,15 +61,6 @@ class VectorConfiguration @Inject constructor(private val context: Context) { ThemeUtils.setApplicationTheme(context, theme) } - /** - * Update the application locale - * - * @param locale - */ - fun updateApplicationLocale(locale: Locale) { - updateApplicationSettings(locale, FontScale.getFontScaleValue(context), ThemeUtils.getApplicationTheme(context)) - } - /** * Compute a localised context * @@ -140,7 +100,6 @@ class VectorConfiguration @Inject constructor(private val context: Context) { * Compute the locale status value * @return the local status value */ - // TODO Create data class for this fun getHash(): String { return (VectorLocale.applicationLocale.toString() + "_" + FontScale.getFontScaleValue(context).preferenceValue 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 3522c5a752..93d6f43763 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 @@ -164,16 +164,16 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment() @OnClick(R.id.keys_backup_setup_step2_button) fun doNext() { when { - viewModel.passphrase.value.isNullOrEmpty() -> { + viewModel.passphrase.value.isNullOrEmpty() -> { viewModel.passphraseError.value = context?.getString(R.string.passphrase_empty_error_message) } viewModel.passphrase.value != viewModel.confirmPassphrase.value -> { viewModel.confirmPassphraseError.value = context?.getString(R.string.passphrase_passphrase_does_not_match) } - viewModel.passwordStrength.value?.score ?: 0 < 4 -> { + viewModel.passwordStrength.value?.score ?: 0 < 4 -> { viewModel.passphraseError.value = context?.getString(R.string.passphrase_passphrase_too_weak) } - else -> { + else -> { viewModel.megolmBackupCreationInfo = null viewModel.prepareRecoveryKey(activity!!, viewModel.passphrase.value) @@ -190,7 +190,7 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment() viewModel.prepareRecoveryKey(activity!!, null) } - else -> { + else -> { // User has entered a passphrase but want to skip this step. viewModel.passphraseError.value = context?.getString(R.string.keys_backup_passphrase_not_empty_error_message) } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/FontScale.kt b/vector/src/main/java/im/vector/riotx/features/settings/FontScale.kt index c538a1ac0c..47c438695c 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/FontScale.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/FontScale.kt @@ -80,7 +80,7 @@ object FontScale { * * @param fontScaleValue the font scale value to store */ - fun saveFontScaleValue(context: Context, fontScaleValue: FontScaleValue) { + private fun saveFontScaleValue(context: Context, fontScaleValue: FontScaleValue) { PreferenceManager.getDefaultSharedPreferences(context) .edit { putString(APPLICATION_FONT_SCALE_KEY, fontScaleValue.preferenceValue) } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt index 13e68d83df..95b31be317 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt @@ -50,10 +50,13 @@ object VectorLocale { var applicationLocale = defaultLocale private set + lateinit var context: Context + /** * Init this object */ fun init(context: Context) { + this.context = context val preferences = PreferenceManager.getDefaultSharedPreferences(context) if (preferences.contains(APPLICATION_LOCALE_LANGUAGE_KEY)) { @@ -70,7 +73,7 @@ object VectorLocale { applicationLocale = defaultLocale } - saveApplicationLocale(context, applicationLocale) + saveApplicationLocale(applicationLocale) } // init the known locales in background @@ -82,7 +85,7 @@ object VectorLocale { /** * Save the new application locale. */ - fun saveApplicationLocale(context: Context, locale: Locale) { + fun saveApplicationLocale(locale: Locale) { applicationLocale = locale PreferenceManager.getDefaultSharedPreferences(context).edit { 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 86a256a2b4..6b021d022f 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 @@ -54,13 +54,9 @@ class VectorSettingsPreferencesFragment @Inject constructor( findPreference(ThemeUtils.APPLICATION_THEME_KEY)!! .onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> if (newValue is String) { - vectorConfiguration.updateApplicationTheme(newValue) + ThemeUtils.setApplicationTheme(requireContext(), newValue) // Restart the Activity - activity?.let { - // Note: recreate does not apply the color correctly - it.startActivity(it.intent) - it.finish() - } + activity?.restart() true } else { false diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewModel.kt index 1adcad5086..61142b7af5 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewModel.kt @@ -24,12 +24,10 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorViewModel -import im.vector.riotx.features.configuration.VectorConfiguration import im.vector.riotx.features.settings.VectorLocale class LocalePickerViewModel @AssistedInject constructor( - @Assisted initialState: LocalePickerViewState, - private val vectorConfiguration: VectorConfiguration + @Assisted initialState: LocalePickerViewState ) : VectorViewModel(initialState) { @AssistedInject.Factory @@ -37,13 +35,15 @@ class LocalePickerViewModel @AssistedInject constructor( fun create(initialState: LocalePickerViewState): LocalePickerViewModel } - companion object : MvRxViewModelFactory { - - override fun initialState(viewModelContext: ViewModelContext): LocalePickerViewState? { - return LocalePickerViewState( + init { + setState { + copy( locales = VectorLocale.supportedLocales ) } + } + + companion object : MvRxViewModelFactory { @JvmStatic override fun create(viewModelContext: ViewModelContext, state: LocalePickerViewState): LocalePickerViewModel? { @@ -62,7 +62,7 @@ class LocalePickerViewModel @AssistedInject constructor( } private fun handleSelectLocale(action: LocalePickerAction.SelectLocale) { - vectorConfiguration.updateApplicationLocale(action.locale) + VectorLocale.saveApplicationLocale(action.locale) _viewEvents.post(LocalePickerViewEvents.RestartActivity) } } From a00ddca188890b660c58315e70416deb71748b60 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 May 2020 22:04:50 +0200 Subject: [PATCH 041/191] Create SystemLocaleProvider --- .../im/vector/riotx/core/utils/SystemUtils.kt | 21 --------- .../riotx/features/rageshake/BugReporter.kt | 15 ++++--- .../settings/locale/LocalePickerController.kt | 2 +- .../settings/locale/LocalePickerViewState.kt | 3 +- .../settings/locale/SystemLocaleProvider.kt | 44 +++++++++++++++++++ 5 files changed, 56 insertions(+), 29 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/locale/SystemLocaleProvider.kt 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 23f3cbc875..9e5af038ef 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 @@ -33,9 +33,6 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import im.vector.riotx.R import im.vector.riotx.features.notifications.NotificationUtils -import im.vector.riotx.features.settings.VectorLocale -import timber.log.Timber -import java.util.Locale /** * Tells if the application ignores battery optimizations. @@ -94,24 +91,6 @@ fun copyToClipboard(context: Context, text: CharSequence, showToast: Boolean = t } } -/** - * Provides the device locale - * - * @return the device locale - */ -fun getDeviceLocale(context: Context): Locale { - return try { - val packageManager = context.packageManager - val resources = packageManager.getResourcesForApplication("android") - @Suppress("DEPRECATION") - resources.configuration.locale - } catch (e: Exception) { - Timber.e(e, "## getDeviceLocale() failed") - // Fallback to application locale - VectorLocale.applicationLocale - } -} - /** * Shows notification settings for the current app. * In android O will directly opens the notification settings, in lower version it will show the App settings diff --git a/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt index 7d58c4aacc..8515a5ac50 100755 --- a/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt +++ b/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt @@ -31,9 +31,9 @@ import im.vector.riotx.BuildConfig import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.extensions.toOnOff -import im.vector.riotx.core.utils.getDeviceLocale import im.vector.riotx.features.settings.VectorLocale import im.vector.riotx.features.settings.VectorPreferences +import im.vector.riotx.features.settings.locale.SystemLocaleProvider import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.version.VersionProvider import okhttp3.Call @@ -58,10 +58,13 @@ import javax.inject.Singleton * BugReporter creates and sends the bug reports. */ @Singleton -class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, - private val versionProvider: VersionProvider, - private val vectorPreferences: VectorPreferences, - private val vectorFileLogger: VectorFileLogger) { +class BugReporter @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, + private val versionProvider: VersionProvider, + private val vectorPreferences: VectorPreferences, + private val vectorFileLogger: VectorFileLogger, + private val systemLocaleProvider: SystemLocaleProvider +) { var inMultiWindowMode = false companion object { @@ -240,7 +243,7 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes + Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME) .addFormDataPart("locale", Locale.getDefault().toString()) .addFormDataPart("app_language", VectorLocale.applicationLocale.toString()) - .addFormDataPart("default_app_language", getDeviceLocale(context).toString()) + .addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString()) .addFormDataPart("theme", ThemeUtils.getApplicationTheme(context)) val buildNumber = context.getString(R.string.build_number) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerController.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerController.kt index ecaeac31c1..3745ba513e 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerController.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerController.kt @@ -60,7 +60,7 @@ class LocalePickerController @Inject constructor( title(stringProvider.getString(R.string.choose_locale_other_locales_title)) } list - .filter { it != data.currentLocale } + .filter { it.toString() != data.currentLocale.toString() } .forEach { localeItem { id(it.toString()) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewState.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewState.kt index 6a0f39ab66..e32cdc632c 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewState.kt @@ -17,9 +17,10 @@ package im.vector.riotx.features.settings.locale import com.airbnb.mvrx.MvRxState +import im.vector.riotx.features.settings.VectorLocale import java.util.Locale data class LocalePickerViewState( - val currentLocale: Locale = Locale.getDefault(), + val currentLocale: Locale = VectorLocale.applicationLocale, val locales: List = emptyList() ) : MvRxState diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/SystemLocaleProvider.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/SystemLocaleProvider.kt new file mode 100644 index 0000000000..d3265f3179 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/SystemLocaleProvider.kt @@ -0,0 +1,44 @@ +/* + * 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.settings.locale + +import android.content.Context +import timber.log.Timber +import java.util.Locale +import javax.inject.Inject + +class SystemLocaleProvider @Inject constructor( + private val context: Context +) { + + /** + * Provides the device locale + * + * @return the device locale, or null in case of error + */ + fun getSystemLocale(): Locale? { + return try { + val packageManager = context.packageManager + val resources = packageManager.getResourcesForApplication("android") + @Suppress("DEPRECATION") + resources.configuration.locale + } catch (e: Exception) { + Timber.e(e, "## getDeviceLocale() failed") + null + } + } +} From 538bda329e78e5309996d91a97370dfeec5ab927 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 14 May 2020 22:32:47 +0200 Subject: [PATCH 042/191] Lazy load available languages --- .../riotx/features/settings/VectorLocale.kt | 33 ++++++--- .../settings/locale/LocalePickerController.kt | 72 +++++++++++-------- .../settings/locale/LocalePickerViewModel.kt | 27 +++++-- .../settings/locale/LocalePickerViewState.kt | 4 +- vector/src/main/res/values/strings.xml | 1 + 5 files changed, 89 insertions(+), 48 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt index 95b31be317..efe4110680 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt @@ -24,7 +24,9 @@ import androidx.preference.PreferenceManager import im.vector.riotx.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber import java.util.Locale @@ -40,9 +42,9 @@ object VectorLocale { private val defaultLocale = Locale("en", "US") /** - * The supported application languages + * The cache of supported application languages */ - val supportedLocales = mutableListOf() + private val supportedLocales = mutableListOf() /** * Provides the current application locale @@ -75,11 +77,6 @@ object VectorLocale { saveApplicationLocale(applicationLocale) } - - // init the known locales in background - GlobalScope.launch(Dispatchers.IO) { - initApplicationLocales(context) - } } /** @@ -167,11 +164,9 @@ object VectorLocale { } /** - * Provides the supported application locales list - * - * @param context the context + * Init the supported application locales list */ - private fun initApplicationLocales(context: Context) { + private fun initApplicationLocales() { val knownLocalesSet = HashSet>() try { @@ -262,4 +257,20 @@ object VectorLocale { append("]") } } + + fun loadLocales(listener: Listener): Job { + return GlobalScope.launch(Dispatchers.Main) { + if (supportedLocales.isEmpty()) { + // init the known locales in background + withContext(Dispatchers.IO) { + initApplicationLocales() + } + } + listener.onLoaded(supportedLocales) + } + } + + interface Listener { + fun onLoaded(locales: List) + } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerController.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerController.kt index 3745ba513e..5e6704818f 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerController.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerController.kt @@ -17,7 +17,10 @@ package im.vector.riotx.features.settings.locale import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Incomplete +import com.airbnb.mvrx.Success import im.vector.riotx.R +import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.epoxy.noResultItem import im.vector.riotx.core.epoxy.profiles.profileSectionItem import im.vector.riotx.core.resources.StringProvider @@ -37,40 +40,49 @@ class LocalePickerController @Inject constructor( override fun buildModels(data: LocalePickerViewState?) { val list = data?.locales ?: return - if (list.isEmpty()) { - noResultItem { - id("noResult") - text(stringProvider.getString(R.string.no_result_placeholder)) + profileSectionItem { + id("currentTitle") + title(stringProvider.getString(R.string.choose_locale_current_locale_title)) + } + localeItem { + id(data.currentLocale.toString()) + title(VectorLocale.localeToLocalisedString(data.currentLocale).capitalize(data.currentLocale)) + if (vectorPreferences.developerMode()) { + subtitle(VectorLocale.localeToLocalisedStringInfo(data.currentLocale)) } - } else { - profileSectionItem { - id("currentTitle") - title(stringProvider.getString(R.string.choose_locale_current_locale_title)) - } - localeItem { - id(data.currentLocale.toString()) - title(VectorLocale.localeToLocalisedString(data.currentLocale).capitalize(data.currentLocale)) - if (vectorPreferences.developerMode()) { - subtitle(VectorLocale.localeToLocalisedStringInfo(data.currentLocale)) + clickListener { listener?.onUseCurrentClicked() } + } + profileSectionItem { + id("otherTitle") + title(stringProvider.getString(R.string.choose_locale_other_locales_title)) + } + when (list) { + is Incomplete -> { + loadingItem { + id("loading") + loadingText(stringProvider.getString(R.string.choose_locale_loading_locales)) } - clickListener { listener?.onUseCurrentClicked() } } - profileSectionItem { - id("otherTitle") - title(stringProvider.getString(R.string.choose_locale_other_locales_title)) - } - list - .filter { it.toString() != data.currentLocale.toString() } - .forEach { - localeItem { - id(it.toString()) - title(VectorLocale.localeToLocalisedString(it).capitalize(it)) - if (vectorPreferences.developerMode()) { - subtitle(VectorLocale.localeToLocalisedStringInfo(it)) - } - clickListener { listener?.onLocaleClicked(it) } - } + is Success -> + if (list().isEmpty()) { + noResultItem { + id("noResult") + text(stringProvider.getString(R.string.no_result_placeholder)) } + } else { + list() + .filter { it.toString() != data.currentLocale.toString() } + .forEach { + localeItem { + id(it.toString()) + title(VectorLocale.localeToLocalisedString(it).capitalize(it)) + if (vectorPreferences.developerMode()) { + subtitle(VectorLocale.localeToLocalisedStringInfo(it)) + } + clickListener { listener?.onLocaleClicked(it) } + } + } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewModel.kt index 61142b7af5..f0a4243439 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewModel.kt @@ -19,28 +19,30 @@ package im.vector.riotx.features.settings.locale import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.FragmentViewModelContext 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.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.features.settings.VectorLocale +import kotlinx.coroutines.Job +import java.util.Locale class LocalePickerViewModel @AssistedInject constructor( @Assisted initialState: LocalePickerViewState -) : VectorViewModel(initialState) { +) : VectorViewModel(initialState), + VectorLocale.Listener { @AssistedInject.Factory interface Factory { fun create(initialState: LocalePickerViewState): LocalePickerViewModel } + private var loadingJob: Job? = null + init { - setState { - copy( - locales = VectorLocale.supportedLocales - ) - } + loadingJob = VectorLocale.loadLocales(this) } companion object : MvRxViewModelFactory { @@ -65,4 +67,17 @@ class LocalePickerViewModel @AssistedInject constructor( VectorLocale.saveApplicationLocale(action.locale) _viewEvents.post(LocalePickerViewEvents.RestartActivity) } + + override fun onLoaded(locales: List) { + setState { + copy( + locales = Success(locales) + ) + } + } + + override fun onCleared() { + super.onCleared() + loadingJob?.cancel() + } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewState.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewState.kt index e32cdc632c..416350d827 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewState.kt @@ -16,11 +16,13 @@ package im.vector.riotx.features.settings.locale +import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized import im.vector.riotx.features.settings.VectorLocale import java.util.Locale data class LocalePickerViewState( val currentLocale: Locale = VectorLocale.applicationLocale, - val locales: List = emptyList() + val locales: Async> = Uninitialized ) : MvRxState diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index f2e5ea4edc..4022b1ef92 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2395,5 +2395,6 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Current language Other available languages + Loading available languages… \ No newline at end of file From 28f8d9500ee9d88f3280bdf39940d561b5147f0c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 18 May 2020 15:59:15 +0200 Subject: [PATCH 043/191] Better coroutine management --- .../riotx/features/settings/VectorLocale.kt | 21 ++++-------- .../settings/locale/LocalePickerViewModel.kt | 32 +++++++------------ 2 files changed, 18 insertions(+), 35 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt index efe4110680..a4ccfdba47 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt @@ -23,9 +23,6 @@ import androidx.core.content.edit import androidx.preference.PreferenceManager import im.vector.riotx.R import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber import java.util.Locale @@ -258,19 +255,13 @@ object VectorLocale { } } - fun loadLocales(listener: Listener): Job { - return GlobalScope.launch(Dispatchers.Main) { - if (supportedLocales.isEmpty()) { - // init the known locales in background - withContext(Dispatchers.IO) { - initApplicationLocales() - } + suspend fun getSupportedLocales(): List { + if (supportedLocales.isEmpty()) { + // init the known locales in background + withContext(Dispatchers.IO) { + initApplicationLocales() } - listener.onLoaded(supportedLocales) } - } - - interface Listener { - fun onLoaded(locales: List) + return supportedLocales } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewModel.kt index f0a4243439..e4cc64733c 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewModel.kt @@ -16,6 +16,7 @@ package im.vector.riotx.features.settings.locale +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory @@ -26,23 +27,27 @@ import com.squareup.inject.assisted.AssistedInject import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.features.settings.VectorLocale -import kotlinx.coroutines.Job -import java.util.Locale +import kotlinx.coroutines.launch class LocalePickerViewModel @AssistedInject constructor( @Assisted initialState: LocalePickerViewState -) : VectorViewModel(initialState), - VectorLocale.Listener { +) : VectorViewModel(initialState) { @AssistedInject.Factory interface Factory { fun create(initialState: LocalePickerViewState): LocalePickerViewModel } - private var loadingJob: Job? = null - init { - loadingJob = VectorLocale.loadLocales(this) + viewModelScope.launch { + val result = VectorLocale.getSupportedLocales() + + setState { + copy( + locales = Success(result) + ) + } + } } companion object : MvRxViewModelFactory { @@ -67,17 +72,4 @@ class LocalePickerViewModel @AssistedInject constructor( VectorLocale.saveApplicationLocale(action.locale) _viewEvents.post(LocalePickerViewEvents.RestartActivity) } - - override fun onLoaded(locales: List) { - setState { - copy( - locales = Success(locales) - ) - } - } - - override fun onCleared() { - super.onCleared() - loadingJob?.cancel() - } } From 05d1e64cb5de5cb6dc44cf132c273e295d216ed8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 May 2020 09:46:39 +0200 Subject: [PATCH 044/191] New rendering for redacted message --- vector/src/main/res/drawable/ic_trash_16.xml | 41 +++++++++++++++++++ vector/src/main/res/drawable/ic_trash_24.xml | 41 +++++++++++++++++++ .../main/res/drawable/redacted_background.xml | 5 --- .../res/layout/item_timeline_event_base.xml | 7 ++-- .../item_timeline_event_redacted_stub.xml | 13 ++++-- vector/src/main/res/values/strings.xml | 1 + 6 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 vector/src/main/res/drawable/ic_trash_16.xml create mode 100644 vector/src/main/res/drawable/ic_trash_24.xml delete mode 100644 vector/src/main/res/drawable/redacted_background.xml diff --git a/vector/src/main/res/drawable/ic_trash_16.xml b/vector/src/main/res/drawable/ic_trash_16.xml new file mode 100644 index 0000000000..ca6052b447 --- /dev/null +++ b/vector/src/main/res/drawable/ic_trash_16.xml @@ -0,0 +1,41 @@ + + + + + + + diff --git a/vector/src/main/res/drawable/ic_trash_24.xml b/vector/src/main/res/drawable/ic_trash_24.xml new file mode 100644 index 0000000000..266855d50c --- /dev/null +++ b/vector/src/main/res/drawable/ic_trash_24.xml @@ -0,0 +1,41 @@ + + + + + + + diff --git a/vector/src/main/res/drawable/redacted_background.xml b/vector/src/main/res/drawable/redacted_background.xml deleted file mode 100644 index f253a9eaf7..0000000000 --- a/vector/src/main/res/drawable/redacted_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index 3ae80424cc..7cc929306e 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -66,16 +66,15 @@ android:id="@+id/decorationSpace" android:layout_width="4dp" android:layout_height="8dp" - android:layout_toEndOf="@id/messageStartGuideline" - /> + android:layout_toEndOf="@id/messageStartGuideline" /> @@ -119,7 +118,7 @@ diff --git a/vector/src/main/res/layout/item_timeline_event_redacted_stub.xml b/vector/src/main/res/layout/item_timeline_event_redacted_stub.xml index 2f930577f0..acc60e6590 100644 --- a/vector/src/main/res/layout/item_timeline_event_redacted_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_redacted_stub.xml @@ -1,4 +1,11 @@ - \ No newline at end of file + android:layout_height="wrap_content" + android:drawableStart="@drawable/ic_trash_16" + android:drawablePadding="8dp" + android:gravity="center_vertical" + android:text="@string/event_redacted" + android:textColor="?riotx_text_primary_body_contrast" + android:textSize="14sp" + app:drawableTint="?riotx_text_primary_body_contrast" /> \ 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 4022b1ef92..f9c45e2236 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1563,6 +1563,7 @@ Why choose Riot.im? View Reactions Reactions + Message deleted Event deleted by user Event moderated by room admin Last edited by %1$s on %2$s From e542e4ba22a7ec70042762ff4d96ba982ff96c37 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 15 May 2020 10:16:01 +0200 Subject: [PATCH 045/191] Add a setting to hide redacted events (#951) --- CHANGES.md | 1 + .../session/room/timeline/TimelineSettings.kt | 4 ++++ .../database/query/UnsignedContent.kt | 21 +++++++++++++++++++ .../session/room/timeline/DefaultTimeline.kt | 12 ++++++++++- .../core/resources/UserPreferencesProvider.kt | 4 ++++ .../home/room/detail/RoomDetailViewModel.kt | 2 ++ .../features/settings/VectorPreferences.kt | 10 +++++++++ vector/src/main/res/values/strings.xml | 2 ++ .../res/xml/vector_settings_preferences.xml | 6 ++++++ 9 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/UnsignedContent.kt diff --git a/CHANGES.md b/CHANGES.md index 991078e704..574d3fe4be 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ Features ✨: Improvements 🙌: - Better connectivity lost indicator when airplane mode is on + - Add a setting to hide redacted events (#951) Bugfix 🐛: - Fix issues with FontScale switch (#69, #645) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt index 992cad41ca..154074b722 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt @@ -28,6 +28,10 @@ data class TimelineSettings( * A flag to filter edit events */ val filterEdits: Boolean = false, + /** + * A flag to filter redacted events + */ + val filterRedacted: Boolean = false, /** * A flag to filter by types. It should be used with [allowedTypes] field */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/UnsignedContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/UnsignedContent.kt new file mode 100644 index 0000000000..110a1e785c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/UnsignedContent.kt @@ -0,0 +1,21 @@ +/* + * 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.query + +internal object UnsignedContent { + internal const val REDACTED_TYPE = """{*"redacted_because":*}""" +} 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 f2bee734ce..894d7dc8e6 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 @@ -36,6 +36,7 @@ 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.FilterContent +import im.vector.matrix.android.internal.database.query.UnsignedContent 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 @@ -727,6 +728,9 @@ internal class DefaultTimeline( not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.EDIT_TYPE) not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.RESPONSE_TYPE) } + if (settings.filterRedacted) { + not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, UnsignedContent.REDACTED_TYPE) + } return this } @@ -737,13 +741,19 @@ internal class DefaultTimeline( } else { true } + if (!filterType) return@filter false + val filterEdits = if (settings.filterEdits && it.root.type == EventType.MESSAGE) { val messageContent = it.root.content.toModel() messageContent?.relatesTo?.type != RelationType.REPLACE } else { true } - filterType && filterEdits + if (!filterEdits) return@filter false + + val filterRedacted = settings.filterRedacted && it.root.isRedacted() + + filterRedacted } } diff --git a/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt b/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt index ac379a8f98..fa4b09ed4c 100644 --- a/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt +++ b/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt @@ -29,6 +29,10 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences: return vectorPreferences.showReadReceipts() } + fun shouldShowRedactedMessages(): Boolean { + return vectorPreferences.showRedactedMessages() + } + fun shouldShowLongClickOnRoomHelp(): Boolean { return vectorPreferences.shouldShowLongClickOnRoomHelp() } 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 d0dcac6ecc..4a767a178e 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 @@ -99,11 +99,13 @@ class RoomDetailViewModel @AssistedInject constructor( private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) { TimelineSettings(30, filterEdits = false, + filterRedacted = userPreferencesProvider.shouldShowRedactedMessages().not(), filterTypes = false, buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) } else { TimelineSettings(30, filterEdits = true, + filterRedacted = userPreferencesProvider.shouldShowRedactedMessages().not(), filterTypes = true, allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES, buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) 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 c995c4d986..1455e2f8d8 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 @@ -88,6 +88,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { private const val SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY = "SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY" private const val SETTINGS_12_24_TIMESTAMPS_KEY = "SETTINGS_12_24_TIMESTAMPS_KEY" private const val SETTINGS_SHOW_READ_RECEIPTS_KEY = "SETTINGS_SHOW_READ_RECEIPTS_KEY" + private const val SETTINGS_SHOW_REDACTED_KEY = "SETTINGS_SHOW_REDACTED_KEY" private const val SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY = "SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY" private const val SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY = "SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY" private const val SETTINGS_VIBRATE_ON_MENTION_KEY = "SETTINGS_VIBRATE_ON_MENTION_KEY" @@ -625,6 +626,15 @@ class VectorPreferences @Inject constructor(private val context: Context) { return defaultPrefs.getBoolean(SETTINGS_SHOW_READ_RECEIPTS_KEY, true) } + /** + * Tells if the redacted message should be shown + * + * @return true if the redacted should be shown + */ + fun showRedactedMessages(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_SHOW_REDACTED_KEY, true) + } + /** * Tells if the help on room list should be shown * diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index f9c45e2236..4dab1d3da0 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1564,6 +1564,8 @@ Why choose Riot.im? Reactions Message deleted + Show removed messages + Show a placeholder for removed messages Event deleted by user Event moderated by room admin Last edited by %1$s on %2$s diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index b1bd6ea5e6..d290b62825 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -70,6 +70,12 @@ android:summary="@string/settings_show_read_receipts_summary" android:title="@string/settings_show_read_receipts" /> + + Date: Mon, 18 May 2020 16:06:19 +0200 Subject: [PATCH 046/191] Ganfra's review: Handle filterRedacted in TimelineHiddenReadReceipts --- .../room/timeline/TimelineHiddenReadReceipts.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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 056f942211..ce0f5c1b14 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 @@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntit 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.FilterContent +import im.vector.matrix.android.internal.database.query.UnsignedContent import im.vector.matrix.android.internal.database.query.whereInRoom import io.realm.OrderedRealmCollectionChangeListener import io.realm.Realm @@ -149,16 +150,21 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu */ private fun RealmQuery.filterReceiptsWithSettings(): RealmQuery { beginGroup() + var needOr = false if (settings.filterTypes) { not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray()) - } - if (settings.filterTypes && settings.filterEdits) { - or() + needOr = true } if (settings.filterEdits) { + if (needOr) or() like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.EDIT_TYPE) or() like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.RESPONSE_TYPE) + needOr = true + } + if (settings.filterRedacted) { + if (needOr) or() + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.UNSIGNED_DATA}", UnsignedContent.REDACTED_TYPE) } endGroup() return this From d45653dbb3731be69ded43e09fa14fcb984e2f20 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 18 May 2020 16:15:05 +0200 Subject: [PATCH 047/191] Ganfra's review: Improve the filters declaration --- .../query/TimelineEventEntityQueries.kt | 4 ++-- ...ilterContent.kt => TimelineEventFilter.kt} | 20 +++++++++++++++--- .../database/query/UnsignedContent.kt | 21 ------------------- .../session/room/timeline/DefaultTimeline.kt | 9 ++++---- .../timeline/TimelineHiddenReadReceipts.kt | 9 ++++---- 5 files changed, 27 insertions(+), 36 deletions(-) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/{FilterContent.kt => TimelineEventFilter.kt} (54%) delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/UnsignedContent.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt index 5168d0728e..f798dbcf41 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt @@ -62,8 +62,8 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm, val liveEvents = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)?.timelineEvents?.where()?.filterTypes(filterTypes) if (filterContentRelation) { liveEvents - ?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.EDIT_TYPE) - ?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.RESPONSE_TYPE) + ?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT) + ?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE) } val query = if (includesSending && sendingTimelineEvents.findAll().isNotEmpty()) { sendingTimelineEvents diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventFilter.kt similarity index 54% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventFilter.kt index 6e89a28b7d..ea8122bc6d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventFilter.kt @@ -16,8 +16,22 @@ package im.vector.matrix.android.internal.database.query -internal object FilterContent { +/** + * Query strings used to filter the timeline events regarding the Json raw string of the Event + */ +internal object TimelineEventFilter { + /** + * To apply to Event.content + */ + internal object Content { + internal const val EDIT = """{*"m.relates_to"*"rel_type":*"m.replace"*}""" + internal const val RESPONSE = """{*"m.relates_to"*"rel_type":*"m.response"*}""" + } - internal const val EDIT_TYPE = """{*"m.relates_to"*"rel_type":*"m.replace"*}""" - internal const val RESPONSE_TYPE = """{*"m.relates_to"*"rel_type":*"m.response"*}""" + /** + * To apply to Event.unsigned + */ + internal object Unsigned { + internal const val REDACTED = """{*"redacted_because":*}""" + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/UnsignedContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/UnsignedContent.kt deleted file mode 100644 index 110a1e785c..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/UnsignedContent.kt +++ /dev/null @@ -1,21 +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.database.query - -internal object UnsignedContent { - internal const val REDACTED_TYPE = """{*"redacted_because":*}""" -} 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 894d7dc8e6..bf6b81b57c 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 @@ -35,8 +35,7 @@ import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryE 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.FilterContent -import im.vector.matrix.android.internal.database.query.UnsignedContent +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 @@ -725,11 +724,11 @@ internal class DefaultTimeline( `in`(TimelineEventEntityFields.ROOT.TYPE, settings.allowedTypes.toTypedArray()) } if (settings.filterEdits) { - not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.EDIT_TYPE) - not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.RESPONSE_TYPE) + not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT) + not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE) } if (settings.filterRedacted) { - not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, UnsignedContent.REDACTED_TYPE) + not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED) } return 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 ce0f5c1b14..72e99701cd 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 @@ -24,8 +24,7 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntit import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntityFields 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.FilterContent -import im.vector.matrix.android.internal.database.query.UnsignedContent +import im.vector.matrix.android.internal.database.query.TimelineEventFilter import im.vector.matrix.android.internal.database.query.whereInRoom import io.realm.OrderedRealmCollectionChangeListener import io.realm.Realm @@ -157,14 +156,14 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu } if (settings.filterEdits) { if (needOr) or() - like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.EDIT_TYPE) + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", TimelineEventFilter.Content.EDIT) or() - like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.RESPONSE_TYPE) + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", TimelineEventFilter.Content.RESPONSE) needOr = true } if (settings.filterRedacted) { if (needOr) or() - like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.UNSIGNED_DATA}", UnsignedContent.REDACTED_TYPE) + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.UNSIGNED_DATA}", TimelineEventFilter.Unsigned.REDACTED) } endGroup() return this From 25bbd7c526e7d44cb0077ef2653ab80eb3d7e989 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 6 May 2020 02:20:37 +0200 Subject: [PATCH 048/191] Identity - Create DB --- .../android/internal/di/DbQualifiers.kt | 4 ++ .../android/internal/session/SessionModule.kt | 20 +++++++ .../identity/db/IdentityRealmModule.kt | 28 ++++++++++ .../identity/db/IdentityServerEntity.kt | 30 ++++++++++ .../identity/db/IdentityServerQuery.kt | 56 +++++++++++++++++++ .../identity/db/IdentityServiceStore.kt | 30 ++++++++++ .../identity/db/RealmIdentityServerStore.kt | 53 ++++++++++++++++++ 7 files changed, 221 insertions(+) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityRealmModule.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityServerEntity.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityServerQuery.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityServiceStore.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/RealmIdentityServerStore.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/DbQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/DbQualifiers.kt index 3fdeb7eacc..fa007fdab6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/DbQualifiers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/DbQualifiers.kt @@ -29,3 +29,7 @@ annotation class SessionDatabase @Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class CryptoDatabase + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class IdentityDatabase 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 8bdfff062f..7fdaf5fe02 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,9 +39,11 @@ import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageSer import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecretStorageService import im.vector.matrix.android.internal.crypto.verification.VerificationMessageLiveObserver import im.vector.matrix.android.internal.database.LiveEntityObserver +import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory import im.vector.matrix.android.internal.di.Authenticated import im.vector.matrix.android.internal.di.DeviceId +import im.vector.matrix.android.internal.di.IdentityDatabase import im.vector.matrix.android.internal.di.SessionCacheDirectory import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionFilesDirectory @@ -60,6 +62,7 @@ import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor 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.db.IdentityRealmModule 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 @@ -170,6 +173,23 @@ internal abstract class SessionModule { .build() } + @JvmStatic + @Provides + @IdentityDatabase + @SessionScope + fun providesIdentityRealmConfiguration(realmKeysUtils: RealmKeysUtils, + @SessionFilesDirectory directory: File, + @UserMd5 userMd5: String): RealmConfiguration { + return RealmConfiguration.Builder() + .directory(directory) + .name("matrix-sdk-identity.realm") + .apply { + realmKeysUtils.configureEncryption(this, getKeyAlias(userMd5)) + } + .modules(IdentityRealmModule()) + .build() + } + @JvmStatic @Provides @SessionScope diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityRealmModule.kt new file mode 100644 index 0000000000..d97c6a5715 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityRealmModule.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.session.identity.db + +import io.realm.annotations.RealmModule + +/** + * Realm module for identity server classes + */ +@RealmModule(library = true, + classes = [ + IdentityServerEntity::class + ]) +internal class IdentityRealmModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityServerEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityServerEntity.kt new file mode 100644 index 0000000000..5545f5bccf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityServerEntity.kt @@ -0,0 +1,30 @@ +/* + * 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.db + +import io.realm.RealmList +import io.realm.RealmObject + +internal open class IdentityServerEntity( + var identityServerUrl: String? = null, + var token: String? = null, + var hashLookupPepper: String? = null, + var hashLookupAlgorithm: RealmList = RealmList() +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityServerQuery.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityServerQuery.kt new file mode 100644 index 0000000000..6bef6109c3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityServerQuery.kt @@ -0,0 +1,56 @@ +/* + * 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.db + +import io.realm.Realm +import io.realm.RealmList +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +/** + * Only one object can be stored at a time + */ +internal fun IdentityServerEntity.Companion.getOrCreate(realm: Realm): IdentityServerEntity { + return realm.where().findFirst() ?: realm.createObject() +} + +internal fun IdentityServerEntity.Companion.setUrl(realm: Realm, + url: String?) { + realm.where().findAll().deleteAllFromRealm() + + if (url != null) { + getOrCreate(realm).apply { + identityServerUrl = url + } + } +} + +internal fun IdentityServerEntity.Companion.setToken(realm: Realm, + newToken: String?) { + getOrCreate(realm).apply { + token = newToken + } +} + +internal fun IdentityServerEntity.Companion.setHashDetails(realm: Realm, + pepper: String, + algorithms: List) { + getOrCreate(realm).apply { + hashLookupPepper = pepper + hashLookupAlgorithm = RealmList().apply { addAll(algorithms) } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityServiceStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityServiceStore.kt new file mode 100644 index 0000000000..6933f0284f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityServiceStore.kt @@ -0,0 +1,30 @@ +/* + * 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.db + +import im.vector.matrix.android.internal.session.identity.model.IdentityHashDetailResponse + +internal interface IdentityServiceStore { + + fun get(): IdentityServerEntity + + fun setUrl(url: String?) + + fun setToken(token: String?) + + fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/RealmIdentityServerStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/RealmIdentityServerStore.kt new file mode 100644 index 0000000000..96194b3dcd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/RealmIdentityServerStore.kt @@ -0,0 +1,53 @@ +/* + * 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.db + +import im.vector.matrix.android.internal.di.IdentityDatabase +import im.vector.matrix.android.internal.session.identity.model.IdentityHashDetailResponse +import io.realm.Realm +import io.realm.RealmConfiguration +import javax.inject.Inject + +internal class RealmIdentityServerStore @Inject constructor( + @IdentityDatabase + private val realmConfiguration: RealmConfiguration +) : IdentityServiceStore { + + override fun get(): IdentityServerEntity { + return Realm.getInstance(realmConfiguration).use { + IdentityServerEntity.getOrCreate(it) + } + } + + override fun setUrl(url: String?) { + Realm.getInstance(realmConfiguration).use { + IdentityServerEntity.setUrl(it, url) + } + } + + override fun setToken(token: String?) { + Realm.getInstance(realmConfiguration).use { + IdentityServerEntity.setToken(it, token) + } + } + + override fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) { + Realm.getInstance(realmConfiguration).use { + IdentityServerEntity.setHashDetails(it, hashDetailResponse.pepper, hashDetailResponse.algorithms) + } + } +} From 9b7c2599a78f17391c2c6f8f83b40d7333cb1122 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 6 May 2020 02:21:09 +0200 Subject: [PATCH 049/191] Add withOlmUtility facility --- .../matrix/android/internal/crypto/tools/Tools.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tools/Tools.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tools/Tools.kt index c3d2c30079..5e406fdc4f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tools/Tools.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tools/Tools.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto.tools import org.matrix.olm.OlmPkDecryption import org.matrix.olm.OlmPkEncryption import org.matrix.olm.OlmPkSigning +import org.matrix.olm.OlmUtility fun withOlmEncryption(block: (OlmPkEncryption) -> T): T { val olmPkEncryption = OlmPkEncryption() @@ -46,3 +47,12 @@ fun withOlmSigning(block: (OlmPkSigning) -> T): T { olmPkSigning.releaseSigning() } } + +fun withOlmUtility(block: (OlmUtility) -> T): T { + val olmUtility = OlmUtility() + try { + return block(olmUtility) + } finally { + olmUtility.releaseUtility() + } +} From 6c9c3e5cb373b3546b0d8e19913f4a287753d042 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 6 May 2020 11:38:36 +0200 Subject: [PATCH 050/191] To merge with previous previous commit --- .../model/IdentityHashDetailResponse.kt | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityHashDetailResponse.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityHashDetailResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityHashDetailResponse.kt new file mode 100644 index 0000000000..17cb25fdaf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityHashDetailResponse.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.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://github.com/matrix-org/matrix-doc/blob/hs/hash-identity/proposals/2134-identity-hash-lookup.md + */ +@JsonClass(generateAdapter = true) +internal data class IdentityHashDetailResponse( + @Json(name = "lookup_pepper") + val pepper: String, + + /** + * "sha256" must be supported by client. "none" can be another possible value. + */ + @Json(name = "algorithms") + val algorithms: List +) From f489265ce725dbbf1387d28cf4fd58c558ff89d7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 6 May 2020 12:00:16 +0200 Subject: [PATCH 051/191] Create AccessTokenProvider --- .idea/dictionaries/bmarty.xml | 1 + .../internal/di/AccessTokenQualifiers.kt | 28 +++++++++++++++++++ .../network/AccessTokenInterceptor.kt | 13 ++------- .../network/token/AccessTokenProvider.kt | 21 ++++++++++++++ .../token/HomeserverAccessTokenProvider.kt | 26 +++++++++++++++++ .../android/internal/session/SessionModule.kt | 20 +++++++++++-- 6 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AccessTokenQualifiers.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/AccessTokenProvider.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/HomeserverAccessTokenProvider.kt diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index 1f93d1feee..26606fedd0 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -12,6 +12,7 @@ fdroid gplay hmac + homeserver ktlint linkified linkify diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AccessTokenQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AccessTokenQualifiers.kt new file mode 100644 index 0000000000..328cf54c23 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AccessTokenQualifiers.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.di + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class HomeserverAccessToken + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class IdentityServerAccessToken + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt index c802d4b63a..a15f660790 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt @@ -16,20 +16,16 @@ package im.vector.matrix.android.internal.network -import im.vector.matrix.android.internal.auth.SessionParamsStore -import im.vector.matrix.android.internal.di.SessionId +import im.vector.matrix.android.internal.network.token.AccessTokenProvider import okhttp3.Interceptor import okhttp3.Response -import javax.inject.Inject -internal class AccessTokenInterceptor @Inject constructor( - @SessionId private val sessionId: String, - private val sessionParamsStore: SessionParamsStore) : Interceptor { +internal class AccessTokenInterceptor(private val accessTokenProvider: AccessTokenProvider) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { var request = chain.request() - accessToken?.let { + accessTokenProvider.getToken()?.let { val newRequestBuilder = request.newBuilder() // Add the access token to all requests if it is set newRequestBuilder.addHeader(HttpHeaders.Authorization, "Bearer $it") @@ -38,7 +34,4 @@ internal class AccessTokenInterceptor @Inject constructor( return chain.proceed(request) } - - private val accessToken - get() = sessionParamsStore.get(sessionId)?.credentials?.accessToken } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/AccessTokenProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/AccessTokenProvider.kt new file mode 100644 index 0000000000..4d6da8a4bf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/AccessTokenProvider.kt @@ -0,0 +1,21 @@ +/* + * 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.network.token + +interface AccessTokenProvider { + fun getToken(): String? +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/HomeserverAccessTokenProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/HomeserverAccessTokenProvider.kt new file mode 100644 index 0000000000..3575eef900 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/HomeserverAccessTokenProvider.kt @@ -0,0 +1,26 @@ +/* + * 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.network.token + +import im.vector.matrix.android.internal.auth.SessionParamsStore + +internal class HomeserverAccessTokenProvider( + private val sessionId: String, + private val sessionParamsStore: SessionParamsStore +) : AccessTokenProvider { + override fun getToken() = sessionParamsStore.get(sessionId)?.credentials?.accessToken +} 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 7fdaf5fe02..46849cf3e1 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 @@ -36,6 +36,7 @@ import im.vector.matrix.android.api.session.accountdata.AccountDataService import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService import im.vector.matrix.android.api.session.securestorage.SecureStorageService import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService +import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecretStorageService import im.vector.matrix.android.internal.crypto.verification.VerificationMessageLiveObserver import im.vector.matrix.android.internal.database.LiveEntityObserver @@ -43,6 +44,7 @@ import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory import im.vector.matrix.android.internal.di.Authenticated import im.vector.matrix.android.internal.di.DeviceId +import im.vector.matrix.android.internal.di.HomeserverAccessToken import im.vector.matrix.android.internal.di.IdentityDatabase import im.vector.matrix.android.internal.di.SessionCacheDirectory import im.vector.matrix.android.internal.di.SessionDatabase @@ -60,6 +62,8 @@ import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.network.PreferredNetworkCallbackStrategy import im.vector.matrix.android.internal.network.RetrofitFactory 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.group.GroupSummaryUpdater import im.vector.matrix.android.internal.session.homeserver.DefaultHomeServerCapabilitiesService import im.vector.matrix.android.internal.session.identity.db.IdentityRealmModule @@ -195,14 +199,14 @@ internal abstract class SessionModule { @SessionScope @Authenticated fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient, - accessTokenInterceptor: AccessTokenInterceptor): OkHttpClient { + @Authenticated accessTokenProvider: AccessTokenProvider): OkHttpClient { return okHttpClient.newBuilder() .apply { // Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor val existingCurlInterceptors = interceptors().filterIsInstance() interceptors().removeAll(existingCurlInterceptors) - addInterceptor(accessTokenInterceptor) + addInterceptor(AccessTokenInterceptor(accessTokenProvider)) // Re add eventually the curl logging interceptors existingCurlInterceptors.forEach { @@ -212,6 +216,14 @@ internal abstract class SessionModule { .build() } + @JvmStatic + @Provides + @Authenticated + fun providesAccessTokenProvider(@SessionId sessionId: String, + sessionParamsStore: SessionParamsStore): AccessTokenProvider { + return HomeserverAccessTokenProvider(sessionId, sessionParamsStore) + } + @JvmStatic @Provides @SessionScope @@ -253,6 +265,10 @@ internal abstract class SessionModule { } } + @Binds + @HomeserverAccessToken + abstract fun bindAccessTokenProvider(provider: HomeserverAccessTokenProvider): AccessTokenProvider + @Binds abstract fun bindSession(session: DefaultSession): Session From ab6e7a3b8a72bbf931f36fa33abf298dea0a9a1b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 6 May 2020 14:22:59 +0200 Subject: [PATCH 052/191] Identity - WIP (compilation ok) --- .../matrix/android/api/failure/MatrixError.kt | 2 + .../matrix/android/api/session/Session.kt | 6 + .../session/identity/FoundThreePid.kt} | 16 +- .../api/session/identity/IdentityService.kt | 46 ++++ .../session/identity/IdentityServiceError.kt | 23 ++ .../identity/IdentityServiceListener.kt | 21 ++ .../android/api/session/identity/ThreePid.kt | 22 ++ .../attachments/MXEncryptedAttachments.kt | 2 +- .../android/internal/di/AuthQualifiers.kt | 5 + .../token/HomeserverAccessTokenProvider.kt | 6 +- .../internal/session/DefaultSession.kt | 8 +- .../internal/session/SessionComponent.kt | 2 + .../android/internal/session/SessionModule.kt | 12 +- .../session/identity/BulkLookupTask.kt | 98 ++++++++ .../identity/DefaultIdentityService.kt | 220 ++++++++++++++++++ .../internal/session/identity/IdentityAPI.kt | 73 ++++++ .../identity/IdentityAccessTokenProvider.kt | 27 +++ .../session/identity/IdentityApiProvider.kt | 26 +++ .../session/identity/IdentityAuthAPI.kt | 50 ++++ .../session/identity/IdentityModule.kt | 73 ++++++ .../session/identity/IdentityRegisterTask.kt | 39 ++++ .../identity/model/IdentityAccountResponse.kt | 27 +++ .../identity/model/IdentityLookUpV2Params.kt | 37 +++ .../model/IdentityLookUpV2Response.kt | 29 +++ .../model/IdentityRegisterResponse.kt | 29 +++ .../model/IdentityRequestOwnershipParams.kt | 31 +++ .../todelete/AccountDataDataSource.kt | 66 ++++++ .../identity/todelete/AccountDataMapper.kt | 36 +++ .../session/identity/todelete/LiveData.kt | 30 +++ .../sync/model/accountdata/UserAccountData.kt | 1 + .../accountdata/UserAccountDataIdentity.kt | 31 +++ .../accountdata/UpdateUserAccountDataTask.kt | 10 + 32 files changed, 1078 insertions(+), 26 deletions(-) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/{internal/di/AccessTokenQualifiers.kt => api/session/identity/FoundThreePid.kt} (69%) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityService.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceError.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceListener.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/ThreePid.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/BulkLookupTask.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAPI.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAccessTokenProvider.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityApiProvider.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAuthAPI.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityModule.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRegisterTask.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityAccountResponse.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpV2Params.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpV2Response.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRegisterResponse.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestOwnershipParams.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataDataSource.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataMapper.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/LiveData.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIdentity.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt index d7a6954fd5..2dcead7477 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt @@ -129,6 +129,8 @@ data class MatrixError( /** (Not documented yet) */ const val M_WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION" + const val M_TERMS_NOT_SIGNED = "M_TERMS_NOT_SIGNED" + // Possible value for "limit_type" const val LIMIT_TYPE_MAU = "monthly_active_user" } 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 1afeed922f..1143732b09 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 @@ -30,6 +30,7 @@ import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService +import im.vector.matrix.android.api.session.identity.IdentityService import im.vector.matrix.android.api.session.profile.ProfileService import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.api.session.room.RoomDirectoryService @@ -145,6 +146,11 @@ interface Session : */ fun cryptoService(): CryptoService + /** + * Returns the identity service associated with the session + */ + fun identityService(): IdentityService + /** * Add a listener to the session. * @param listener the listener to add. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AccessTokenQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/FoundThreePid.kt similarity index 69% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AccessTokenQualifiers.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/FoundThreePid.kt index 328cf54c23..5817699636 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AccessTokenQualifiers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/FoundThreePid.kt @@ -14,15 +14,9 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.di - -import javax.inject.Qualifier - -@Qualifier -@Retention(AnnotationRetention.RUNTIME) -annotation class HomeserverAccessToken - -@Qualifier -@Retention(AnnotationRetention.RUNTIME) -annotation class IdentityServerAccessToken +package im.vector.matrix.android.api.session.identity +data class FoundThreePid( + val threePid: ThreePid, + val matrixId: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityService.kt new file mode 100644 index 0000000000..0a844d0921 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityService.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.matrix.android.api.session.identity + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable + +/** + * Provides access to the identity server configuration and services identity server can provide + */ +interface IdentityService { + + /** + * Return the default identity server of the homeserver (using Wellknown request) + */ + fun getDefaultIdentityServer(): String? + + fun getCurrentIdentityServer(): String? + + fun setNewIdentityServer(url: String?, callback: MatrixCallback): Cancelable + + fun disconnect() + + fun bindThreePid() + + fun unbindThreePid() + + fun lookUp(threePids: List, callback: MatrixCallback>): Cancelable + + fun addListener(listener: IdentityServiceListener) + fun removeListener(listener: IdentityServiceListener) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceError.kt new file mode 100644 index 0000000000..7b05409f09 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceError.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.matrix.android.api.session.identity + +sealed class IdentityServiceError(cause: Throwable? = null) : Throwable(cause = cause) { + object NoIdentityServerConfigured : IdentityServiceError(null) + object TermsNotSignedException : IdentityServiceError(null) + object BulkLookupSha256NotSupported : IdentityServiceError(null) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceListener.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceListener.kt new file mode 100644 index 0000000000..13f622fe77 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceListener.kt @@ -0,0 +1,21 @@ +/* + * 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.identity + +interface IdentityServiceListener { + fun onIdentityServerChange() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/ThreePid.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/ThreePid.kt new file mode 100644 index 0000000000..7aabb2b9e0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/ThreePid.kt @@ -0,0 +1,22 @@ +/* + * 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.identity + +sealed class ThreePid(open val value: String) { + data class Email(val email: String) : ThreePid(email) + data class Msisdn(val msisdn: String) : ThreePid(msisdn) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt index e83895709e..19243f1a23 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt @@ -199,7 +199,7 @@ internal object MXEncryptedAttachments { .replace('_', '/') } - private fun base64ToBase64Url(base64: String): String { + internal fun base64ToBase64Url(base64: String): String { return base64.replace("\n".toRegex(), "") .replace("\\+".toRegex(), "-") .replace('/', '_') diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AuthQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AuthQualifiers.kt index 8ee27b3375..47f255cd40 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AuthQualifiers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AuthQualifiers.kt @@ -18,10 +18,15 @@ package im.vector.matrix.android.internal.di import javax.inject.Qualifier +// TODO Add internal ? @Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class Authenticated +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class AuthenticatedIdentity + @Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class Unauthenticated diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/HomeserverAccessTokenProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/HomeserverAccessTokenProvider.kt index 3575eef900..b570cb362e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/HomeserverAccessTokenProvider.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/HomeserverAccessTokenProvider.kt @@ -17,9 +17,11 @@ package im.vector.matrix.android.internal.network.token import im.vector.matrix.android.internal.auth.SessionParamsStore +import im.vector.matrix.android.internal.di.SessionId +import javax.inject.Inject -internal class HomeserverAccessTokenProvider( - private val sessionId: String, +internal class HomeserverAccessTokenProvider @Inject constructor( + @SessionId private val sessionId: String, private val sessionParamsStore: SessionParamsStore ) : AccessTokenProvider { override fun getToken() = sessionParamsStore.get(sessionId)?.credentials?.accessToken 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 b30c29a719..1dfade2388 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,6 +50,7 @@ import im.vector.matrix.android.internal.crypto.crosssigning.ShieldTrustUpdater import im.vector.matrix.android.internal.database.LiveEntityObserver 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.TimelineEventDecryptor import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.session.sync.job.SyncThread @@ -97,7 +98,8 @@ internal class DefaultSession @Inject constructor( private val _sharedSecretStorageService: Lazy, private val accountService: Lazy, private val timelineEventDecryptor: TimelineEventDecryptor, - private val shieldTrustUpdater: ShieldTrustUpdater) + private val shieldTrustUpdater: ShieldTrustUpdater, + private val defaultIdentityService: DefaultIdentityService) : Session, RoomService by roomService.get(), RoomDirectoryService by roomDirectoryService.get(), @@ -133,6 +135,7 @@ internal class DefaultSession @Inject constructor( eventBus.register(this) timelineEventDecryptor.start() shieldTrustUpdater.start() + defaultIdentityService.start() } override fun requireBackgroundSync() { @@ -175,6 +178,7 @@ internal class DefaultSession @Inject constructor( isOpen = false eventBus.unregister(this) shieldTrustUpdater.stop() + defaultIdentityService.stop() } override fun getSyncStateLive(): LiveData { @@ -218,6 +222,8 @@ internal class DefaultSession @Inject constructor( override fun cryptoService(): CryptoService = cryptoService.get() + override fun identityService() = defaultIdentityService + override fun addListener(listener: Session.Listener) { sessionListeners.addListener(listener) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt index 0ebfc1c4c5..a6e3d9493c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.session.filter.FilterModule import im.vector.matrix.android.internal.session.group.GetGroupDataWorker import im.vector.matrix.android.internal.session.group.GroupModule import im.vector.matrix.android.internal.session.homeserver.HomeServerCapabilitiesModule +import im.vector.matrix.android.internal.session.identity.IdentityModule import im.vector.matrix.android.internal.session.openid.OpenIdModule import im.vector.matrix.android.internal.session.profile.ProfileModule import im.vector.matrix.android.internal.session.pushers.AddHttpPusherWorker @@ -72,6 +73,7 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers CryptoModule::class, PushersModule::class, OpenIdModule::class, + IdentityModule::class, AccountDataModule::class, ProfileModule::class, SessionAssistedInjectModule::class, 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 46849cf3e1..3f9c5a2364 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 @@ -36,7 +36,6 @@ import im.vector.matrix.android.api.session.accountdata.AccountDataService import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService import im.vector.matrix.android.api.session.securestorage.SecureStorageService import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService -import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecretStorageService import im.vector.matrix.android.internal.crypto.verification.VerificationMessageLiveObserver import im.vector.matrix.android.internal.database.LiveEntityObserver @@ -44,7 +43,6 @@ import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory import im.vector.matrix.android.internal.di.Authenticated import im.vector.matrix.android.internal.di.DeviceId -import im.vector.matrix.android.internal.di.HomeserverAccessToken import im.vector.matrix.android.internal.di.IdentityDatabase import im.vector.matrix.android.internal.di.SessionCacheDirectory import im.vector.matrix.android.internal.di.SessionDatabase @@ -216,14 +214,6 @@ internal abstract class SessionModule { .build() } - @JvmStatic - @Provides - @Authenticated - fun providesAccessTokenProvider(@SessionId sessionId: String, - sessionParamsStore: SessionParamsStore): AccessTokenProvider { - return HomeserverAccessTokenProvider(sessionId, sessionParamsStore) - } - @JvmStatic @Provides @SessionScope @@ -266,7 +256,7 @@ internal abstract class SessionModule { } @Binds - @HomeserverAccessToken + @Authenticated abstract fun bindAccessTokenProvider(provider: HomeserverAccessTokenProvider): AccessTokenProvider @Binds diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/BulkLookupTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/BulkLookupTask.kt new file mode 100644 index 0000000000..9d701ad7c5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/BulkLookupTask.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.session.identity + +import im.vector.matrix.android.api.session.identity.FoundThreePid +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments.base64ToBase64Url +import im.vector.matrix.android.internal.crypto.tools.withOlmUtility +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.identity.db.IdentityServiceStore +import im.vector.matrix.android.internal.session.identity.model.IdentityHashDetailResponse +import im.vector.matrix.android.internal.session.identity.model.IdentityLookUpV2Params +import im.vector.matrix.android.internal.session.identity.model.IdentityLookUpV2Response +import im.vector.matrix.android.internal.task.Task +import java.util.Locale +import javax.inject.Inject + +internal interface BulkLookupTask : Task> { + data class Params( + val threePids: List + ) +} + +internal class DefaultBulkLookupTask @Inject constructor( + private val identityApiProvider: IdentityApiProvider, + private val identityServiceStore: IdentityServiceStore +) : BulkLookupTask { + + override suspend fun execute(params: BulkLookupTask.Params): List { + val identityAPI = identityApiProvider.identityApi ?: throw IdentityServiceError.NoIdentityServerConfigured + val entity = identityServiceStore.get() + val pepper = entity.hashLookupPepper + val hashDetailResponse = if (pepper == null) { + // We need to fetch the hash details first + executeRequest(null) { + apiCall = identityAPI.hashDetails() + } + .also { identityServiceStore.setHashDetails(it) } + } else { + IdentityHashDetailResponse(pepper, entity.hashLookupAlgorithm.toList()) + } + + if (hashDetailResponse.algorithms.contains("sha256").not()) { + // TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment do not do it + throw IdentityServiceError.BulkLookupSha256NotSupported + } + + val hashedAddresses = withOlmUtility { olmUtility -> + params.threePids.map { threePid -> + base64ToBase64Url( + olmUtility.sha256(threePid.value.toLowerCase(Locale.ROOT) + + " " + threePid.toMedium() + " " + hashDetailResponse.pepper) + ) + } + } + + val identityLookUpV2Response = executeRequest(null) { + apiCall = identityAPI.bulkLookupV2(IdentityLookUpV2Params( + hashedAddresses, + "sha256", + hashDetailResponse.pepper + )) + } + + // TODO Catch invalid hash pepper and retry + + // Convert back to List + return handleSuccess(params.threePids, hashedAddresses, identityLookUpV2Response) + } + + private fun handleSuccess(threePids: List, hashedAddresses: List, identityLookUpV2Response: IdentityLookUpV2Response): List { + return identityLookUpV2Response.mappings.keys.map { hashedAddress -> + FoundThreePid(threePids[hashedAddresses.indexOf(hashedAddress)], identityLookUpV2Response.mappings[hashedAddress] ?: error("")) + } + } + + private fun ThreePid.toMedium(): String { + return when (this) { + is ThreePid.Email -> "email" + is ThreePid.Msisdn -> "msisdn" + } + } +} 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 new file mode 100644 index 0000000000..f02e5446d8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt @@ -0,0 +1,220 @@ +/* + * 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 androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import dagger.Lazy +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.MatrixError +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.identity.FoundThreePid +import im.vector.matrix.android.api.session.identity.IdentityService +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.api.session.identity.IdentityServiceListener +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.di.AuthenticatedIdentity +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.identity.db.IdentityServiceStore +import im.vector.matrix.android.internal.session.identity.todelete.AccountDataDataSource +import im.vector.matrix.android.internal.session.identity.todelete.observeNotNull +import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask +import im.vector.matrix.android.internal.session.sync.model.accountdata.IdentityContent +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataIdentity +import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.launchToCallback +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.GlobalScope +import okhttp3.OkHttpClient +import timber.log.Timber +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +@SessionScope +internal class DefaultIdentityService @Inject constructor( + private val identityServiceStore: IdentityServiceStore, + private val openIdTokenTask: GetOpenIdTokenTask, + private val bulkLookupTask: BulkLookupTask, + private val identityRegisterTask: IdentityRegisterTask, + private val taskExecutor: TaskExecutor, + @Unauthenticated + private val unauthenticatedOkHttpClient: Lazy, + @AuthenticatedIdentity + private val okHttpClient: Lazy, + private val retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val identityApiProvider: IdentityApiProvider, + private val accountDataDataSource: AccountDataDataSource +) : IdentityService { + + private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry } + private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(lifecycleOwner) + + private val listeners = mutableSetOf() + + fun start() { + lifecycleRegistry.currentState = Lifecycle.State.STARTED + // Observe the account data change + accountDataDataSource + .getLiveAccountDataEvent(UserAccountData.TYPE_IDENTITY) + .observeNotNull(lifecycleOwner) { + val identityServerContent = it.getOrNull()?.content?.toModel() + if (identityServerContent != null) { + notifyIdentityServerUrlChange(identityServerContent.content?.baseUrl) + } + // TODO Handle the case where the account data is deleted? + } + } + + private fun notifyIdentityServerUrlChange(baseUrl: String?) { + // This is maybe not a real change (local echo of account data we are just setting + if (identityServiceStore.get().identityServerUrl == baseUrl) { + Timber.d("Local echo of identity server url change") + } else { + // Url has changed, we have to reset our store, update internal configuration and notify listeners + identityServiceStore.setUrl(baseUrl) + updateIdentityAPI(baseUrl) + listeners.toList().forEach { it.onIdentityServerChange() } + } + } + + fun stop() { + lifecycleRegistry.currentState = Lifecycle.State.DESTROYED + } + + override fun getDefaultIdentityServer(): String? { + TODO("Not yet implemented") + } + + override fun getCurrentIdentityServer(): String? { + return identityServiceStore.get().identityServerUrl + } + + override fun disconnect() { + TODO("Not yet implemented") + } + + override fun setNewIdentityServer(url: String?, callback: MatrixCallback): Cancelable { + return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + val current = getCurrentIdentityServer() + when (url) { + current -> + // Nothing to do + Timber.d("Same URL, nothing to do") + null -> { + // TODO + // Disconnect previous one if any + identityServiceStore.setUrl(null) + updateAccountData(null) + } + else -> { + // TODO: check first that it is a valid identity server + updateAccountData(url) + } + } + } + } + + private suspend fun updateAccountData(url: String?) { + updateUserAccountDataTask.execute(UpdateUserAccountDataTask.IdentityParams( + identityContent = IdentityContent(baseUrl = url) + )) + } + + override fun bindThreePid() { + TODO("Not yet implemented") + } + + override fun unbindThreePid() { + TODO("Not yet implemented") + } + + override fun lookUp(threePids: List, callback: MatrixCallback>): Cancelable { + return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + lookUpInternal(true, threePids) + } + } + + private suspend fun lookUpInternal(firstTime: Boolean, threePids: List): List { + ensureToken() + + return try { + bulkLookupTask.execute(BulkLookupTask.Params(threePids)) + } catch (throwable: Throwable) { + // Refresh token? + when { + throwable.isInvalidToken() && firstTime -> { + identityServiceStore.setToken(null) + lookUpInternal(false, threePids) + } + throwable.isTermsNotSigned() -> throw IdentityServiceError.TermsNotSignedException + else -> throw throwable + } + } + } + + private suspend fun ensureToken() { + val entity = identityServiceStore.get() + val url = entity.identityServerUrl ?: throw IdentityServiceError.NoIdentityServerConfigured + + if (entity.token == null) { + // Try to get a token + val openIdToken = openIdTokenTask.execute(Unit) + + val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java) + val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken)) + + identityServiceStore.setToken(token.token) + } + } + + override fun addListener(listener: IdentityServiceListener) { + listeners.add(listener) + } + + override fun removeListener(listener: IdentityServiceListener) { + listeners.remove(listener) + } + + private fun updateIdentityAPI(url: String?) { + if (url == null) { + identityApiProvider.identityApi = null + } else { + val retrofit = retrofitFactory.create(okHttpClient, url) + identityApiProvider.identityApi = retrofit.create(IdentityAPI::class.java) + } + } +} + +private fun Throwable.isInvalidToken(): Boolean { + return this is Failure.ServerError + && this.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ +} + +private fun Throwable.isTermsNotSigned(): Boolean { + return this is Failure.ServerError + && httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */ + && error.code == MatrixError.M_TERMS_NOT_SIGNED +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAPI.kt new file mode 100644 index 0000000000..5f65db3554 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAPI.kt @@ -0,0 +1,73 @@ +/* + * 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 im.vector.matrix.android.internal.auth.registration.SuccessResult +import im.vector.matrix.android.internal.network.NetworkConstants +import im.vector.matrix.android.internal.session.identity.model.IdentityAccountResponse +import im.vector.matrix.android.internal.session.identity.model.IdentityHashDetailResponse +import im.vector.matrix.android.internal.session.identity.model.IdentityLookUpV2Params +import im.vector.matrix.android.internal.session.identity.model.IdentityLookUpV2Response +import im.vector.matrix.android.internal.session.identity.model.IdentityRequestOwnershipParams +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path + +/** + * Ref: https://matrix.org/docs/spec/identity_service/latest + * This contain the requests which need an identity server token + */ +internal interface IdentityAPI { + /** + * Gets information about what user owns the access token used in the request. + * Will return a 403 for when terms are not signed + */ + @GET(NetworkConstants.URI_IDENTITY_PATH_V2 + "account") + fun getAccount(): Call + + /** + * Logs out the access token, preventing it from being used to authenticate future requests to the server. + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "logout") + fun logout(): Call + + /** + * Request the hash detail to request a bunch of 3PIDs + */ + @GET(NetworkConstants.URI_IDENTITY_PATH_V2 + "hash_details") + fun hashDetails(): Call + + /** + * Request a bunch of 3PIDs + * + * @param body the body request + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "lookup") + fun bulkLookupV2(@Body body: IdentityLookUpV2Params): Call + + /** + * Request the ownership validation of an email address or a phone number previously set + * by [ProfileApi.requestEmailValidation] + * + * @param medium the medium of the 3pid + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/{medium}/submitToken") + fun requestOwnershipValidationV2(@Path("medium") medium: String?, + @Body body: IdentityRequestOwnershipParams): Call +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAccessTokenProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAccessTokenProvider.kt new file mode 100644 index 0000000000..2fe2ca033b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAccessTokenProvider.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.identity + +import im.vector.matrix.android.internal.network.token.AccessTokenProvider +import im.vector.matrix.android.internal.session.identity.db.IdentityServiceStore +import javax.inject.Inject + +internal class IdentityAccessTokenProvider @Inject constructor( + private val identityServiceStore: IdentityServiceStore +) : AccessTokenProvider { + override fun getToken() = identityServiceStore.get().token +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityApiProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityApiProvider.kt new file mode 100644 index 0000000000..3262a56398 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityApiProvider.kt @@ -0,0 +1,26 @@ +/* + * 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 im.vector.matrix.android.internal.session.SessionScope +import javax.inject.Inject + +@SessionScope +internal class IdentityApiProvider @Inject constructor() { + + var identityApi: IdentityAPI? = null +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAuthAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAuthAPI.kt new file mode 100644 index 0000000000..7b61f2522e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAuthAPI.kt @@ -0,0 +1,50 @@ +/* + * 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 im.vector.matrix.android.internal.network.NetworkConstants +import im.vector.matrix.android.internal.session.identity.model.IdentityRegisterResponse +import im.vector.matrix.android.internal.session.openid.RequestOpenIdTokenResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST + +/** + * Ref: https://matrix.org/docs/spec/identity_service/latest + * This contain the requests which do not need an identity server token + */ +internal interface IdentityAuthAPI { + + /** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + * Simple ping call to check if server alive + * + * Ref: https://matrix.org/docs/spec/identity_service/unstable#status-check + * + * @return 200 in case of success + */ + @GET(NetworkConstants.URI_API_PREFIX_IDENTITY) + fun ping(): Call + + /** + * Exchanges an OpenID token from the homeserver for an access token to access the identity server. + * The request body is the same as the values returned by /openid/request_token in the Client-Server API. + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "account/register") + fun register(@Body openIdToken: RequestOpenIdTokenResponse): Call +} 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 new file mode 100644 index 0000000000..b320bac28d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityModule.kt @@ -0,0 +1,73 @@ +/* + * 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.Binds +import dagger.Module +import dagger.Provides +import im.vector.matrix.android.internal.di.AuthenticatedIdentity +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.network.AccessTokenInterceptor +import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor +import im.vector.matrix.android.internal.network.token.AccessTokenProvider +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.identity.db.IdentityServiceStore +import im.vector.matrix.android.internal.session.identity.db.RealmIdentityServerStore +import okhttp3.OkHttpClient + +@Module +internal abstract class IdentityModule { + + @Module + companion object { + @JvmStatic + @Provides + @SessionScope + @AuthenticatedIdentity + fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient, + @AuthenticatedIdentity accessTokenProvider: AccessTokenProvider): OkHttpClient { + // TODO Create an helper because there is code duplication + return okHttpClient.newBuilder() + .apply { + // Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor + val existingCurlInterceptors = interceptors().filterIsInstance() + interceptors().removeAll(existingCurlInterceptors) + + addInterceptor(AccessTokenInterceptor(accessTokenProvider)) + + // Re add eventually the curl logging interceptors + existingCurlInterceptors.forEach { + addInterceptor(it) + } + } + .build() + } + } + + @Binds + @AuthenticatedIdentity + abstract fun bindAccessTokenProvider(provider: IdentityAccessTokenProvider): AccessTokenProvider + + @Binds + abstract fun bindIdentityServiceStore(store: RealmIdentityServerStore): IdentityServiceStore + + @Binds + abstract fun bindIdentityRegisterTask(task: DefaultIdentityRegisterTask): IdentityRegisterTask + + @Binds + abstract fun bindBulkLookupTask(task: DefaultBulkLookupTask): BulkLookupTask +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRegisterTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRegisterTask.kt new file mode 100644 index 0000000000..c72e364ef8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRegisterTask.kt @@ -0,0 +1,39 @@ +/* + * 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 im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.identity.model.IdentityRegisterResponse +import im.vector.matrix.android.internal.session.openid.RequestOpenIdTokenResponse +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject + +internal interface IdentityRegisterTask : Task { + data class Params( + val identityAuthAPI: IdentityAuthAPI, + val openIdTokenResponse: RequestOpenIdTokenResponse + ) +} + +internal class DefaultIdentityRegisterTask @Inject constructor() : IdentityRegisterTask { + + override suspend fun execute(params: IdentityRegisterTask.Params): IdentityRegisterResponse { + return executeRequest(null) { + apiCall = params.identityAuthAPI.register(params.openIdTokenResponse) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityAccountResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityAccountResponse.kt new file mode 100644 index 0000000000..d24fc77274 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityAccountResponse.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.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityAccountResponse( + @Json(name = "user_id") + val userId: String +) + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpV2Params.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpV2Params.kt new file mode 100644 index 0000000000..acdad92758 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpV2Params.kt @@ -0,0 +1,37 @@ +/* + * 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.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://github.com/matrix-org/matrix-doc/blob/hs/hash-identity/proposals/2134-identity-hash-lookup.md + */ +@JsonClass(generateAdapter = true) +internal data class IdentityLookUpV2Params( + @Json(name = "addresses") + val hashedAddresses: List, + + @JvmField + @Json(name = "algorithm") + val algorithm: String, + + @JvmField + @Json(name = "pepper") + val pepper: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpV2Response.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpV2Response.kt new file mode 100644 index 0000000000..825e6c0017 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpV2Response.kt @@ -0,0 +1,29 @@ +/* + * 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.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://github.com/matrix-org/matrix-doc/blob/hs/hash-identity/proposals/2134-identity-hash-lookup.md + */ +@JsonClass(generateAdapter = true) +internal data class IdentityLookUpV2Response( + @Json(name = "mappings") + val mappings: Map +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRegisterResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRegisterResponse.kt new file mode 100644 index 0000000000..b99b0f0d53 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRegisterResponse.kt @@ -0,0 +1,29 @@ +/* + * 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.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityRegisterResponse( + /** + * A token which can be used to authenticate future requests to the identity server. + */ + @Json(name = "token") + val token: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestOwnershipParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestOwnershipParams.kt new file mode 100644 index 0000000000..d3f4778d7f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestOwnershipParams.kt @@ -0,0 +1,31 @@ +/* + * 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.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityRequestOwnershipParams( + @Json(name = "client_secret") + var clientSecret: String? = null, + + @Json(name = "sid") + var sid: String? = null, + + @Json(name = "token") + var token: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataDataSource.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataDataSource.kt new file mode 100644 index 0000000000..37b0da9101 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataDataSource.kt @@ -0,0 +1,66 @@ +/* + * 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.todelete + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.api.util.toOptional +import im.vector.matrix.android.internal.database.model.UserAccountDataEntity +import im.vector.matrix.android.internal.database.model.UserAccountDataEntityFields +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent +import io.realm.Realm +import io.realm.RealmQuery +import javax.inject.Inject + +// There will be a duplicated class when Integration manager will be merged, so delete this one +internal class AccountDataDataSource @Inject constructor(private val monarchy: Monarchy, + private val accountDataMapper: AccountDataMapper) { + + fun getAccountDataEvent(type: String): UserAccountDataEvent? { + return getAccountDataEvents(setOf(type)).firstOrNull() + } + + fun getLiveAccountDataEvent(type: String): LiveData> { + return Transformations.map(getLiveAccountDataEvents(setOf(type))) { + it.firstOrNull()?.toOptional() + } + } + + fun getAccountDataEvents(types: Set): List { + return monarchy.fetchAllMappedSync( + { accountDataEventsQuery(it, types) }, + accountDataMapper::map + ) + } + + fun getLiveAccountDataEvents(types: Set): LiveData> { + return monarchy.findAllMappedWithChanges( + { accountDataEventsQuery(it, types) }, + accountDataMapper::map + ) + } + + private fun accountDataEventsQuery(realm: Realm, types: Set): RealmQuery { + val query = realm.where(UserAccountDataEntity::class.java) + if (types.isNotEmpty()) { + query.`in`(UserAccountDataEntityFields.TYPE, types.toTypedArray()) + } + return query + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataMapper.kt new file mode 100644 index 0000000000..4627911b72 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataMapper.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.matrix.android.internal.session.identity.todelete + +import com.squareup.moshi.Moshi +import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE +import im.vector.matrix.android.internal.database.model.UserAccountDataEntity +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent +import javax.inject.Inject + +// There will be a duplicated class when Integration manager will be merged, so delete this one +internal class AccountDataMapper @Inject constructor(moshi: Moshi) { + + private val adapter = moshi.adapter>(JSON_DICT_PARAMETERIZED_TYPE) + + fun map(entity: UserAccountDataEntity): UserAccountDataEvent { + return UserAccountDataEvent( + type = entity.type ?: "", + content = entity.contentStr?.let { adapter.fromJson(it) } ?: emptyMap() + ) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/LiveData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/LiveData.kt new file mode 100644 index 0000000000..f84756fa86 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/LiveData.kt @@ -0,0 +1,30 @@ +/* + * 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.todelete + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer + +// There will be a duplicated class when Integration manager will be merged, so delete this one +inline fun LiveData.observeK(owner: LifecycleOwner, crossinline observer: (T?) -> Unit) { + this.observe(owner, Observer { observer(it) }) +} + +inline fun LiveData.observeNotNull(owner: LifecycleOwner, crossinline observer: (T) -> Unit) { + this.observe(owner, Observer { it?.run(observer) }) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt index c508413665..e7a7939540 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt @@ -30,5 +30,6 @@ abstract class UserAccountData : AccountDataContent { const val TYPE_PREVIEW_URLS = "org.matrix.preview_urls" const val TYPE_WIDGETS = "m.widgets" const val TYPE_PUSH_RULES = "m.push_rules" + const val TYPE_IDENTITY = "m.identity" } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIdentity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIdentity.kt new file mode 100644 index 0000000000..4777daf591 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIdentity.kt @@ -0,0 +1,31 @@ +/* + * 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.sync.model.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UserAccountDataIdentity( + @Json(name = "type") override val type: String = TYPE_IDENTITY, + @Json(name = "content") val content: IdentityContent? = null +) : UserAccountData() + +@JsonClass(generateAdapter = true) +internal data class IdentityContent( + @Json(name = "base_url") val baseUrl: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt index beb3a0fcc0..7daeef699e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.user.accountdata import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.sync.model.accountdata.BreadcrumbsContent +import im.vector.matrix.android.internal.session.sync.model.accountdata.IdentityContent import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData import im.vector.matrix.android.internal.task.Task import org.greenrobot.eventbus.EventBus @@ -31,6 +32,15 @@ internal interface UpdateUserAccountDataTask : Task> From 0199cf9a037e224924c1cba5a3083f8523c8d37a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 6 May 2020 14:49:47 +0200 Subject: [PATCH 053/191] Identity - Fix issue with Realm --- .../internal/session/identity/BulkLookupTask.kt | 2 +- .../session/identity/DefaultIdentityService.kt | 6 +++--- .../identity/IdentityAccessTokenProvider.kt | 2 +- .../session/identity/db/IdentityServerQuery.kt | 6 +++++- .../session/identity/db/IdentityServiceStore.kt | 2 +- .../identity/db/RealmIdentityServerStore.kt | 16 +++++++++++----- 6 files changed, 22 insertions(+), 12 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/BulkLookupTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/BulkLookupTask.kt index 9d701ad7c5..0524e704f3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/BulkLookupTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/BulkLookupTask.kt @@ -43,7 +43,7 @@ internal class DefaultBulkLookupTask @Inject constructor( override suspend fun execute(params: BulkLookupTask.Params): List { val identityAPI = identityApiProvider.identityApi ?: throw IdentityServiceError.NoIdentityServerConfigured - val entity = identityServiceStore.get() + val entity = identityServiceStore.get() ?: throw IdentityServiceError.NoIdentityServerConfigured val pepper = entity.hashLookupPepper val hashDetailResponse = if (pepper == null) { // We need to fetch the hash details first 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 f02e5446d8..01f2c466ac 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 @@ -90,7 +90,7 @@ internal class DefaultIdentityService @Inject constructor( private fun notifyIdentityServerUrlChange(baseUrl: String?) { // This is maybe not a real change (local echo of account data we are just setting - if (identityServiceStore.get().identityServerUrl == baseUrl) { + if (identityServiceStore.get()?.identityServerUrl == baseUrl) { Timber.d("Local echo of identity server url change") } else { // Url has changed, we have to reset our store, update internal configuration and notify listeners @@ -109,7 +109,7 @@ internal class DefaultIdentityService @Inject constructor( } override fun getCurrentIdentityServer(): String? { - return identityServiceStore.get().identityServerUrl + return identityServiceStore.get()?.identityServerUrl } override fun disconnect() { @@ -176,7 +176,7 @@ internal class DefaultIdentityService @Inject constructor( } private suspend fun ensureToken() { - val entity = identityServiceStore.get() + val entity = identityServiceStore.get() ?: throw IdentityServiceError.NoIdentityServerConfigured val url = entity.identityServerUrl ?: throw IdentityServiceError.NoIdentityServerConfigured if (entity.token == null) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAccessTokenProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAccessTokenProvider.kt index 2fe2ca033b..1a7c724892 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAccessTokenProvider.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAccessTokenProvider.kt @@ -23,5 +23,5 @@ import javax.inject.Inject internal class IdentityAccessTokenProvider @Inject constructor( private val identityServiceStore: IdentityServiceStore ) : AccessTokenProvider { - override fun getToken() = identityServiceStore.get().token + override fun getToken() = identityServiceStore.get()?.token } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityServerQuery.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityServerQuery.kt index 6bef6109c3..7b7bec13c0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityServerQuery.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityServerQuery.kt @@ -24,8 +24,12 @@ import io.realm.kotlin.where /** * Only one object can be stored at a time */ +internal fun IdentityServerEntity.Companion.get(realm: Realm): IdentityServerEntity? { + return realm.where().findFirst() +} + internal fun IdentityServerEntity.Companion.getOrCreate(realm: Realm): IdentityServerEntity { - return realm.where().findFirst() ?: realm.createObject() + return get(realm) ?: realm.createObject() } internal fun IdentityServerEntity.Companion.setUrl(realm: Realm, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityServiceStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityServiceStore.kt index 6933f0284f..af44766631 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityServiceStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityServiceStore.kt @@ -20,7 +20,7 @@ import im.vector.matrix.android.internal.session.identity.model.IdentityHashDeta internal interface IdentityServiceStore { - fun get(): IdentityServerEntity + fun get(): IdentityServerEntity? fun setUrl(url: String?) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/RealmIdentityServerStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/RealmIdentityServerStore.kt index 96194b3dcd..2c7ffbd75f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/RealmIdentityServerStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/RealmIdentityServerStore.kt @@ -27,27 +27,33 @@ internal class RealmIdentityServerStore @Inject constructor( private val realmConfiguration: RealmConfiguration ) : IdentityServiceStore { - override fun get(): IdentityServerEntity { + override fun get(): IdentityServerEntity? { return Realm.getInstance(realmConfiguration).use { - IdentityServerEntity.getOrCreate(it) + IdentityServerEntity.get(it) } } override fun setUrl(url: String?) { Realm.getInstance(realmConfiguration).use { - IdentityServerEntity.setUrl(it, url) + it.executeTransaction { realm -> + IdentityServerEntity.setUrl(realm, url) + } } } override fun setToken(token: String?) { Realm.getInstance(realmConfiguration).use { - IdentityServerEntity.setToken(it, token) + it.executeTransaction { realm -> + IdentityServerEntity.setToken(realm, token) + } } } override fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) { Realm.getInstance(realmConfiguration).use { - IdentityServerEntity.setHashDetails(it, hashDetailResponse.pepper, hashDetailResponse.algorithms) + it.executeTransaction { realm -> + IdentityServerEntity.setHashDetails(realm, hashDetailResponse.pepper, hashDetailResponse.algorithms) + } } } } From 784918350b21f72bc44cc48efc2d63f6333441a6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 6 May 2020 23:34:26 +0200 Subject: [PATCH 054/191] Identity: import UI/UX From Riot and adapt to RiotX architecture --- .../api/session/identity/IdentityService.kt | 8 +- .../android/api/session/identity/ThreePid.kt | 2 +- .../identity/DefaultIdentityService.kt | 68 ++- .../sync/model/accountdata/UserAccountData.kt | 2 +- .../accountdata/UserAccountDataIdentity.kt | 2 +- .../accountdata/UpdateUserAccountDataTask.kt | 2 +- .../im/vector/riotx/core/di/FragmentModule.kt | 12 + .../vector/riotx/core/di/ViewModelModule.kt | 6 + .../discovery/DiscoverySettingsController.kt | 303 +++++++++++ .../discovery/DiscoverySettingsFragment.kt | 199 +++++++ .../discovery/DiscoverySettingsViewModel.kt | 503 ++++++++++++++++++ .../discovery/DiscoverySharedViewModel.kt | 35 ++ .../features/discovery/SettingsButtonItem.kt | 71 +++ .../features/discovery/SettingsImageItem.kt | 70 +++ .../features/discovery/SettingsInfoItem.kt | 70 +++ .../riotx/features/discovery/SettingsItem.kt | 83 +++ .../features/discovery/SettingsItemText.kt | 76 +++ .../features/discovery/SettingsLoadingItem.kt | 44 ++ .../discovery/SettingsSectionTitle.kt | 50 ++ .../discovery/SettingsTextButtonItem.kt | 173 ++++++ .../change/SetIdentityServerFragment.kt | 170 ++++++ .../change/SetIdentityServerViewModel.kt | 171 ++++++ .../settings/VectorSettingsGeneralFragment.kt | 3 +- .../ic_notification_privacy_warning.png | Bin 0 -> 2165 bytes .../layout/fragment_set_identity_server.xml | 52 ++ .../main/res/layout/item_settings_button.xml | 15 + .../item_settings_button_single_line.xml | 86 +++ .../res/layout/item_settings_edit_text.xml | 56 ++ .../res/layout/item_settings_helper_info.xml | 15 + .../item_settings_radio_single_line.xml | 40 ++ .../layout/item_settings_section_title.xml | 15 + .../res/layout/item_settings_simple_item.xml | 47 ++ .../res/menu/menu_phone_number_addition.xml | 11 + vector/src/main/res/values/colors_riot.xml | 1 + .../main/res/xml/vector_settings_general.xml | 9 +- 35 files changed, 2440 insertions(+), 30 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsController.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySharedViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/discovery/SettingsButtonItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/discovery/SettingsImageItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/discovery/SettingsInfoItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/discovery/SettingsItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/discovery/SettingsItemText.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/discovery/SettingsLoadingItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/discovery/SettingsSectionTitle.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/discovery/SettingsTextButtonItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerViewModel.kt create mode 100644 vector/src/main/res/drawable-xxhdpi/ic_notification_privacy_warning.png create mode 100644 vector/src/main/res/layout/fragment_set_identity_server.xml create mode 100644 vector/src/main/res/layout/item_settings_button.xml create mode 100644 vector/src/main/res/layout/item_settings_button_single_line.xml create mode 100644 vector/src/main/res/layout/item_settings_edit_text.xml create mode 100644 vector/src/main/res/layout/item_settings_helper_info.xml create mode 100644 vector/src/main/res/layout/item_settings_radio_single_line.xml create mode 100644 vector/src/main/res/layout/item_settings_section_title.xml create mode 100644 vector/src/main/res/layout/item_settings_simple_item.xml create mode 100644 vector/src/main/res/menu/menu_phone_number_addition.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityService.kt index 0a844d0921..668cae5e00 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityService.kt @@ -27,7 +27,7 @@ interface IdentityService { /** * Return the default identity server of the homeserver (using Wellknown request) */ - fun getDefaultIdentityServer(): String? + fun getDefaultIdentityServer(callback: MatrixCallback): Cancelable fun getCurrentIdentityServer(): String? @@ -35,9 +35,11 @@ interface IdentityService { fun disconnect() - fun bindThreePid() + fun startBindSession(threePid: ThreePid, nothing: Nothing?, matrixCallback: MatrixCallback) + fun finalizeBindSessionFor3PID(threePid: ThreePid, matrixCallback: MatrixCallback) + fun submitValidationToken(pid: ThreePid, code: String, matrixCallback: MatrixCallback) - fun unbindThreePid() + fun startUnBindSession(threePid: ThreePid, nothing: Nothing?, matrixCallback: MatrixCallback>) fun lookUp(threePids: List, callback: MatrixCallback>): Cancelable diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/ThreePid.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/ThreePid.kt index 7aabb2b9e0..2fa97492fd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/ThreePid.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/ThreePid.kt @@ -18,5 +18,5 @@ package im.vector.matrix.android.api.session.identity sealed class ThreePid(open val value: String) { data class Email(val email: String) : ThreePid(email) - data class Msisdn(val msisdn: String) : ThreePid(msisdn) + data class Msisdn(val msisdn: String, val countryCode: String? = null) : ThreePid(msisdn) } 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 01f2c466ac..5a4a66a722 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 @@ -30,6 +30,7 @@ import im.vector.matrix.android.api.session.identity.IdentityServiceError import im.vector.matrix.android.api.session.identity.IdentityServiceListener import im.vector.matrix.android.api.session.identity.ThreePid import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.NoOpCancellable import im.vector.matrix.android.internal.di.AuthenticatedIdentity import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.network.RetrofitFactory @@ -78,7 +79,7 @@ internal class DefaultIdentityService @Inject constructor( lifecycleRegistry.currentState = Lifecycle.State.STARTED // Observe the account data change accountDataDataSource - .getLiveAccountDataEvent(UserAccountData.TYPE_IDENTITY) + .getLiveAccountDataEvent(UserAccountData.TYPE_IDENTITY_SERVER) .observeNotNull(lifecycleOwner) { val identityServerContent = it.getOrNull()?.content?.toModel() if (identityServerContent != null) { @@ -104,8 +105,10 @@ internal class DefaultIdentityService @Inject constructor( lifecycleRegistry.currentState = Lifecycle.State.DESTROYED } - override fun getDefaultIdentityServer(): String? { - TODO("Not yet implemented") + override fun getDefaultIdentityServer(callback: MatrixCallback): Cancelable { + // TODO Use Wellknown request + callback.onSuccess("https://vector.im") + return NoOpCancellable } override fun getCurrentIdentityServer(): String? { @@ -116,22 +119,49 @@ internal class DefaultIdentityService @Inject constructor( TODO("Not yet implemented") } + override fun startBindSession(threePid: ThreePid, nothing: Nothing?, matrixCallback: MatrixCallback) { + TODO("Not yet implemented") + } + + override fun finalizeBindSessionFor3PID(threePid: ThreePid, matrixCallback: MatrixCallback) { + TODO("Not yet implemented") + } + + override fun submitValidationToken(pid: ThreePid, code: String, matrixCallback: MatrixCallback) { + TODO("Not yet implemented") + } + + override fun startUnBindSession(threePid: ThreePid, nothing: Nothing?, matrixCallback: MatrixCallback>) { + TODO("Not yet implemented") + } + override fun setNewIdentityServer(url: String?, callback: MatrixCallback): Cancelable { + val urlCandidate = url?.let { param -> + buildString { + if (!param.startsWith("http")) { + append("https://") + } + append(param) + } + } + return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { val current = getCurrentIdentityServer() - when (url) { + when (urlCandidate) { current -> // Nothing to do Timber.d("Same URL, nothing to do") null -> { - // TODO - // Disconnect previous one if any + // TODO Disconnect previous one if any identityServiceStore.setUrl(null) updateAccountData(null) } else -> { // TODO: check first that it is a valid identity server - updateAccountData(url) + // Try to get a token + getIdentityServerToken(urlCandidate) + + updateAccountData(urlCandidate) } } } @@ -143,14 +173,6 @@ internal class DefaultIdentityService @Inject constructor( )) } - override fun bindThreePid() { - TODO("Not yet implemented") - } - - override fun unbindThreePid() { - TODO("Not yet implemented") - } - override fun lookUp(threePids: List, callback: MatrixCallback>): Cancelable { return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { lookUpInternal(true, threePids) @@ -181,15 +203,19 @@ internal class DefaultIdentityService @Inject constructor( if (entity.token == null) { // Try to get a token - val openIdToken = openIdTokenTask.execute(Unit) - - val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java) - val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken)) - - identityServiceStore.setToken(token.token) + getIdentityServerToken(url) } } + private suspend fun getIdentityServerToken(url: String) { + val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java) + + val openIdToken = openIdTokenTask.execute(Unit) + val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken)) + + identityServiceStore.setToken(token.token) + } + override fun addListener(listener: IdentityServiceListener) { listeners.add(listener) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt index e7a7939540..ce46d3ba77 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt @@ -30,6 +30,6 @@ abstract class UserAccountData : AccountDataContent { const val TYPE_PREVIEW_URLS = "org.matrix.preview_urls" const val TYPE_WIDGETS = "m.widgets" const val TYPE_PUSH_RULES = "m.push_rules" - const val TYPE_IDENTITY = "m.identity" + const val TYPE_IDENTITY_SERVER = "m.identity_server" } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIdentity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIdentity.kt index 4777daf591..354022420e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIdentity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIdentity.kt @@ -21,7 +21,7 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) internal data class UserAccountDataIdentity( - @Json(name = "type") override val type: String = TYPE_IDENTITY, + @Json(name = "type") override val type: String = TYPE_IDENTITY_SERVER, @Json(name = "content") val content: IdentityContent? = null ) : UserAccountData() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt index 7daeef699e..01dc297946 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt @@ -32,7 +32,7 @@ internal interface UpdateUserAccountDataTask : Task() { + + var listener: Listener? = null + + override fun buildModels(data: DiscoverySettingsState) { + when (data.identityServer) { + is Loading -> { + settingsLoadingItem { + id("identityServerLoading") + } + } + is Fail -> { + settingsInfoItem { + id("identityServerError") + helperText(data.identityServer.error.message) + } + } + is Success -> { + buildIdentityServerSection(data) + + val hasIdentityServer = data.identityServer().isNullOrBlank().not() + + if (hasIdentityServer) { + buildMailSection(data) + buildPhoneNumberSection(data) + } + } + } + } + + private fun buildPhoneNumberSection(data: DiscoverySettingsState) { + settingsSectionTitle { + id("msisdn") + titleResId(R.string.settings_discovery_msisdn_title) + } + + when (data.phoneNumbersList) { + is Loading -> { + settingsLoadingItem { + id("phoneLoading") + } + } + is Fail -> { + settingsInfoItem { + id("msisdnListError") + helperText(data.phoneNumbersList.error.message) + } + } + is Success -> { + val phones = data.phoneNumbersList.invoke() + if (phones.isEmpty()) { + settingsInfoItem { + id("no_msisdn") + helperText(stringProvider.getString(R.string.settings_discovery_no_msisdn)) + } + } else { + phones.forEach { piState -> + val phoneNumber = PhoneNumberUtil.getInstance() + .parse("+${piState.value}", null) + ?.let { + PhoneNumberUtil.getInstance().format(it, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL) + } + + settingsTextButtonItem { + id(piState.value) + title(phoneNumber) + colorProvider(colorProvider) + stringProvider(stringProvider) + when { + piState.isShared is Loading -> buttonIndeterminate(true) + piState.isShared is Fail -> { + buttonType(SettingsTextButtonItem.ButtonType.NORMAL) + buttonStyle(SettingsTextButtonItem.ButtonStyle.DESTRUCTIVE) + buttonTitle(stringProvider.getString(R.string.global_retry)) + infoMessage(piState.isShared.error.message) + buttonClickListener(View.OnClickListener { + listener?.onTapRetryToRetrieveBindings() + }) + } + piState.isShared is Success -> when (piState.isShared()) { + PidInfo.SharedState.SHARED, + PidInfo.SharedState.NOT_SHARED -> { + checked(piState.isShared() == PidInfo.SharedState.SHARED) + buttonType(SettingsTextButtonItem.ButtonType.SWITCH) + switchChangeListener { _, checked -> + if (checked) { + listener?.onTapShareMsisdn(piState.value) + } else { + listener?.onTapRevokeMsisdn(piState.value) + } + } + } + PidInfo.SharedState.NOT_VERIFIED_FOR_BIND, + PidInfo.SharedState.NOT_VERIFIED_FOR_UNBIND -> { + buttonType(SettingsTextButtonItem.ButtonType.NORMAL) + buttonTitle("") + } + } + } + } + when (piState.isShared()) { + PidInfo.SharedState.NOT_VERIFIED_FOR_BIND, + PidInfo.SharedState.NOT_VERIFIED_FOR_UNBIND -> { + settingsItemText { + id("tverif" + piState.value) + descriptionText(stringProvider.getString(R.string.settings_text_message_sent, phoneNumber)) + interactionListener(object : SettingsItemText.Listener { + override fun onValidate(code: String) { + val bind = piState.isShared() == PidInfo.SharedState.NOT_VERIFIED_FOR_BIND + listener?.checkMsisdnVerification(piState.value, code, bind) + } + }) + } + } + else -> { + } + } + } + } + } + } + } + + private fun buildMailSection(data: DiscoverySettingsState) { + settingsSectionTitle { + id("emails") + titleResId(R.string.settings_discovery_emails_title) + } + when (data.emailList) { + is Loading -> { + settingsLoadingItem { + id("mailLoading") + } + } + is Fail -> { + settingsInfoItem { + id("mailListError") + helperText(data.emailList.error.message) + } + } + is Success -> { + val emails = data.emailList.invoke() + if (emails.isEmpty()) { + settingsInfoItem { + id("no_emails") + helperText(stringProvider.getString(R.string.settings_discovery_no_mails)) + } + } else { + emails.forEach { piState -> + settingsTextButtonItem { + id(piState.value) + title(piState.value) + colorProvider(colorProvider) + stringProvider(stringProvider) + when (piState.isShared) { + is Loading -> buttonIndeterminate(true) + is Fail -> { + buttonType(SettingsTextButtonItem.ButtonType.NORMAL) + buttonStyle(SettingsTextButtonItem.ButtonStyle.DESTRUCTIVE) + buttonTitle(stringProvider.getString(R.string.global_retry)) + infoMessage(piState.isShared.error.message) + buttonClickListener(View.OnClickListener { + listener?.onTapRetryToRetrieveBindings() + }) + } + is Success -> when (piState.isShared()) { + PidInfo.SharedState.SHARED, + PidInfo.SharedState.NOT_SHARED -> { + checked(piState.isShared() == PidInfo.SharedState.SHARED) + buttonType(SettingsTextButtonItem.ButtonType.SWITCH) + switchChangeListener { _, checked -> + if (checked) { + listener?.onTapShareEmail(piState.value) + } else { + listener?.onTapRevokeEmail(piState.value) + } + } + } + PidInfo.SharedState.NOT_VERIFIED_FOR_BIND, + PidInfo.SharedState.NOT_VERIFIED_FOR_UNBIND -> { + buttonType(SettingsTextButtonItem.ButtonType.NORMAL) + buttonTitleId(R.string._continue) + infoMessageTintColorId(R.color.vector_info_color) + infoMessage(stringProvider.getString(R.string.settings_discovery_confirm_mail, piState.value)) + buttonClickListener(View.OnClickListener { + val bind = piState.isShared() == PidInfo.SharedState.NOT_VERIFIED_FOR_BIND + listener?.checkEmailVerification(piState.value, bind) + }) + } + } + } + } + } + } + } + } + } + + private fun buildIdentityServerSection(data: DiscoverySettingsState) { + val identityServer = data.identityServer() ?: stringProvider.getString(R.string.none) + + settingsSectionTitle { + id("idsTitle") + titleResId(R.string.identity_server) + } + + settingsItem { + id("idServer") + description(identityServer) + } + + settingsInfoItem { + id("idServerFooter") + if (data.termsNotSigned) { + helperText(stringProvider.getString(R.string.settings_agree_to_terms, identityServer)) + showCompoundDrawable(true) + itemClickListener(View.OnClickListener { listener?.onSelectIdentityServer() }) + } else { + showCompoundDrawable(false) + if (data.identityServer() != null) { + helperText(stringProvider.getString(R.string.settings_discovery_identity_server_info, identityServer)) + } else { + helperTextResId(R.string.settings_discovery_identity_server_info_none) + } + } + } + + settingsButtonItem { + id("change") + colorProvider(colorProvider) + if (data.identityServer() != null) { + buttonTitleId(R.string.change_identity_server) + } else { + buttonTitleId(R.string.add_identity_server) + } + buttonStyle(SettingsTextButtonItem.ButtonStyle.POSITIVE) + buttonClickListener(View.OnClickListener { + listener?.onTapChangeIdentityServer() + }) + } + + if (data.identityServer() != null) { + settingsInfoItem { + id("removeInfo") + helperTextResId(R.string.settings_discovery_disconnect_identity_server_info) + } + settingsButtonItem { + id("remove") + colorProvider(colorProvider) + buttonTitleId(R.string.disconnect_identity_server) + buttonStyle(SettingsTextButtonItem.ButtonStyle.DESTRUCTIVE) + buttonClickListener(View.OnClickListener { + listener?.onTapDisconnectIdentityServer() + }) + } + } + } + + interface Listener { + fun onSelectIdentityServer() + fun onTapRevokeEmail(email: String) + fun onTapShareEmail(email: String) + fun checkEmailVerification(email: String, bind: Boolean) + fun checkMsisdnVerification(msisdn: String, code: String, bind: Boolean) + fun onTapRevokeMsisdn(msisdn: String) + fun onTapShareMsisdn(msisdn: String) + fun onTapChangeIdentityServer() + fun onTapDisconnectIdentityServer() + fun onTapRetryToRetrieveBindings() + } +} + diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsFragment.kt new file mode 100644 index 0000000000..bdf0ca6de4 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsFragment.kt @@ -0,0 +1,199 @@ +/* + * 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.discovery + +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.Observer +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.session.identity.ThreePid +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.exhaustive +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.features.discovery.change.SetIdentityServerFragment +import kotlinx.android.synthetic.main.fragment_generic_recycler.* +import javax.inject.Inject + +class DiscoverySettingsFragment @Inject constructor( + private val controller: DiscoverySettingsController, + val viewModelFactory: DiscoverySettingsViewModel.Factory +) : VectorBaseFragment(), DiscoverySettingsController.Listener { + + override fun getLayoutResId() = R.layout.fragment_generic_recycler + + private val viewModel by fragmentViewModel(DiscoverySettingsViewModel::class) + + lateinit var sharedViewModel: DiscoverySharedViewModel + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + sharedViewModel = activityViewModelProvider.get(DiscoverySharedViewModel::class.java) + + controller.listener = this + recyclerView.configureWith(controller) + + sharedViewModel.navigateEvent.observe(viewLifecycleOwner, Observer { + if (it.peekContent().first == DiscoverySharedViewModel.NEW_IDENTITY_SERVER_SET_REQUEST) { + viewModel.handle(DiscoverySettingsAction.ChangeIdentityServer(it.peekContent().second)) + } + }) + + viewModel.observeViewEvents { + when (it) { + is DiscoverySettingsViewEvents.Failure -> { + // TODO Snackbar.make(view, throwable.toString(), Snackbar.LENGTH_LONG).show() + } + }.exhaustive + } + } + + override fun onDestroyView() { + recyclerView.cleanup() + controller.listener = null + super.onDestroyView() + } + + override fun invalidate() = withState(viewModel) { state -> + controller.setData(state) + } + + override fun onResume() { + super.onResume() + (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_discovery_category) + + //If some 3pids are pending, we can try to check if they have been verified here + viewModel.handle(DiscoverySettingsAction.Refresh) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + /* TODO + if (requestCode == TERMS_REQUEST_CODE) { + if (Activity.RESULT_OK == resultCode) { + viewModel.refreshModel() + } else { + //add some error? + } + } + + */ + super.onActivityResult(requestCode, resultCode, data) + } + + override fun onSelectIdentityServer() = withState(viewModel) { state -> + if (state.termsNotSigned) { + /* + TODO + ReviewTermsActivity.intent(requireContext(), + TermsManager.ServiceType.IdentityService, + SetIdentityServerViewModel.sanitatizeBaseURL(state.identityServer() ?: ""), + null).also { + startActivityForResult(it, TERMS_REQUEST_CODE) + } + + */ + } + } + + override fun onTapRevokeEmail(email: String) { + viewModel.handle(DiscoverySettingsAction.RevokeThreePid(ThreePid.Email(email))) + } + + override fun onTapShareEmail(email: String) { + viewModel.handle(DiscoverySettingsAction.ShareThreePid(ThreePid.Email(email))) + } + + override fun checkEmailVerification(email: String, bind: Boolean) { + viewModel.handle(DiscoverySettingsAction.FinalizeBind3pid(ThreePid.Email(email), bind)) + } + + override fun checkMsisdnVerification(msisdn: String, code: String, bind: Boolean) { + viewModel.handle(DiscoverySettingsAction.SubmitMsisdnToken(msisdn, code, bind)) + } + + override fun onTapRevokeMsisdn(msisdn: String) { + viewModel.handle(DiscoverySettingsAction.RevokeThreePid(ThreePid.Msisdn(msisdn))) + } + + override fun onTapShareMsisdn(msisdn: String) { + viewModel.handle(DiscoverySettingsAction.ShareThreePid(ThreePid.Msisdn(msisdn))) + } + + override fun onTapChangeIdentityServer() = withState(viewModel) { state -> + //we should prompt if there are bound items with current is + val pidList = ArrayList().apply { + state.emailList()?.let { addAll(it) } + state.phoneNumbersList()?.let { addAll(it) } + } + + val hasBoundIds = pidList.any { it.isShared() == PidInfo.SharedState.SHARED } + + if (hasBoundIds) { + //we should prompt + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.change_identity_server) + .setMessage(getString(R.string.settings_discovery_disconnect_with_bound_pid, state.identityServer(), state.identityServer())) + .setPositiveButton(R.string._continue) { _, _ -> navigateToChangeIdentityServerFragment() } + .setNegativeButton(R.string.cancel, null) + .show() + Unit + } else { + navigateToChangeIdentityServerFragment() + } + } + + override fun onTapDisconnectIdentityServer() { + //we should prompt if there are bound items with current is + withState(viewModel) { state -> + val pidList = ArrayList().apply { + state.emailList()?.let { addAll(it) } + state.phoneNumbersList()?.let { addAll(it) } + } + + val hasBoundIds = pidList.any { it.isShared() == PidInfo.SharedState.SHARED } + + if (hasBoundIds) { + //we should prompt + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.disconnect_identity_server) + .setMessage(getString(R.string.settings_discovery_disconnect_with_bound_pid, state.identityServer(), state.identityServer())) + .setPositiveButton(R.string._continue) { _, _ -> viewModel.handle(DiscoverySettingsAction.ChangeIdentityServer(null)) } + .setNegativeButton(R.string.cancel, null) + .show() + } else { + viewModel.handle(DiscoverySettingsAction.ChangeIdentityServer(null)) + } + } + } + + override fun onTapRetryToRetrieveBindings() { + viewModel.handle(DiscoverySettingsAction.RetrieveBinding) + } + + private fun navigateToChangeIdentityServerFragment() { + parentFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom, R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom) + .replace(R.id.vector_settings_page, SetIdentityServerFragment::class.java, null) + .addToBackStack(null) + .commit() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewModel.kt new file mode 100644 index 0000000000..34c5bcf66b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewModel.kt @@ -0,0 +1,503 @@ +/* + * 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.discovery + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail +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.google.i18n.phonenumbers.PhoneNumberUtil +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.IdentityServiceListener +import im.vector.matrix.android.api.session.identity.ThreePid +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 + +data class PidInfo( + val value: String, + val isShared: Async, + val _3pid: ThreePid? = null +) { + enum class SharedState { + SHARED, + NOT_SHARED, + NOT_VERIFIED_FOR_BIND, + NOT_VERIFIED_FOR_UNBIND + } +} + +data class DiscoverySettingsState( + val identityServer: Async = Uninitialized, + val emailList: Async> = Uninitialized, + val phoneNumbersList: Async> = Uninitialized, + // TODO Use ViewEvents + val termsNotSigned: Boolean = false +) : MvRxState + +sealed class DiscoverySettingsAction : VectorViewModelAction { + object RetrieveBinding : DiscoverySettingsAction() + object Refresh : DiscoverySettingsAction() + + data class ChangeIdentityServer(val url: String?) : DiscoverySettingsAction() + data class RevokeThreePid(val threePid: ThreePid) : DiscoverySettingsAction() + data class ShareThreePid(val threePid: ThreePid) : DiscoverySettingsAction() + data class FinalizeBind3pid(val threePid: ThreePid, val bind: Boolean) : DiscoverySettingsAction() + data class SubmitMsisdnToken(val msisdn: String, val code: String, val bind: Boolean) : DiscoverySettingsAction() +} + +sealed class DiscoverySettingsViewEvents : VectorViewEvents { + data class Failure(val throwable: Throwable) : DiscoverySettingsViewEvents() +} + +class DiscoverySettingsViewModel @AssistedInject constructor( + @Assisted initialState: DiscoverySettingsState, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: DiscoverySettingsState): DiscoverySettingsViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: DiscoverySettingsState): DiscoverySettingsViewModel? { + val fragment: DiscoverySettingsFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.viewModelFactory.create(state) + } + } + + private val identityService = session.identityService() + + private val identityServerManagerListener = object : IdentityServiceListener { + override fun onIdentityServerChange() = withState { state -> + val identityServerUrl = identityService.getCurrentIdentityServer() + val currentIS = state.identityServer() + setState { + copy(identityServer = Success(identityServerUrl)) + } + if (currentIS != identityServerUrl) refreshModel() + } + } + + init { + startListenToIdentityManager() + refreshModel() + } + + override fun onCleared() { + super.onCleared() + stopListenToIdentityManager() + } + + override fun handle(action: DiscoverySettingsAction) { + when (action) { + DiscoverySettingsAction.Refresh -> refreshPendingEmailBindings() + DiscoverySettingsAction.RetrieveBinding -> retrieveBinding() + is DiscoverySettingsAction.ChangeIdentityServer -> changeIdentityServer(action) + is DiscoverySettingsAction.RevokeThreePid -> revokeThreePid(action) + is DiscoverySettingsAction.ShareThreePid -> shareThreePid(action) + is DiscoverySettingsAction.FinalizeBind3pid -> finalizeBind3pid(action) + is DiscoverySettingsAction.SubmitMsisdnToken -> submitMsisdnToken(action) + }.exhaustive + } + + private fun changeIdentityServer(action: DiscoverySettingsAction.ChangeIdentityServer) { + setState { + copy( + identityServer = Loading() + ) + } + + session.identityService().setNewIdentityServer(action.url, object : MatrixCallback { + override fun onSuccess(data: Unit) { + setState { + copy( + identityServer = Success(action.url) + ) + } + refreshModel() + } + + override fun onFailure(failure: Throwable) { + setState { + copy( + identityServer = Fail(failure) + ) + } + } + }) + } + + private fun shareThreePid(action: DiscoverySettingsAction.ShareThreePid) { + when (action.threePid) { + is ThreePid.Email -> shareEmail(action.threePid.email) + is ThreePid.Msisdn -> shareMsisdn(action.threePid.msisdn) + }.exhaustive + } + + private fun shareEmail(email: String) = withState { state -> + if (state.identityServer() == null) return@withState + changeMailState(email, Loading(), null) + + identityService.startBindSession(ThreePid.Email(email), null, + object : MatrixCallback { + override fun onSuccess(data: ThreePid) { + changeMailState(email, Success(PidInfo.SharedState.NOT_VERIFIED_FOR_BIND), data) + } + + override fun onFailure(failure: Throwable) { + _viewEvents.post(DiscoverySettingsViewEvents.Failure(failure)) + + changeMailState(email, Fail(failure)) + } + }) + } + + private fun changeMailState(address: String, state: Async, threePid: ThreePid?) { + setState { + val currentMails = emailList() ?: emptyList() + copy(emailList = Success( + currentMails.map { + if (it.value == address) { + it.copy( + _3pid = threePid, + isShared = state + ) + } else { + it + } + } + )) + } + } + + private fun changeMailState(address: String, state: Async) { + setState { + val currentMails = emailList() ?: emptyList() + copy(emailList = Success( + currentMails.map { + if (it.value == address) { + it.copy(isShared = state) + } else { + it + } + } + )) + } + } + + private fun changeMsisdnState(address: String, state: Async, threePid: ThreePid?) { + setState { + val phones = phoneNumbersList() ?: emptyList() + copy(phoneNumbersList = Success( + phones.map { + if (it.value == address) { + it.copy( + _3pid = threePid, + isShared = state + ) + } else { + it + } + } + )) + } + } + + private fun revokeThreePid(action: DiscoverySettingsAction.RevokeThreePid) { + when (action.threePid) { + is ThreePid.Email -> revokeEmail(action.threePid.email) + is ThreePid.Msisdn -> revokeMsisdn(action.threePid.msisdn) + }.exhaustive + } + + private fun revokeEmail(email: String) = withState { state -> + if (state.identityServer() == null) return@withState + if (state.emailList() == null) return@withState + changeMailState(email, Loading()) + + identityService.startUnBindSession(ThreePid.Email(email), null, object : MatrixCallback> { + override fun onSuccess(data: Pair) { + if (data.first) { + // requires mail validation + changeMailState(email, Success(PidInfo.SharedState.NOT_VERIFIED_FOR_UNBIND), data.second) + } else { + changeMailState(email, Success(PidInfo.SharedState.NOT_SHARED)) + } + } + + override fun onFailure(failure: Throwable) { + _viewEvents.post(DiscoverySettingsViewEvents.Failure(failure)) + + changeMailState(email, Fail(failure)) + } + }) + } + + private fun revokeMsisdn(msisdn: String) = withState { state -> + if (state.identityServer() == null) return@withState + if (state.emailList() == null) return@withState + changeMsisdnState(msisdn, Loading()) + + val phoneNumber = PhoneNumberUtil.getInstance() + .parse("+$msisdn", null) + val countryCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(phoneNumber.countryCode) + + identityService.startUnBindSession(ThreePid.Msisdn(msisdn, countryCode), null, object : MatrixCallback> { + override fun onSuccess(data: Pair) { + if (data.first /*requires mail validation */) { + changeMsisdnState(msisdn, Success(PidInfo.SharedState.NOT_VERIFIED_FOR_UNBIND), data.second) + } else { + changeMsisdnState(msisdn, Success(PidInfo.SharedState.NOT_SHARED)) + } + } + + override fun onFailure(failure: Throwable) { + _viewEvents.post(DiscoverySettingsViewEvents.Failure(failure)) + + changeMsisdnState(msisdn, Fail(failure)) + } + }) + + } + + private fun shareMsisdn(msisdn: String) = withState { state -> + if (state.identityServer() == null) return@withState + changeMsisdnState(msisdn, Loading()) + + val phoneNumber = PhoneNumberUtil.getInstance() + .parse("+$msisdn", null) + val countryCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(phoneNumber.countryCode) + + + identityService.startBindSession(ThreePid.Msisdn(msisdn, countryCode), null, object : MatrixCallback { + override fun onSuccess(data: ThreePid) { + changeMsisdnState(msisdn, Success(PidInfo.SharedState.NOT_VERIFIED_FOR_BIND), data) + } + + override fun onFailure(failure: Throwable) { + _viewEvents.post(DiscoverySettingsViewEvents.Failure(failure)) + + changeMsisdnState(msisdn, Fail(failure)) + } + }) + } + + private fun changeMsisdnState(msisdn: String, sharedState: Async) { + setState { + val currentMsisdns = phoneNumbersList()!! + copy(phoneNumbersList = Success( + currentMsisdns.map { + if (it.value == msisdn) { + it.copy(isShared = sharedState) + } else { + it + } + }) + ) + } + } + + private fun startListenToIdentityManager() { + identityService.addListener(identityServerManagerListener) + } + + private fun stopListenToIdentityManager() { + identityService.addListener(identityServerManagerListener) + } + + private fun refreshModel() = withState { state -> + if (state.identityServer().isNullOrBlank()) return@withState + + setState { + copy( + emailList = Loading(), + phoneNumbersList = Loading() + ) + } + + /* TODO + session.refreshThirdPartyIdentifiers(object : MatrixCallback { + override fun onFailure(failure: Throwable) { + _errorLiveEvent.postValue(LiveEvent(failure)) + + setState { + copy( + emailList = Fail(failure), + phoneNumbersList = Fail(failure) + ) + } + } + + override fun onSuccess(data: Unit) { + setState { + copy(termsNotSigned = false) + } + + retrieveBinding() + } + }) + */ + } + + private fun retrieveBinding() { + /* TODO + val linkedMailsInfo = session.myUser.getlinkedEmails() + val knownEmails = linkedMailsInfo.map { it.address } + // Note: it will be a list of "email" + val knownEmailMedium = linkedMailsInfo.map { it.medium } + + val linkedMsisdnsInfo = session.myUser.getlinkedPhoneNumbers() + val knownMsisdns = linkedMsisdnsInfo.map { it.address } + // Note: it will be a list of "msisdn" + val knownMsisdnMedium = linkedMsisdnsInfo.map { it.medium } + + setState { + copy( + emailList = Success(knownEmails.map { PidInfo(it, Loading()) }), + phoneNumbersList = Success(knownMsisdns.map { PidInfo(it, Loading()) }) + ) + } + + identityService.lookup(knownEmails + knownMsisdns, + knownEmailMedium + knownMsisdnMedium, + object : MatrixCallback> { + override fun onSuccess(data: List) { + setState { + copy( + emailList = Success(toPidInfoList(knownEmails, data.take(knownEmails.size))), + phoneNumbersList = Success(toPidInfoList(knownMsisdns, data.takeLast(knownMsisdns.size))) + ) + } + } + + override fun onUnexpectedError(e: Exception) { + if (e is TermsNotSignedException) { + setState { + // TODO Use ViewEvent + copy(termsNotSigned = true) + } + } + onError(e) + } + + override fun onNetworkError(e: Exception) { + onError(e) + } + + override fun onMatrixError(e: MatrixError) { + onError(Throwable(e.message)) + } + + private fun onError(e: Throwable) { + _errorLiveEvent.postValue(LiveEvent(e)) + + setState { + copy( + emailList = Success(knownEmails.map { PidInfo(it, Fail(e)) }), + phoneNumbersList = Success(knownMsisdns.map { PidInfo(it, Fail(e)) }) + ) + } + } + }) + */ + } + + private fun toPidInfoList(addressList: List, matrixIds: List): List { + return addressList.map { + val hasMatrixId = matrixIds[addressList.indexOf(it)].isNotBlank() + PidInfo( + value = it, + isShared = Success(PidInfo.SharedState.SHARED.takeIf { hasMatrixId } ?: PidInfo.SharedState.NOT_SHARED) + ) + } + } + + private fun submitMsisdnToken(action: DiscoverySettingsAction.SubmitMsisdnToken) = withState { state -> + val pid = state.phoneNumbersList()?.find { it.value == action.msisdn }?._3pid ?: return@withState + + identityService.submitValidationToken(pid, + action.code, + object : MatrixCallback { + override fun onSuccess(data: Unit) { + finalizeBind3pid(DiscoverySettingsAction.FinalizeBind3pid(ThreePid.Msisdn(action.msisdn), action.bind)) + } + + override fun onFailure(failure: Throwable) { + _viewEvents.post(DiscoverySettingsViewEvents.Failure(failure)) + changeMsisdnState(action.msisdn, Fail(failure)) + } + } + ) + } + + private fun finalizeBind3pid(action: DiscoverySettingsAction.FinalizeBind3pid) = withState { state -> + val _3pid = when (action.threePid) { + is ThreePid.Email -> { + changeMailState(action.threePid.email, Loading()) + state.emailList()?.find { it.value == action.threePid.email }?._3pid ?: return@withState + } + is ThreePid.Msisdn -> { + changeMsisdnState(action.threePid.msisdn, Loading()) + state.phoneNumbersList()?.find { it.value == action.threePid.msisdn }?._3pid ?: return@withState + } + } + + identityService.finalizeBindSessionFor3PID(_3pid, object : MatrixCallback { + override fun onSuccess(data: Unit) { + val sharedState = Success(if (action.bind) PidInfo.SharedState.SHARED else PidInfo.SharedState.NOT_SHARED) + when (action.threePid) { + is ThreePid.Email -> changeMailState(action.threePid.email, sharedState, null) + is ThreePid.Msisdn -> changeMsisdnState(action.threePid.msisdn, sharedState, null) + } + } + + override fun onFailure(failure: Throwable) { + _viewEvents.post(DiscoverySettingsViewEvents.Failure(failure)) + + // Restore previous state after an error + val sharedState = Success(if (action.bind) PidInfo.SharedState.NOT_VERIFIED_FOR_BIND else PidInfo.SharedState.NOT_VERIFIED_FOR_UNBIND) + when (action.threePid) { + is ThreePid.Email -> changeMailState(action.threePid.email, sharedState) + is ThreePid.Msisdn -> changeMsisdnState(action.threePid.msisdn, sharedState) + } + } + }) + + } + + private fun refreshPendingEmailBindings() = withState { state -> + state.emailList()?.forEach { info -> + when (info.isShared()) { + PidInfo.SharedState.NOT_VERIFIED_FOR_BIND -> finalizeBind3pid(DiscoverySettingsAction.FinalizeBind3pid(ThreePid.Email(info.value), true)) + PidInfo.SharedState.NOT_VERIFIED_FOR_UNBIND -> finalizeBind3pid(DiscoverySettingsAction.FinalizeBind3pid(ThreePid.Email(info.value), false)) + else -> Unit + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySharedViewModel.kt new file mode 100644 index 0000000000..91e43187c2 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySharedViewModel.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.discovery + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import im.vector.riotx.core.utils.LiveEvent +import javax.inject.Inject + +// TODO Rework this +class DiscoverySharedViewModel @Inject constructor() : ViewModel() { + + var navigateEvent = MutableLiveData>>() + + companion object { + const val NEW_IDENTITY_SERVER_SET_REQUEST = "NEW_IDENTITY_SERVER_SET_REQUEST" + } + + fun requestChangeToIdentityServer(server: String) { + navigateEvent.postValue(LiveEvent(NEW_IDENTITY_SERVER_SET_REQUEST to server)) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/SettingsButtonItem.kt b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsButtonItem.kt new file mode 100644 index 0000000000..8deb500e82 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsButtonItem.kt @@ -0,0 +1,71 @@ +/* + * 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.discovery + +import android.view.View +import android.widget.Button +import androidx.annotation.StringRes +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.core.resources.ColorProvider + +@EpoxyModelClass(layout = R.layout.item_settings_button) +abstract class SettingsButtonItem : EpoxyModelWithHolder() { + + @EpoxyAttribute + lateinit var colorProvider: ColorProvider + + @EpoxyAttribute + var buttonTitle: String? = null + + @EpoxyAttribute + @StringRes + var buttonTitleId: Int? = null + + @EpoxyAttribute + var buttonStyle: SettingsTextButtonItem.ButtonStyle = SettingsTextButtonItem.ButtonStyle.POSITIVE + + @EpoxyAttribute + var buttonClickListener: View.OnClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + if (buttonTitleId != null) { + holder.button.setText(buttonTitleId!!) + } else { + holder.button.setTextOrHide(buttonTitle) + } + + when (buttonStyle) { + SettingsTextButtonItem.ButtonStyle.POSITIVE -> { + holder.button.setTextColor(colorProvider.getColorFromAttribute(R.attr.colorAccent)) + } + SettingsTextButtonItem.ButtonStyle.DESTRUCTIVE -> { + holder.button.setTextColor(colorProvider.getColor(R.color.vector_error_color)) + } + } + + holder.button.setOnClickListener(buttonClickListener) + } + + class Holder : VectorEpoxyHolder() { + val button by bind