diff --git a/CHANGES.md b/CHANGES.md index dfda3cef..c5f686b2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,34 @@ +Changes in RiotX 0.3.0 (2019-08-08) +=================================================== + +Features: + - Create Direct Room flow + - Handle `/markdown` command + +Improvements: + - UI for pending edits (#193) + - UX image preview screen transition (#393) + - Basic support for resending failed messages (retry/remove) + - Enable proper cancellation of suspending functions (including db transaction) + - Enhances network connectivity checks in SDK + - Add "View Edit History" item in the message bottom sheet (#401) + - Cancel sync request on pause and timeout to 0 after pause (#404) + +Other changes: + - Show sync progress also in room detail screen (#403) + +Bugfix: + - Edited message: link confusion when (edited) appears in body (#398) + - Close detail room screen when the room is left with another client (#256) + - Clear notification for a room left on another client + - Fix messages with empty `in_reply_to` not rendering (#447) + - Fix clear cache (#408) and Logout (#205) + - Fix `(edited)` link can be copied to clipboard (#402) + +Build: + - Split APK: generate one APK per arch, to reduce APK size of about 30% + + Changes in RiotX 0.2.0 (2019-07-18) =================================================== @@ -36,7 +67,7 @@ Mode details here: https://medium.com/@RiotChat/introducing-the-riotx-beta-for-a ======================================================= -Changes in RiotX 0.XX (2019-XX-XX) +Changes in RiotX 0.0.0 (2019-XX-XX) =================================================== Features: diff --git a/matrix-sdk-android-rx/build.gradle b/matrix-sdk-android-rx/build.gradle index 546922f2..6e24d1d5 100644 --- a/matrix-sdk-android-rx/build.gradle +++ b/matrix-sdk-android-rx/build.gradle @@ -33,11 +33,12 @@ android { } dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(":matrix-sdk-android") implementation 'androidx.appcompat:appcompat:1.1.0-beta01' implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + // Paging + implementation "androidx.paging:paging-runtime-ktx:2.1.0" testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt index d4c9a79f..a1943bbe 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/LiveDataObservable.kt @@ -20,6 +20,8 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import io.reactivex.Observable import io.reactivex.android.MainThreadDisposable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers private class LiveDataObservable( private val liveData: LiveData, @@ -57,5 +59,5 @@ private class LiveDataObservable( } fun LiveData.asObservable(): Observable { - return LiveDataObservable(this) + return LiveDataObservable(this).observeOn(Schedulers.computation()) } \ No newline at end of file diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackCompletable.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackCompletable.kt new file mode 100644 index 00000000..58c015df --- /dev/null +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackCompletable.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.rx + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable +import io.reactivex.CompletableEmitter +import io.reactivex.SingleEmitter + +internal class MatrixCallbackCompletable(private val completableEmitter: CompletableEmitter) : MatrixCallback { + + override fun onSuccess(data: T) { + completableEmitter.onComplete() + } + + override fun onFailure(failure: Throwable) { + completableEmitter.tryOnError(failure) + } +} + +fun Cancelable.toCompletable(completableEmitter: CompletableEmitter) { + completableEmitter.setCancellable { + this.cancel() + } +} \ No newline at end of file diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackSingle.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackSingle.kt new file mode 100644 index 00000000..8d554df2 --- /dev/null +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackSingle.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.rx + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable +import io.reactivex.SingleEmitter + +internal class MatrixCallbackSingle(private val singleEmitter: SingleEmitter) : MatrixCallback { + + override fun onSuccess(data: T) { + singleEmitter.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + singleEmitter.tryOnError(failure) + } +} + +fun Cancelable.toSingle(singleEmitter: SingleEmitter) { + singleEmitter.setCancellable { + this.cancel() + } +} \ No newline at end of file diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index 2c9c7d8b..201622b3 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -21,24 +21,32 @@ import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import io.reactivex.Observable -import io.reactivex.schedulers.Schedulers +import io.reactivex.Single class RxRoom(private val room: Room) { fun liveRoomSummary(): Observable { - return room.liveRoomSummary().asObservable().observeOn(Schedulers.computation()) + return room.liveRoomSummary().asObservable() } fun liveRoomMemberIds(): Observable> { - return room.getRoomMemberIdsLive().asObservable().observeOn(Schedulers.computation()) + return room.getRoomMemberIdsLive().asObservable() } fun liveAnnotationSummary(eventId: String): Observable { - return room.getEventSummaryLive(eventId).asObservable().observeOn(Schedulers.computation()) + return room.getEventSummaryLive(eventId).asObservable() } fun liveTimelineEvent(eventId: String): Observable { - return room.liveTimeLineEvent(eventId).asObservable().observeOn(Schedulers.computation()) + return room.liveTimeLineEvent(eventId).asObservable() + } + + fun loadRoomMembersIfNeeded(): Single = Single.create { + room.loadRoomMembersIfNeeded(MatrixCallbackSingle(it)).toSingle(it) + } + + fun joinRoom(viaServers: List = emptyList()): Single = Single.create { + room.join(viaServers, MatrixCallbackSingle(it)).toSingle(it) } } diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt index 30d31f94..f3fb06a4 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt @@ -16,30 +16,55 @@ package im.vector.matrix.rx +import androidx.paging.PagedList import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.pushers.Pusher import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.sync.SyncState +import im.vector.matrix.android.api.session.user.model.User import io.reactivex.Observable -import io.reactivex.schedulers.Schedulers +import io.reactivex.Single class RxSession(private val session: Session) { fun liveRoomSummaries(): Observable> { - return session.liveRoomSummaries().asObservable().observeOn(Schedulers.computation()) + return session.liveRoomSummaries().asObservable() } fun liveGroupSummaries(): Observable> { - return session.liveGroupSummaries().asObservable().observeOn(Schedulers.computation()) + return session.liveGroupSummaries().asObservable() } fun liveSyncState(): Observable { - return session.syncState().asObservable().observeOn(Schedulers.computation()) + return session.syncState().asObservable() } fun livePushers(): Observable> { - return session.livePushers().asObservable().observeOn(Schedulers.computation()) + return session.livePushers().asObservable() + } + + fun liveUsers(): Observable> { + return session.liveUsers().asObservable() + } + + fun livePagedUsers(filter: String? = null): Observable> { + return session.livePagedUsers(filter).asObservable() + } + + fun createRoom(roomParams: CreateRoomParams): Single = Single.create { + session.createRoom(roomParams, MatrixCallbackSingle(it)).toSingle(it) + } + + fun searchUsersDirectory(search: String, + limit: Int, + excludedUserIds: Set): Single> = Single.create { + session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it) + } + + fun joinRoom(roomId: String, viaServers: List = emptyList()): Single = Single.create { + session.joinRoom(roomId, viaServers, MatrixCallbackSingle(it)).toSingle(it) } } diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index b62b3fea..fbe09691 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -94,7 +94,6 @@ dependencies { def markwon_version = '3.0.0' def daggerVersion = '2.23.1' - implementation fileTree(dir: 'libs', include: ['*.aar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" @@ -110,7 +109,7 @@ dependencies { implementation 'com.squareup.retrofit2:converter-moshi:2.4.0' implementation 'com.squareup.okhttp3:okhttp:3.14.1' implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0' - implementation 'com.novoda:merlin:1.1.6' + implementation 'com.novoda:merlin:1.2.0' implementation "com.squareup.moshi:moshi-adapters:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" @@ -126,9 +125,6 @@ dependencies { // FP implementation "io.arrow-kt:arrow-core:$arrow_version" implementation "io.arrow-kt:arrow-instances-core:$arrow_version" - implementation "io.arrow-kt:arrow-effects:$arrow_version" - implementation "io.arrow-kt:arrow-effects-instances:$arrow_version" - implementation "io.arrow-kt:arrow-integration-retrofit-adapter:$arrow_version" // olm lib is now hosted by jitpack: https://jitpack.io/#org.matrix.gitlab.matrix-org/olm implementation 'org.matrix.gitlab.matrix-org:olm:3.1.2' diff --git a/matrix-sdk-android/libs/react-native-webrtc.aar b/matrix-sdk-android/libs/react-native-webrtc.aar deleted file mode 100644 index ff5bb99f..00000000 Binary files a/matrix-sdk-android/libs/react-native-webrtc.aar and /dev/null differ diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/LiveDataTestObserver.java b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/LiveDataTestObserver.java index 512d9d0e..10d25135 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/LiveDataTestObserver.java +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/LiveDataTestObserver.java @@ -16,10 +16,10 @@ package im.vector.matrix.android; +import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; -import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt index be351fb0..c6da3c46 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt @@ -19,11 +19,7 @@ package im.vector.matrix.android.session.room.timeline import androidx.test.ext.junit.runners.AndroidJUnit4 import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.InstrumentedTest -import im.vector.matrix.android.internal.database.helper.add -import im.vector.matrix.android.internal.database.helper.addAll -import im.vector.matrix.android.internal.database.helper.isUnlinked -import im.vector.matrix.android.internal.database.helper.lastStateIndex -import im.vector.matrix.android.internal.database.helper.merge +import im.vector.matrix.android.internal.database.helper.* import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/FakeGetContextOfEventTask.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/FakeGetContextOfEventTask.kt index f77c9b1a..48f22392 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/FakeGetContextOfEventTask.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/FakeGetContextOfEventTask.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.session.room.timeline -import arrow.core.Try import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor @@ -24,7 +23,7 @@ import kotlin.random.Random internal class FakeGetContextOfEventTask constructor(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : GetContextOfEventTask { - override suspend fun execute(params: GetContextOfEventTask.Params): Try { + override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result { val fakeEvents = RoomDataHelper.createFakeListOfEvents(30) val tokenChunkEvent = FakeTokenChunkEvent( Random.nextLong(System.currentTimeMillis()).toString(), diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/FakePaginationTask.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/FakePaginationTask.kt index bf163401..2f7f63d7 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/FakePaginationTask.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/FakePaginationTask.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.session.room.timeline -import arrow.core.Try import im.vector.matrix.android.internal.session.room.timeline.PaginationTask import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor import javax.inject.Inject @@ -24,7 +23,7 @@ import kotlin.random.Random internal class FakePaginationTask @Inject constructor(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : PaginationTask { - override suspend fun execute(params: PaginationTask.Params): Try { + override suspend fun execute(params: PaginationTask.Params): TokenChunkEventPersistor.Result { val fakeEvents = RoomDataHelper.createFakeListOfEvents(30) val tokenChunkEvent = FakeTokenChunkEvent(params.from, Random.nextLong(System.currentTimeMillis()).toString(), fakeEvents) return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, params.direction) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt index 5cb7f4ca..93540829 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt @@ -28,7 +28,7 @@ object MatrixPatterns { // regex pattern to find matrix user ids in a string. // See https://matrix.org/speculator/spec/HEAD/appendices.html#historical-user-ids private const val MATRIX_USER_IDENTIFIER_REGEX = "@[A-Z0-9\\x21-\\x39\\x3B-\\x7F]+$DOMAIN_REGEX" - private val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE) + val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE) // regex pattern to find room ids in a string. private const val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9]+$DOMAIN_REGEX" @@ -123,9 +123,9 @@ object MatrixPatterns { */ fun isEventId(str: String?): Boolean { return str != null - && (str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER - || str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 - || str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4) + && (str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER + || str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 + || str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4) } /** @@ -137,4 +137,23 @@ object MatrixPatterns { fun isGroupId(str: String?): Boolean { return str != null && str matches PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER } + + /** + * Extract server name from a matrix id + * + * @param matrixId + * @return null if not found or if matrixId is null + */ + fun extractServerNameFromId(matrixId: String?): String? { + if (matrixId == null) { + return null + } + + val index = matrixId.indexOf(":") + + return if (index == -1) { + null + } else matrixId.substring(index + 1) + + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/HomeServerConnectionConfig.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/HomeServerConnectionConfig.kt index f231d3f1..850c4f71 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/HomeServerConnectionConfig.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/HomeServerConnectionConfig.kt @@ -31,7 +31,7 @@ import okhttp3.TlsVersion @JsonClass(generateAdapter = true) data class HomeServerConnectionConfig( val homeServerUri: Uri, - val identityServerUri: Uri, + val identityServerUri: Uri? = null, val antiVirusServerUri: Uri? = null, val allowedFingerprints: MutableList = ArrayList(), val shouldPin: Boolean = false, @@ -48,7 +48,7 @@ data class HomeServerConnectionConfig( class Builder { private lateinit var homeServerUri: Uri - private lateinit var identityServerUri: Uri + private var identityServerUri: Uri? = null private var antiVirusServerUri: Uri? = null private val allowedFingerprints: MutableList = ArrayList() private var shouldPin: Boolean = false 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 1e87cfc1..7d433ba7 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 @@ -26,8 +26,13 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class MatrixError( @Json(name = "errcode") val code: String, - @Json(name = "error") val message: String -) { + @Json(name = "error") val message: String, + + @Json(name = "consent_uri") val consentUri: String? = null, + // RESOURCE_LIMIT_EXCEEDED data + @Json(name = "limit_type") val limitType: String? = null, + @Json(name = "admin_contact") val adminUri: String? = null) { + companion object { const val FORBIDDEN = "M_FORBIDDEN" @@ -55,5 +60,8 @@ data class MatrixError( const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN" const val RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED" const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION" + + // Possible value for "limit_type" + const val LIMIT_TYPE_MAU = "monthly_active_user" } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixPermalinkSpan.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixPermalinkSpan.kt index bbef1d36..58cd76a0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixPermalinkSpan.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixPermalinkSpan.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.permalinks import android.text.style.ClickableSpan import android.view.View +import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan.Callback /** * This MatrixPermalinkSpan is a clickable span which use a [Callback] to communicate back. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt index b00450b5..28fcff0c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.pushrules import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.pushrules.rest.PushRule import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.util.Cancelable interface PushRuleService { @@ -31,7 +32,7 @@ interface PushRuleService { //TODO update rule - fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback) + fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback): Cancelable fun addPushRuleListener(listener: PushRuleListener) @@ -41,6 +42,7 @@ interface PushRuleService { interface PushRuleListener { fun onMatchRule(event: Event, actions: List) + fun onRoomLeft(roomId: String) fun batchFinish() } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt index 8e933426..0397b514 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt @@ -26,14 +26,12 @@ import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult import im.vector.matrix.android.internal.crypto.NewSessionListener -import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody -import java.io.File interface CryptoService { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt index 547e627f..902515a2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt @@ -20,6 +20,9 @@ import android.text.TextUtils import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.crypto.MXCryptoError +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.di.MoshiProvider @@ -79,9 +82,15 @@ data class Event( ) { + @Transient var mxDecryptionResult: OlmDecryptionResult? = null + + @Transient var mCryptoError: MXCryptoError.ErrorType? = null + @Transient + var sendState: SendState = SendState.UNKNOWN + /** * Check if event is a state event. @@ -95,42 +104,6 @@ data class Event( // Crypto //============================================================================================================== -// /** -// * For encrypted events, the plaintext payload for the event. -// * This is a small MXEvent instance with typically value for `type` and 'content' fields. -// */ -// @Transient -// var mClearEvent: Event? = null -// private set -// -// /** -// * Curve25519 key which we believe belongs to the sender of the event. -// * See `senderKey` property. -// */ -// @Transient -// private var mSenderCurve25519Key: String? = null -// -// /** -// * Ed25519 key which the sender of this event (for olm) or the creator of the megolm session (for megolm) claims to own. -// * See `claimedEd25519Key` property. -// */ -// @Transient -// private var mClaimedEd25519Key: String? = null -// -// /** -// * Curve25519 keys of devices involved in telling us about the senderCurve25519Key and claimedEd25519Key. -// * See `forwardingCurve25519KeyChain` property. -// */ -// @Transient -// private var mForwardingCurve25519KeyChain: List = ArrayList() -// -// /** -// * Decryption error -// */ -// @Transient -// var mCryptoError: MXCryptoError? = null -// private set - /** * @return true if this event is encrypted. */ @@ -138,51 +111,11 @@ data class Event( return TextUtils.equals(type, EventType.ENCRYPTED) } - /** - * Update the clear data on this event. - * This is used after decrypting an event; it should not be used by applications. - * - * @param decryptionResult the decryption result, including the plaintext and some key info. - */ -// internal fun setClearData(decryptionResult: MXEventDecryptionResult?) { -// mClearEvent = null -// if (decryptionResult != null) { -// if (decryptionResult.clearEvent != null) { -// val adapter = MoshiProvider.providesMoshi().adapter(Event::class.java) -// mClearEvent = adapter.fromJsonValue(decryptionResult.clearEvent) -// -// if (mClearEvent != null) { -// mSenderCurve25519Key = decryptionResult.senderCurve25519Key -// mClaimedEd25519Key = decryptionResult.claimedEd25519Key -// mForwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain -// -// // For encrypted events with relation, the m.relates_to is kept in clear, so we need to put it back -// // in the clear event -// try { -// content?.get("m.relates_to")?.let { clearRelates -> -// mClearEvent = mClearEvent?.copy( -// content = HashMap(mClearEvent!!.content).apply { -// this["m.relates_to"] = clearRelates -// } -// ) -// } -// } catch (e: Exception) { -// Timber.e(e, "Unable to restore 'm.relates_to' the clear event") -// } -// } -// -// -// } -// } -// mCryptoError = null -// } - /** * @return The curve25519 key that sent this event. */ fun getSenderKey(): String? { return mxDecryptionResult?.senderKey - // return mClearEvent?.mSenderCurve25519Key ?: mSenderCurve25519Key } /** @@ -190,23 +123,13 @@ data class Event( */ fun getKeysClaimed(): Map { return mxDecryptionResult?.keysClaimed ?: HashMap() -// val res = HashMap() -// -// val claimedEd25519Key = if (null != mClearEvent) mClearEvent!!.mClaimedEd25519Key else mClaimedEd25519Key -// -// if (null != claimedEd25519Key) { -// res["ed25519"] = claimedEd25519Key -// } -// -// return res } -// + /** * @return the event type */ fun getClearType(): String { - return mxDecryptionResult?.payload?.get("type")?.toString() - ?: type//get("type")?.toString() ?: type + return mxDecryptionResult?.payload?.get("type")?.toString() ?: type } /** @@ -216,30 +139,8 @@ data class Event( return mxDecryptionResult?.payload?.get("content") as? Content ?: content } -// /** -// * @return the linked crypto error -// */ -// fun getCryptoError(): MXCryptoError? { -// return mCryptoError -// } -// -// /** -// * Update the linked crypto error -// * -// * @param error the new crypto error. -// */ -// fun setCryptoError(error: MXCryptoError?) { -// mCryptoError = error -// if (null != error) { -// mClearEvent = null -// } -// } - - fun toContentStringWithIndent(): String { - val contentMap = this.toContent()?.toMutableMap() ?: HashMap() - contentMap.remove("mxDecryptionResult") - contentMap.remove("mCryptoError") + val contentMap = toContent()?.toMutableMap() ?: HashMap() return JSONObject(contentMap).toString(4) } @@ -272,6 +173,7 @@ data class Event( if (redacts != other.redacts) return false if (mxDecryptionResult != other.mxDecryptionResult) return false if (mCryptoError != other.mCryptoError) return false + if (sendState != other.sendState) return false return true } @@ -289,6 +191,27 @@ data class Event( result = 31 * result + (redacts?.hashCode() ?: 0) result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0) result = 31 * result + (mCryptoError?.hashCode() ?: 0) + result = 31 * result + sendState.hashCode() return result } + +} + + +fun Event.isTextMessage(): Boolean { + return getClearType() == EventType.MESSAGE + && when (getClearContent()?.toModel()?.type) { + MessageType.MSGTYPE_TEXT, + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_NOTICE -> true + else -> false + } +} + +fun Event.isImageMessage(): Boolean { + return getClearType() == EventType.MESSAGE + && when (getClearContent()?.toModel()?.type) { + MessageType.MSGTYPE_IMAGE -> true + else -> false + } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt index 5b66ddd8..272ab567 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt @@ -30,20 +30,17 @@ interface RoomDirectoryService { /** * Get rooms from directory */ - fun getPublicRooms(server: String?, - publicRoomsParams: PublicRoomsParams, - callback: MatrixCallback): Cancelable + fun getPublicRooms(server: String?, publicRoomsParams: PublicRoomsParams, callback: MatrixCallback): Cancelable /** * Join a room by id */ - fun joinRoom(roomId: String, - callback: MatrixCallback) + fun joinRoom(roomId: String, callback: MatrixCallback): Cancelable /** * Fetches the overall metadata about protocols supported by the homeserver. * Includes both the available protocols and all fields required for queries against each protocol. */ - fun getThirdPartyProtocol(callback: MatrixCallback>) + fun getThirdPartyProtocol(callback: MatrixCallback>): Cancelable } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt index fc0bf499..7ec50bd2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt @@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams +import im.vector.matrix.android.api.util.Cancelable /** * This interface defines methods to get rooms. It's implemented at the session level. @@ -27,10 +28,18 @@ import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams interface RoomService { /** - * Create a room + * Create a room asynchronously */ - fun createRoom(createRoomParams: CreateRoomParams, - callback: MatrixCallback) + fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback): Cancelable + + /** + * Join a room by id + * @param roomId the roomId of the room to join + * @param viaServers the servers to attempt to join the room through. One of the servers must be participating in the room. + */ + fun joinRoom(roomId: String, + viaServers: List = emptyList(), + callback: MatrixCallback): Cancelable /** * Get a room from a roomId diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/failure/CreateRoomFailure.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/failure/CreateRoomFailure.kt new file mode 100644 index 00000000..086dc621 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/failure/CreateRoomFailure.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.failure + +import im.vector.matrix.android.api.failure.Failure + +sealed class CreateRoomFailure : Failure.FeatureFailure() { + + object CreatedWithTimeout: CreateRoomFailure() + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/failure/JoinRoomFailure.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/failure/JoinRoomFailure.kt new file mode 100644 index 00000000..4c7dd62a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/failure/JoinRoomFailure.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.failure + +import im.vector.matrix.android.api.failure.Failure + +sealed class JoinRoomFailure : Failure.FeatureFailure() { + + object JoinedWithTimeout : JoinRoomFailure() + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt index ca3b99b6..9dba26cb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt @@ -30,7 +30,7 @@ interface MembershipService { * This methods load all room members if it was done yet. * @return a [Cancelable] */ - fun loadRoomMembersIfNeeded(): Cancelable + fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback): Cancelable /** * Return the roomMember with userId or null. @@ -52,16 +52,17 @@ interface MembershipService { /** * Invite a user in the room */ - fun invite(userId: String, callback: MatrixCallback) + fun invite(userId: String, callback: MatrixCallback): Cancelable /** * Join the room, or accept an invitation. */ - fun join(callback: MatrixCallback) + + fun join(viaServers: List = emptyList(), callback: MatrixCallback): Cancelable /** * Leave the room, or reject an invitation. */ - fun leave(callback: MatrixCallback) + fun leave(callback: MatrixCallback): Cancelable } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt index b54f17db..aae72dd4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt @@ -34,5 +34,10 @@ data class RoomSummary( val notificationCount: Int = 0, val highlightCount: Int = 0, val tags: List = emptyList(), - val membership: Membership = Membership.NONE -) \ No newline at end of file + val membership: Membership = Membership.NONE, + val versioningState: VersioningState = VersioningState.NONE +) { + + val isVersioned: Boolean + get() = versioningState != VersioningState.NONE +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/VersioningState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/VersioningState.kt new file mode 100644 index 00000000..f2753fbf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/VersioningState.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.model + +enum class VersioningState { + NONE, + UPGRADED_ROOM_NOT_JOINED, + UPGRADED_ROOM_JOINED +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt index 77c4fcf1..94d6e0a8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt @@ -128,8 +128,8 @@ class CreateRoomParams { contentMap["algorithm"] = algorithm val algoEvent = Event(type = EventType.ENCRYPTION, - stateKey = "", - content = contentMap.toContent() + stateKey = "", + content = contentMap.toContent() ) if (null == initialStates) { @@ -162,8 +162,8 @@ class CreateRoomParams { contentMap["history_visibility"] = historyVisibility val historyVisibilityEvent = Event(type = EventType.STATE_HISTORY_VISIBILITY, - stateKey = "", - content = contentMap.toContent()) + stateKey = "", + content = contentMap.toContent()) if (null == initialStates) { initialStates = Arrays.asList(historyVisibilityEvent) @@ -202,8 +202,8 @@ class CreateRoomParams { */ fun isDirect(): Boolean { return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT - && isDirect == true - && (1 == getInviteCount() || 1 == getInvite3PidCount()) + && isDirect == true + && (1 == getInviteCount() || 1 == getInvite3PidCount()) } /** @@ -223,14 +223,13 @@ class CreateRoomParams { credentials: Credentials, ids: List) { for (id in ids) { - if (Patterns.EMAIL_ADDRESS.matcher(id).matches()) { + if (Patterns.EMAIL_ADDRESS.matcher(id).matches() && hsConfig.identityServerUri != null) { if (null == invite3pids) { invite3pids = ArrayList() } - val pid = Invite3Pid(idServer = hsConfig.identityServerUri.host!!, - medium = ThreePidMedium.EMAIL, - address = id) + medium = ThreePidMedium.EMAIL, + address = id) invite3pids!!.add(pid) } else if (isUserId(id)) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/Predecessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/Predecessor.kt new file mode 100644 index 00000000..960f9130 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/Predecessor.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.api.session.room.model.create + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * A link to an old room in case of room versioning + */ +@JsonClass(generateAdapter = true) +data class Predecessor( + @Json(name = "room_id") val roomId: String? = null, + @Json(name = "event_id") val eventId: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/RoomCreateContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/RoomCreateContent.kt new file mode 100644 index 00000000..afb318bc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/RoomCreateContent.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.model.create + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Content of a m.room.create type event + */ +@JsonClass(generateAdapter = true) +data class RoomCreateContent( + @Json(name = "creator") val creator: String? = null, + @Json(name = "room_version") val roomVersion: String? = null, + @Json(name = "predecessor") val predecessor: Predecessor? = null +) + + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt index bd32a75a..c116c6b3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt @@ -29,5 +29,5 @@ interface MessageContent { fun MessageContent?.isReply(): Boolean { - return this?.relatesTo?.inReplyTo != null -} \ No newline at end of file + return this?.relatesTo?.inReplyTo?.eventId != null +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReplyToContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReplyToContent.kt index 3df8a534..9ed629ac 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReplyToContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReplyToContent.kt @@ -21,5 +21,5 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class ReplyToContent( - @Json(name = "event_id") val eventId: String -) \ No newline at end of file + @Json(name = "event_id") val eventId: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tombstone/RoomTombstoneContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tombstone/RoomTombstoneContent.kt new file mode 100644 index 00000000..035e76d1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tombstone/RoomTombstoneContent.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.api.session.room.model.tombstone + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class to contains Tombstone information + */ +@JsonClass(generateAdapter = true) +data class RoomTombstoneContent( + @Json(name = "body") val body: String? = null, + @Json(name = "replacement_room") val replacementRoom: String? +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt index 94abd5d3..ae276adb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.api.session.room.send import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.Cancelable @@ -65,4 +66,31 @@ interface SendService { */ fun redactEvent(event: Event, reason: String?): Cancelable + + /** + * Schedule this message to be resent + * @param localEcho the unsent local echo + */ + fun resendTextMessage(localEcho: TimelineEvent): Cancelable? + + /** + * Schedule this message to be resent + * @param localEcho the unsent local echo + */ + fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? + + + /** + * Remove this failed message from the timeline + * @param localEcho the unsent local echo + */ + fun deleteFailedEcho(localEcho: TimelineEvent) + + fun clearSendingQueue() + + /** + * Resend all failed messages one by one (and keep order) + */ + fun resendAllFailedMessages() + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendState.kt index 75e3c0f6..aaa7020b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendState.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendState.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.api.session.room.send + enum class SendState { UNKNOWN, // the event has not been sent @@ -33,12 +34,19 @@ enum class SendState { // the event failed to be sent because some unknown devices have been found while encrypting it FAILED_UNKNOWN_DEVICES; - fun isSent(): Boolean { - return this == SENT || this == SYNCED + internal companion object { + val HAS_FAILED_STATES = listOf(UNDELIVERED, FAILED_UNKNOWN_DEVICES) + val IS_SENT_STATES = listOf(SENT, SYNCED) + val IS_SENDING_STATES = listOf(UNSENT, ENCRYPTING, SENDING) + val PENDING_STATES = IS_SENDING_STATES + HAS_FAILED_STATES } - fun hasFailed(): Boolean { - return this == UNDELIVERED || this == FAILED_UNKNOWN_DEVICES - } + fun isSent() = IS_SENT_STATES.contains(this) + + fun hasFailed() = HAS_FAILED_STATES.contains(this) + + fun isSending() = IS_SENDING_STATES.contains(this) } + + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt index 79443d0b..21aae95b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.room.state import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.events.model.Event interface StateService { @@ -25,4 +26,6 @@ interface StateService { */ fun updateTopic(topic: String, callback: MatrixCallback) + fun getStateEvent(eventType: String): Event? + } \ No newline at end of file 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 e52ac3b4..314c9f61 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 @@ -56,6 +56,9 @@ interface Timeline { */ fun paginate(direction: Direction, count: Int) + fun pendingEventCount() : Int + + fun failedToDeliverEventCount() : Int interface Listener { /** 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 044aa957..5d04d2f5 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 @@ -22,7 +22,6 @@ import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.isReply -import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent @@ -38,7 +37,6 @@ data class TimelineEvent( val senderName: String?, val isUniqueDisplayName: Boolean, val senderAvatar: String?, - val sendState: SendState, val annotations: EventAnnotationsSummary? = null ) { @@ -86,6 +84,12 @@ data class TimelineEvent( } } + +/** + * Tells if the event has been edited + */ +fun TimelineEvent.hasBeenEdited() = annotations?.editSummary != null + /** * Get last MessageContent, after a possible edition */ @@ -102,4 +106,4 @@ fun TimelineEvent.getTextEditableContent(): String? { } else { lastContent?.body ?: "" } -} \ No newline at end of file +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt index eb09fbd2..d3c58edd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt @@ -17,7 +17,10 @@ package im.vector.matrix.android.api.session.user import androidx.lifecycle.LiveData +import androidx.paging.PagedList +import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.api.util.Cancelable /** * This interface defines methods to get users. It's implemented at the session level. @@ -31,11 +34,34 @@ interface UserService { */ fun getUser(userId: String): User? + /** + * Search list of users on server directory. + * @param search the searched term + * @param limit the max number of users to return + * @param excludedUserIds the user ids to filter from the search + * @param callback the async callback + * @return Cancelable + */ + fun searchUsersDirectory(search: String, limit: Int, excludedUserIds: Set, callback: MatrixCallback>): Cancelable + /** * Observe a live user from a userId * @param userId the userId to look for. * @return a Livedata of user with userId */ - fun observeUser(userId: String): LiveData + fun liveUser(userId: String): LiveData + + /** + * Observe a live list of users sorted alphabetically + * @return a Livedata of users + */ + fun liveUsers(): LiveData> + + /** + * Observe a live [PagedList] of users sorted alphabetically. You can filter the users. + * @param filter the filter. It will look into userId and displayName. + * @return a Livedata of users + */ + fun livePagedUsers(filter: String? = null): LiveData> } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/CancelableBag.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/CancelableBag.kt index f5689b4d..145ddb66 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/CancelableBag.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/CancelableBag.kt @@ -19,5 +19,6 @@ package im.vector.matrix.android.api.util class CancelableBag : Cancelable, MutableList by ArrayList() { override fun cancel() { forEach { it.cancel() } + clear() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt index 81e64f5b..adea7c89 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt @@ -68,7 +68,9 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated callback: MatrixCallback): Cancelable { val job = GlobalScope.launch(coroutineDispatchers.main) { - val sessionOrFailure = authenticate(homeServerConnectionConfig, login, password) + val sessionOrFailure = runCatching { + authenticate(homeServerConnectionConfig, login, password) + } sessionOrFailure.foldToCallback(callback) } return CancelableCoroutine(job) @@ -85,16 +87,12 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated } else { PasswordLoginParams.userIdentifier(login, password, "Mobile") } - executeRequest { + val credentials = executeRequest { apiCall = authAPI.login(loginParams) - }.map { - val sessionParams = SessionParams(it, homeServerConnectionConfig) - sessionParamsStore.save(sessionParams) - sessionParams - }.map { - sessionManager.getOrCreateSession(it) } - + val sessionParams = SessionParams(credentials, homeServerConnectionConfig) + sessionParamsStore.save(sessionParams) + sessionManager.getOrCreateSession(sessionParams) } private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt index e7729d37..9067e818 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt @@ -27,7 +27,7 @@ internal interface SessionParamsStore { fun getAll(): List - fun save(sessionParams: SessionParams): Try + fun save(sessionParams: SessionParams): Try fun delete(userId: String): Try diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt index 1bb27d20..38771c91 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt @@ -62,7 +62,7 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S return sessionParams } - override fun save(sessionParams: SessionParams): Try { + override fun save(sessionParams: SessionParams): Try { return Try { val entity = mapper.map(sessionParams) if (entity != null) { @@ -72,7 +72,6 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S } realm.close() } - sessionParams } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt index 64303ea0..36a03ab1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt @@ -20,7 +20,6 @@ import com.squareup.moshi.Moshi import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.SessionParams -import im.vector.matrix.android.internal.di.MatrixScope import javax.inject.Inject internal class SessionParamsMapper @Inject constructor(moshi: Moshi) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt index 3fadb09b..96d337bd 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt @@ -72,7 +72,6 @@ import im.vector.matrix.android.internal.session.room.membership.RoomMembers import im.vector.matrix.android.internal.session.sync.model.SyncResponse import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith -import im.vector.matrix.android.internal.task.toConfigurableTask import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.fetchCopied @@ -167,22 +166,25 @@ internal class CryptoManager @Inject constructor( override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) { setDeviceNameTask - .configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) - .dispatchTo(callback) + .configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) { + this.callback = callback + } .executeBy(taskExecutor) } override fun deleteDevice(deviceId: String, callback: MatrixCallback) { deleteDeviceTask - .configureWith(DeleteDeviceTask.Params(deviceId)) - .dispatchTo(callback) + .configureWith(DeleteDeviceTask.Params(deviceId)) { + this.callback = callback + } .executeBy(taskExecutor) } override fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback) { deleteDeviceWithUserPasswordTask - .configureWith(DeleteDeviceWithUserPasswordTask.Params(deviceId, authSession, password)) - .dispatchTo(callback) + .configureWith(DeleteDeviceWithUserPasswordTask.Params(deviceId, authSession, password)) { + this.callback = callback + } .executeBy(taskExecutor) } @@ -196,8 +198,9 @@ internal class CryptoManager @Inject constructor( override fun getDevicesList(callback: MatrixCallback) { getDevicesTask - .toConfigurableTask() - .dispatchTo(callback) + .configureWith { + this.callback = callback + } .executeBy(taskExecutor) } @@ -254,35 +257,36 @@ internal class CryptoManager @Inject constructor( private suspend fun internalStart(isInitialSync: Boolean) { // Open the store cryptoStore.open() - uploadDeviceKeys() - .flatMap { oneTimeKeysUploader.maybeUploadOneTimeKeys() } - .fold( - { - Timber.e("Start failed: $it") - delay(1000) - isStarting.set(false) - internalStart(isInitialSync) - }, - { - outgoingRoomKeyRequestManager.start() - keysBackup.checkAndStartKeysBackup() - if (isInitialSync) { - // refresh the devices list for each known room members - deviceListManager.invalidateAllDeviceLists() - deviceListManager.refreshOutdatedDeviceLists() - } else { - incomingRoomKeyRequestManager.processReceivedRoomKeyRequests() - } - isStarting.set(false) - isStarted.set(true) - } - ) + runCatching { + uploadDeviceKeys() + oneTimeKeysUploader.maybeUploadOneTimeKeys() + outgoingRoomKeyRequestManager.start() + keysBackup.checkAndStartKeysBackup() + if (isInitialSync) { + // refresh the devices list for each known room members + deviceListManager.invalidateAllDeviceLists() + deviceListManager.refreshOutdatedDeviceLists() + } else { + incomingRoomKeyRequestManager.processReceivedRoomKeyRequests() + } + }.fold( + { + isStarting.set(false) + isStarted.set(true) + }, + { + Timber.e("Start failed: $it") + delay(1000) + isStarting.set(false) + internalStart(isInitialSync) + } + ) } /** * Close the crypto */ - fun close() { + fun close() = runBlocking(coroutineDispatchers.crypto) { olmDevice.release() cryptoStore.close() outgoingRoomKeyRequestManager.stop() @@ -556,13 +560,16 @@ internal class CryptoManager @Inject constructor( if (safeAlgorithm != null) { val t0 = System.currentTimeMillis() Timber.v("## encryptEventContent() starts") - safeAlgorithm.encryptEventContent(eventContent, eventType, userIds) + runCatching { + safeAlgorithm.encryptEventContent(eventContent, eventType, userIds) + } .fold( - { callback.onFailure(it) }, { Timber.v("## encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms") callback.onSuccess(MXEncryptEventContentResult(it, EventType.ENCRYPTED)) - } + }, + { callback.onFailure(it) } + ) } else { val algorithm = getEncryptionAlgorithm(roomId) @@ -584,10 +591,7 @@ internal class CryptoManager @Inject constructor( @Throws(MXCryptoError::class) override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { return runBlocking { - internalDecryptEvent(event, timeline).fold( - { throw it }, - { it } - ) + internalDecryptEvent(event, timeline) } } @@ -600,8 +604,10 @@ internal class CryptoManager @Inject constructor( */ override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback) { GlobalScope.launch { - val result = withContext(coroutineDispatchers.crypto) { - internalDecryptEvent(event, timeline) + val result = runCatching { + withContext(coroutineDispatchers.crypto) { + internalDecryptEvent(event, timeline) + } } result.foldToCallback(callback) } @@ -612,22 +618,22 @@ internal class CryptoManager @Inject constructor( * * @param event the raw event. * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. - * @return the MXEventDecryptionResult data, or null in case of error wrapped into [Try] + * @return the MXEventDecryptionResult data, or null in case of error */ - private suspend fun internalDecryptEvent(event: Event, timeline: String): Try { + private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { val eventContent = event.content - return if (eventContent == null) { + if (eventContent == null) { Timber.e("## decryptEvent : empty event content") - Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)) + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) } else { val algorithm = eventContent["algorithm"]?.toString() val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm) if (alg == null) { val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm) Timber.e("## decryptEvent() : $reason") - Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)) + throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) } else { - alg.decryptEvent(event, timeline) + return alg.decryptEvent(event, timeline) } } } @@ -689,12 +695,13 @@ internal class CryptoManager @Inject constructor( private fun onRoomEncryptionEvent(roomId: String, event: Event) { GlobalScope.launch(coroutineDispatchers.crypto) { val params = LoadRoomMembersTask.Params(roomId) - loadRoomMembersTask - .execute(params) - .map { _ -> - val userIds = getRoomUserIds(roomId) - setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds) - } + try { + loadRoomMembersTask.execute(params) + val userIds = getRoomUserIds(roomId) + setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds) + } catch (throwable: Throwable) { + Timber.e(throwable) + } } } @@ -761,7 +768,7 @@ internal class CryptoManager @Inject constructor( /** * Upload my user's device keys. */ - private suspend fun uploadDeviceKeys(): Try { + private suspend fun uploadDeviceKeys(): KeysUploadResponse { // Prepare the device keys data to send // Sign it val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, getMyDevice().signalableJSONDictionary()) @@ -868,10 +875,8 @@ internal class CryptoManager @Inject constructor( fun checkUnknownDevices(userIds: List, callback: MatrixCallback) { // force the refresh to ensure that the devices list is up-to-date GlobalScope.launch(coroutineDispatchers.crypto) { - deviceListManager - .downloadKeys(userIds, true) + runCatching { deviceListManager.downloadKeys(userIds, true) } .fold( - { callback.onFailure(it) }, { val unknownDevices = getUnknownDevices(it) if (unknownDevices.map.isEmpty()) { @@ -880,7 +885,8 @@ internal class CryptoManager @Inject constructor( // trigger an an unknown devices exception callback.onFailure(Failure.CryptoError(MXCryptoError.UnknownDevice(unknownDevices))) } - } + }, + { callback.onFailure(it) } ) } } @@ -1035,16 +1041,17 @@ internal class CryptoManager @Inject constructor( override fun downloadKeys(userIds: List, forceDownload: Boolean, callback: MatrixCallback>) { GlobalScope.launch(coroutineDispatchers.crypto) { - deviceListManager - .downloadKeys(userIds, forceDownload) - .foldToCallback(callback) + runCatching { + deviceListManager.downloadKeys(userIds, forceDownload) + }.foldToCallback(callback) } } override fun clearCryptoCache(callback: MatrixCallback) { clearCryptoDataTask - .toConfigurableTask() - .dispatchTo(callback) + .configureWith { + this.callback = callback + } .executeBy(taskExecutor) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt index e8f65b9d..e7cfdfe1 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt @@ -18,14 +18,12 @@ package im.vector.matrix.android.internal.crypto import android.text.TextUtils -import arrow.core.Try import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.tasks.DownloadKeysForUsersTask -import im.vector.matrix.android.internal.extensions.onError import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.sync.SyncTokenStore import timber.log.Timber @@ -237,7 +235,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM * @param forceDownload Always download the keys even if cached. * @param callback the asynchronous callback */ - suspend fun downloadKeys(userIds: List?, forceDownload: Boolean): Try> { + suspend fun downloadKeys(userIds: List?, forceDownload: Boolean): MXUsersDevicesMap { Timber.v("## downloadKeys() : forceDownload $forceDownload : $userIds") // Map from userId -> deviceId -> MXDeviceInfo val stored = MXUsersDevicesMap() @@ -268,16 +266,15 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM } return if (downloadUsers.isEmpty()) { Timber.v("## downloadKeys() : no new user device") - Try.just(stored) + stored } else { Timber.v("## downloadKeys() : starts") val t0 = System.currentTimeMillis() - doKeyDownloadForUsers(downloadUsers) - .map { - Timber.v("## downloadKeys() : doKeyDownloadForUsers succeeds after " + (System.currentTimeMillis() - t0) + " ms") - it.addEntriesFromMap(stored) - it - } + val result = doKeyDownloadForUsers(downloadUsers) + Timber.v("## downloadKeys() : doKeyDownloadForUsers succeeds after " + (System.currentTimeMillis() - t0) + " ms") + result.also { + it.addEntriesFromMap(stored) + } } } @@ -286,60 +283,60 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM * * @param downloadUsers the user ids list */ - private suspend fun doKeyDownloadForUsers(downloadUsers: MutableList): Try> { + private suspend fun doKeyDownloadForUsers(downloadUsers: MutableList): MXUsersDevicesMap { Timber.v("## doKeyDownloadForUsers() : doKeyDownloadForUsers $downloadUsers") // get the user ids which did not already trigger a keys download val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) } if (filteredUsers.isEmpty()) { // trigger nothing - return Try.just(MXUsersDevicesMap()) + return MXUsersDevicesMap() } val params = DownloadKeysForUsersTask.Params(filteredUsers, syncTokenStore.getLastToken()) - return downloadKeysForUsersTask.execute(params) - .map { response -> - Timber.v("## doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users") - for (userId in filteredUsers) { - val devices = response.deviceKeys?.get(userId) - Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $devices") - if (devices != null) { - val mutableDevices = HashMap(devices) - val deviceIds = ArrayList(mutableDevices.keys) - for (deviceId in deviceIds) { - // Get the potential previously store device keys for this device - val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(deviceId, userId) - val deviceInfo = mutableDevices[deviceId] + val response = try { + downloadKeysForUsersTask.execute(params) + } catch (throwable: Throwable) { + Timber.e(throwable, "##doKeyDownloadForUsers(): error") + onKeysDownloadFailed(filteredUsers) + throw throwable + } + Timber.v("## doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users") + for (userId in filteredUsers) { + val devices = response.deviceKeys?.get(userId) + Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $devices") + if (devices != null) { + val mutableDevices = HashMap(devices) + val deviceIds = ArrayList(mutableDevices.keys) + for (deviceId in deviceIds) { + // Get the potential previously store device keys for this device + val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(deviceId, userId) + val deviceInfo = mutableDevices[deviceId] - // in some race conditions (like unit tests) - // the self device must be seen as verified - if (TextUtils.equals(deviceInfo!!.deviceId, credentials.deviceId) && TextUtils.equals(userId, credentials.userId)) { - deviceInfo.verified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED - } - // Validate received keys - if (!validateDeviceKeys(deviceInfo, userId, deviceId, previouslyStoredDeviceKeys)) { - // New device keys are not valid. Do not store them - mutableDevices.remove(deviceId) - if (null != previouslyStoredDeviceKeys) { - // But keep old validated ones if any - mutableDevices[deviceId] = previouslyStoredDeviceKeys - } - } else if (null != previouslyStoredDeviceKeys) { - // The verified status is not sync'ed with hs. - // This is a client side information, valid only for this client. - // So, transfer its previous value - mutableDevices[deviceId]!!.verified = previouslyStoredDeviceKeys.verified - } - } - // Update the store - // Note that devices which aren't in the response will be removed from the stores - cryptoStore.storeUserDevices(userId, mutableDevices) - } + // in some race conditions (like unit tests) + // the self device must be seen as verified + if (TextUtils.equals(deviceInfo!!.deviceId, credentials.deviceId) && TextUtils.equals(userId, credentials.userId)) { + deviceInfo.verified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED + } + // Validate received keys + if (!validateDeviceKeys(deviceInfo, userId, deviceId, previouslyStoredDeviceKeys)) { + // New device keys are not valid. Do not store them + mutableDevices.remove(deviceId) + if (null != previouslyStoredDeviceKeys) { + // But keep old validated ones if any + mutableDevices[deviceId] = previouslyStoredDeviceKeys + } + } else if (null != previouslyStoredDeviceKeys) { + // The verified status is not sync'ed with hs. + // This is a client side information, valid only for this client. + // So, transfer its previous value + mutableDevices[deviceId]!!.verified = previouslyStoredDeviceKeys.verified } - onKeysDownloadSucceed(filteredUsers, response.failures) - } - .onError { - Timber.e(it, "##doKeyDownloadForUsers(): error") - onKeysDownloadFailed(filteredUsers) } + // Update the store + // Note that devices which aren't in the response will be removed from the stores + cryptoStore.storeUserDevices(userId, mutableDevices) + } + } + return onKeysDownloadSucceed(filteredUsers, response.failures) } /** @@ -465,15 +462,16 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM } cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) - doKeyDownloadForUsers(users) - .fold( - { - Timber.e(it, "## refreshOutdatedDeviceLists() : ERROR updating device keys for users $users") - }, - { - Timber.v("## refreshOutdatedDeviceLists() : done") - } - ) + runCatching { + doKeyDownloadForUsers(users) + }.fold( + { + Timber.v("## refreshOutdatedDeviceLists() : done") + }, + { + Timber.e(it, "## refreshOutdatedDeviceLists() : ERROR updating device keys for users $users") + } + ) } companion object { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt index 8f0bfcd4..7c5131fc 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt @@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.crypto import android.text.TextUtils -import arrow.core.Try import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE import im.vector.matrix.android.api.util.JsonDict @@ -80,8 +79,7 @@ internal class MXOlmDevice @Inject constructor( // // The first level keys are timeline ids. // The second level keys are strings of form "||" - // Values are true. - private val inboundGroupSessionMessageIndexes: MutableMap> = HashMap() + private val inboundGroupSessionMessageIndexes: MutableMap> = HashMap() init { // Retrieve the account from the store @@ -506,25 +504,25 @@ internal class MXOlmDevice @Inject constructor( keysClaimed: Map, exportFormat: Boolean): Boolean { val session = OlmInboundGroupSessionWrapper(sessionKey, exportFormat) + runCatching { getInboundGroupSession(sessionId, senderKey, roomId) } + .fold( + { + // If we already have this session, consider updating it + Timber.e("## addInboundGroupSession() : Update for megolm session $senderKey/$sessionId") - getInboundGroupSession(sessionId, senderKey, roomId).fold( - { - // Nothing to do in case of error - }, - { - // If we already have this session, consider updating it - Timber.e("## addInboundGroupSession() : Update for megolm session $senderKey/$sessionId") + val existingFirstKnown = it.firstKnownIndex!! + val newKnownFirstIndex = session.firstKnownIndex - val existingFirstKnown = it.firstKnownIndex!! - val newKnownFirstIndex = session.firstKnownIndex - - //If our existing session is better we keep it - if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) { - session.olmInboundGroupSession?.releaseSession() - return false - } - } - ) + //If our existing session is better we keep it + if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) { + session.olmInboundGroupSession?.releaseSession() + return false + } + }, + { + // Nothing to do in case of error + } + ) // sanity check if (null == session.olmInboundGroupSession) { @@ -595,12 +593,8 @@ internal class MXOlmDevice @Inject constructor( continue } - getInboundGroupSession(sessionId, senderKey, roomId) + runCatching { getInboundGroupSession(sessionId, senderKey, roomId) } .fold( - { - // Session does not already exist, add it - sessions.add(session) - }, { // If we already have this session, consider updating it Timber.e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") @@ -613,7 +607,12 @@ internal class MXOlmDevice @Inject constructor( sessions.add(session) } Unit + }, + { + // Session does not already exist, add it + sessions.add(session) } + ) } @@ -648,61 +647,55 @@ internal class MXOlmDevice @Inject constructor( roomId: String, timeline: String?, sessionId: String, - senderKey: String): Try { - return getInboundGroupSession(sessionId, senderKey, roomId) - .flatMap { session -> - // Check that the room id matches the original one for the session. This stops - // the HS pretending a message was targeting a different room. - if (roomId == session.roomId) { - var decryptResult: OlmInboundGroupSession.DecryptMessageResult? = null - try { - decryptResult = session.olmInboundGroupSession!!.decryptMessage(body) - } catch (e: OlmException) { - Timber.e(e, "## decryptGroupMessage () : decryptMessage failed") - return@flatMap Try.Failure(MXCryptoError.OlmError(e)) - } + senderKey: String): OlmDecryptionResult { + val session = getInboundGroupSession(sessionId, senderKey, roomId) + // Check that the room id matches the original one for the session. This stops + // the HS pretending a message was targeting a different room. + if (roomId == session.roomId) { + var decryptResult: OlmInboundGroupSession.DecryptMessageResult? = null + try { + decryptResult = session.olmInboundGroupSession!!.decryptMessage(body) + } catch (e: OlmException) { + Timber.e(e, "## decryptGroupMessage () : decryptMessage failed") + throw MXCryptoError.OlmError(e) + } - if (null != timeline) { - if (!inboundGroupSessionMessageIndexes.containsKey(timeline)) { - inboundGroupSessionMessageIndexes[timeline] = HashMap() - } + if (null != timeline) { + val timelineSet = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableSetOf() } - val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex + val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex - if (inboundGroupSessionMessageIndexes[timeline]?.get(messageIndexKey) != null) { - val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex) - Timber.e("## decryptGroupMessage() : $reason") - return@flatMap Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)) - } - - inboundGroupSessionMessageIndexes[timeline]!!.put(messageIndexKey, true) - } - - store.storeInboundGroupSessions(listOf(session)) - val payload = try { - val adapter = MoshiProvider.providesMoshi().adapter(JSON_DICT_PARAMETERIZED_TYPE) - val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage) - adapter.fromJson(payloadString) - } catch (e: Exception) { - Timber.e("## decryptGroupMessage() : fails to parse the payload") - return@flatMap Try.Failure( - MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)) - } - - return@flatMap Try.just( - OlmDecryptionResult( - payload, - session.keysClaimed, - senderKey, - session.forwardingCurve25519KeyChain - ) - ) - } else { - val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId) - Timber.e("## decryptGroupMessage() : $reason") - return@flatMap Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason)) - } + if (timelineSet.contains(messageIndexKey)) { + val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex) + Timber.e("## decryptGroupMessage() : $reason") + throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason) } + + timelineSet.add(messageIndexKey) + } + + store.storeInboundGroupSessions(listOf(session)) + val payload = try { + val adapter = MoshiProvider.providesMoshi().adapter(JSON_DICT_PARAMETERIZED_TYPE) + val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage) + adapter.fromJson(payloadString) + } catch (e: Exception) { + Timber.e("## decryptGroupMessage() : fails to parse the payload") + throw + MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON) + } + + return OlmDecryptionResult( + payload, + session.keysClaimed, + senderKey, + session.forwardingCurve25519KeyChain + ) + } else { + val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId) + Timber.e("## decryptGroupMessage() : $reason") + throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason) + } } /** @@ -766,26 +759,26 @@ internal class MXOlmDevice @Inject constructor( * @param senderKey the base64-encoded curve25519 key of the sender. * @return the inbound group session. */ - fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): Try { + fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): OlmInboundGroupSessionWrapper { if (sessionId.isNullOrBlank() || senderKey.isNullOrBlank()) { - return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON)) + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON) } val session = store.getInboundGroupSession(sessionId, senderKey) - return if (null != session) { + if (session != null) { // Check that the room id matches the original one for the session. This stops // the HS pretending a message was targeting a different room. if (!TextUtils.equals(roomId, session.roomId)) { val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId) Timber.e("## getInboundGroupSession() : $errorDescription") - Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription)) + throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription) } else { - Try.just(session) + return session } } else { Timber.e("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId") - Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON)) + throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON) } } @@ -798,6 +791,6 @@ internal class MXOlmDevice @Inject constructor( * @return true if the unbound session keys are known. */ fun hasInboundSessionKeys(roomId: String, senderKey: String, sessionId: String): Boolean { - return getInboundGroupSession(sessionId, senderKey, roomId).isSuccess() + return runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }.isSuccess } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/ObjectSigner.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/ObjectSigner.kt index 7b8e0199..29e20290 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/ObjectSigner.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/ObjectSigner.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.crypto import im.vector.matrix.android.api.auth.data.Credentials -import im.vector.matrix.android.internal.session.SessionScope import java.util.* import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt index a81cbfcf..f0d7662f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OneTimeKeysUploader.kt @@ -16,8 +16,6 @@ package im.vector.matrix.android.internal.crypto -import arrow.core.Try -import arrow.instances.`try`.applicativeError.handleError import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.crypto.model.MXKey import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse @@ -59,13 +57,13 @@ internal class OneTimeKeysUploader @Inject constructor( /** * Check if the OTK must be uploaded. */ - suspend fun maybeUploadOneTimeKeys(): Try { + suspend fun maybeUploadOneTimeKeys() { if (oneTimeKeyCheckInProgress) { - return Try.just(Unit) + return } if (System.currentTimeMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) { // we've done a key upload recently. - return Try.just(Unit) + return } lastOneTimeKeyCheck = System.currentTimeMillis() @@ -81,41 +79,31 @@ internal class OneTimeKeysUploader @Inject constructor( // discard the oldest private keys first. This will eventually clean // out stale private keys that won't receive a message. val keyLimit = Math.floor(maxOneTimeKeys / 2.0).toInt() - val result = if (oneTimeKeyCount != null) { + if (oneTimeKeyCount != null) { uploadOTK(oneTimeKeyCount!!, keyLimit) } else { // ask the server how many keys we have val uploadKeysParams = UploadKeysTask.Params(null, null, credentials.deviceId!!) - uploadKeysTask.execute(uploadKeysParams) - .flatMap { - // We need to keep a pool of one time public keys on the server so that - // other devices can start conversations with us. But we can only store - // a finite number of private keys in the olm Account object. - // To complicate things further then can be a delay between a device - // claiming a public one time key from the server and it sending us a - // message. We need to keep the corresponding private key locally until - // we receive the message. - // But that message might never arrive leaving us stuck with duff - // private keys clogging up our local storage. - // So we need some kind of engineering compromise to balance all of - // these factors. - // TODO Why we do not set oneTimeKeyCount here? - // TODO This is not needed anymore, see https://github.com/matrix-org/matrix-js-sdk/pull/493 (TODO on iOS also) - val keyCount = it.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE) - uploadOTK(keyCount, keyLimit) - } + val response = uploadKeysTask.execute(uploadKeysParams) + // We need to keep a pool of one time public keys on the server so that + // other devices can start conversations with us. But we can only store + // a finite number of private keys in the olm Account object. + // To complicate things further then can be a delay between a device + // claiming a public one time key from the server and it sending us a + // message. We need to keep the corresponding private key locally until + // we receive the message. + // But that message might never arrive leaving us stuck with duff + // private keys clogging up our local storage. + // So we need some kind of engineering compromise to balance all of + // these factors. + // TODO Why we do not set oneTimeKeyCount here? + // TODO This is not needed anymore, see https://github.com/matrix-org/matrix-js-sdk/pull/493 (TODO on iOS also) + val keyCount = response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE) + uploadOTK(keyCount, keyLimit) } - return result - .map { - Timber.v("## uploadKeys() : success") - oneTimeKeyCount = null - oneTimeKeyCheckInProgress = false - } - .handleError { - Timber.e(it, "## uploadKeys() : failed") - oneTimeKeyCount = null - oneTimeKeyCheckInProgress = false - } + Timber.v("## uploadKeys() : success") + oneTimeKeyCount = null + oneTimeKeyCheckInProgress = false } /** @@ -124,29 +112,26 @@ internal class OneTimeKeysUploader @Inject constructor( * @param keyCount the key count * @param keyLimit the limit */ - private suspend fun uploadOTK(keyCount: Int, keyLimit: Int): Try { + private suspend fun uploadOTK(keyCount: Int, keyLimit: Int) { if (keyLimit <= keyCount) { // If we don't need to generate any more keys then we are done. - return Try.just(Unit) + return } - val keysThisLoop = Math.min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER) olmDevice.generateOneTimeKeys(keysThisLoop) - return uploadOneTimeKeys() - .flatMap { - if (it.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) { - uploadOTK(it.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit) - } else { - Timber.e("## uploadLoop() : response for uploading keys does not contain one_time_key_counts.signed_curve25519") - Try.raise(Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519")) - } - } + val response = uploadOneTimeKeys() + if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) { + uploadOTK(response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit) + } else { + Timber.e("## uploadLoop() : response for uploading keys does not contain one_time_key_counts.signed_curve25519") + throw Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519") + } } /** * Upload my user's one time keys. */ - private suspend fun uploadOneTimeKeys(): Try { + private suspend fun uploadOneTimeKeys(): KeysUploadResponse { val oneTimeKeys = olmDevice.getOneTimeKeys() val oneTimeJson = HashMap() @@ -169,13 +154,10 @@ internal class OneTimeKeysUploader @Inject constructor( // For now, we set the device id explicitly, as we may not be using the // same one as used in login. val uploadParams = UploadKeysTask.Params(null, oneTimeJson, credentials.deviceId!!) - return uploadKeysTask - .execute(uploadParams) - .map { - lastPublishedOneTimeKeys = oneTimeKeys - olmDevice.markKeysAsPublished() - it - } + val response = uploadKeysTask.execute(uploadParams) + lastPublishedOneTimeKeys = oneTimeKeys + olmDevice.markKeysAsPublished() + return response } companion object { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt index a13ae75b..c0702f70 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt @@ -299,10 +299,12 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor( // TODO Change this two hard coded key to something better contentMap.setObject(recipient["userId"], recipient["deviceId"], message) } - sendToDeviceTask.configureWith(SendToDeviceTask.Params(EventType.ROOM_KEY_REQUEST, contentMap, transactionId)) - .dispatchTo(callback) - .executeOn(TaskThread.CALLER) - .callbackOn(TaskThread.CALLER) + sendToDeviceTask + .configureWith(SendToDeviceTask.Params(EventType.ROOM_KEY_REQUEST, contentMap, transactionId)) { + this.callback = callback + this.callbackThread = TaskThread.CALLER + this.executionThread = TaskThread.CALLER + } .executeBy(taskExecutor) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt index 4c4ee106..7a56f46b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.crypto.actions import android.text.TextUtils -import arrow.core.Try import im.vector.matrix.android.internal.crypto.MXOlmDevice import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXKey @@ -32,7 +31,7 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) { - suspend fun handle(devicesByUser: Map>): Try> { + suspend fun handle(devicesByUser: Map>): MXUsersDevicesMap { val devicesWithoutSession = ArrayList() val results = MXUsersDevicesMap() @@ -58,7 +57,7 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val } if (devicesWithoutSession.size == 0) { - return Try.just(results) + return results } // Prepare the request for claiming one-time keys @@ -79,39 +78,36 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val Timber.v("## claimOneTimeKeysForUsersDevices() : $usersDevicesToClaim") val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim) - return oneTimeKeysForUsersDeviceTask - .execute(claimParams) - .map { - Timber.v("## claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $it") - for (userId in userIds) { - val deviceInfos = devicesByUser[userId] - for (deviceInfo in deviceInfos!!) { - var oneTimeKey: MXKey? = null - val deviceIds = it.getUserDeviceIds(userId) - if (null != deviceIds) { - for (deviceId in deviceIds) { - val olmSessionResult = results.getObject(userId, deviceId) - if (olmSessionResult!!.sessionId != null) { - // We already have a result for this device - continue - } - val key = it.getObject(userId, deviceId) - if (key?.type == oneTimeKeyAlgorithm) { - oneTimeKey = key - } - if (oneTimeKey == null) { - Timber.v("## ensureOlmSessionsForDevices() : No one-time keys " + oneTimeKeyAlgorithm - + " for device " + userId + " : " + deviceId) - continue - } - // Update the result for this device in results - olmSessionResult.sessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo) - } - } + val oneTimeKeys = oneTimeKeysForUsersDeviceTask.execute(claimParams) + Timber.v("## claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys") + for (userId in userIds) { + val deviceInfos = devicesByUser[userId] + for (deviceInfo in deviceInfos!!) { + var oneTimeKey: MXKey? = null + val deviceIds = oneTimeKeys.getUserDeviceIds(userId) + if (null != deviceIds) { + for (deviceId in deviceIds) { + val olmSessionResult = results.getObject(userId, deviceId) + if (olmSessionResult!!.sessionId != null) { + // We already have a result for this device + continue } + val key = oneTimeKeys.getObject(userId, deviceId) + if (key?.type == oneTimeKeyAlgorithm) { + oneTimeKey = key + } + if (oneTimeKey == null) { + Timber.v("## ensureOlmSessionsForDevices() : No one-time keys " + oneTimeKeyAlgorithm + + " for device " + userId + " : " + deviceId) + continue + } + // Update the result for this device in results + olmSessionResult.sessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo) } - results } + } + } + return results } private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: MXDeviceInfo): String? { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt index f6817e14..840a66c5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt @@ -17,14 +17,11 @@ package im.vector.matrix.android.internal.crypto.actions import android.text.TextUtils -import arrow.core.Try -import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.internal.crypto.MXOlmDevice import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXOlmSessionResult import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore -import im.vector.matrix.android.internal.session.SessionScope import timber.log.Timber import java.util.* import javax.inject.Inject @@ -37,7 +34,7 @@ internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val o * Try to make sure we have established olm sessions for the given users. * @param users a list of user ids. */ - suspend fun handle(users: List) : Try> { + suspend fun handle(users: List) : MXUsersDevicesMap { Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users") val devicesByUser = HashMap>() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MegolmSessionDataImporter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MegolmSessionDataImporter.kt index 918bdd73..9d345dfb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MegolmSessionDataImporter.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MegolmSessionDataImporter.kt @@ -26,7 +26,6 @@ import im.vector.matrix.android.internal.crypto.RoomDecryptorProvider import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore -import im.vector.matrix.android.internal.session.SessionScope import timber.log.Timber import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt index dfc54ffe..7fc39312 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt @@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.crypto.actions import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore -import im.vector.matrix.android.internal.session.SessionScope import timber.log.Timber import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt index 8714e156..f63eaa93 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.crypto.algorithms -import arrow.core.Try import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult @@ -35,7 +34,7 @@ internal interface IMXDecrypting { * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. * @return the decryption information, or an error */ - suspend fun decryptEvent(event: Event, timeline: String): Try + suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult /** * Handle a key event. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXEncrypting.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXEncrypting.kt index 544bbe60..555ce9df 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXEncrypting.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXEncrypting.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.crypto.algorithms -import arrow.core.Try import im.vector.matrix.android.api.session.events.model.Content /** @@ -31,7 +30,7 @@ internal interface IMXEncrypting { * @param eventContent the content of the event. * @param eventType the type of the event. * @param userIds the room members the event will be sent to. - * @return the encrypted content wrapped by [Try] + * @return the encrypted content */ - suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List): Try + suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List): Content } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index b30176b2..47bf9956 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.crypto.algorithms.megolm import android.text.TextUtils -import arrow.core.Try import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.events.model.Event @@ -40,7 +39,6 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import timber.log.Timber -import kotlin.collections.HashMap internal class MXMegolmDecryption(private val credentials: Credentials, private val olmDevice: MXOlmDevice, @@ -61,30 +59,46 @@ internal class MXMegolmDecryption(private val credentials: Credentials, */ private var pendingEvents: MutableMap>> = HashMap() - override suspend fun decryptEvent(event: Event, timeline: String): Try { + override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { return decryptEvent(event, timeline, true) } - private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): Try { + private suspend fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult { if (event.roomId.isNullOrBlank()) { - return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)) + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) } val encryptedEventContent = event.content.toModel() - ?: return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)) + ?: throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) if (encryptedEventContent.senderKey.isNullOrBlank() || encryptedEventContent.sessionId.isNullOrBlank() || encryptedEventContent.ciphertext.isNullOrBlank()) { - return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)) + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) } - return olmDevice.decryptGroupMessage(encryptedEventContent.ciphertext, - event.roomId, - timeline, - encryptedEventContent.sessionId, - encryptedEventContent.senderKey) + return runCatching { + olmDevice.decryptGroupMessage(encryptedEventContent.ciphertext, + event.roomId, + timeline, + encryptedEventContent.sessionId, + encryptedEventContent.senderKey) + } .fold( + { olmDecryptionResult -> + // the decryption succeeds + if (olmDecryptionResult.payload != null) { + MXEventDecryptionResult( + clearEvent = olmDecryptionResult.payload, + senderCurve25519Key = olmDecryptionResult.senderKey, + claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"), + forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain + ?: emptyList() + ) + } else { + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) + } + }, { throwable -> if (throwable is MXCryptoError.OlmError) { // TODO Check the value of .message @@ -98,10 +112,10 @@ internal class MXMegolmDecryption(private val credentials: Credentials, val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message) val detailedReason = String.format(MXCryptoError.DETAILED_OLM_REASON, encryptedEventContent.ciphertext, reason) - Try.Failure(MXCryptoError.Base( + throw MXCryptoError.Base( MXCryptoError.ErrorType.OLM, reason, - detailedReason)) + detailedReason) } if (throwable is MXCryptoError.Base) { if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { @@ -111,23 +125,7 @@ internal class MXMegolmDecryption(private val credentials: Credentials, } } } - - Try.Failure(throwable) - }, - { olmDecryptionResult -> - // the decryption succeeds - if (olmDecryptionResult.payload != null) { - Try.just( - MXEventDecryptionResult( - clearEvent = olmDecryptionResult.payload, - senderCurve25519Key = olmDecryptionResult.senderKey, - claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"), - forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain ?: emptyList() - ) - ) - } else { - Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)) - } + throw throwable } ) } @@ -311,51 +309,48 @@ internal class MXMegolmDecryption(private val credentials: Credentials, } val userId = request.userId ?: return GlobalScope.launch(coroutineDispatchers.crypto) { - deviceListManager - .downloadKeys(listOf(userId), false) - .flatMap { + runCatching { deviceListManager.downloadKeys(listOf(userId), false) } + .mapCatching { val deviceId = request.deviceId val deviceInfo = cryptoStore.getUserDevice(deviceId ?: "", userId) if (deviceInfo == null) { throw RuntimeException() } else { val devicesByUser = mapOf(userId to listOf(deviceInfo)) - ensureOlmSessionsForDevicesAction - .handle(devicesByUser) - .flatMap { - val body = request.requestBody - val olmSessionResult = it.getObject(userId, deviceId) - if (olmSessionResult?.sessionId == null) { - // no session with this device, probably because there - // were no one-time keys. - Try.just(Unit) - } - Timber.v("## shareKeysWithDevice() : sharing keys for session" + - " ${body?.senderKey}|${body?.sessionId} with device $userId:$deviceId") + val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser) + val body = request.requestBody + val olmSessionResult = usersDeviceMap.getObject(userId, deviceId) + if (olmSessionResult?.sessionId == null) { + // no session with this device, probably because there + // were no one-time keys. + return@mapCatching + } + Timber.v("## shareKeysWithDevice() : sharing keys for session" + + " ${body?.senderKey}|${body?.sessionId} with device $userId:$deviceId") - val payloadJson = mutableMapOf("type" to EventType.FORWARDED_ROOM_KEY) + val payloadJson = mutableMapOf("type" to EventType.FORWARDED_ROOM_KEY) + runCatching { olmDevice.getInboundGroupSession(body?.sessionId, body?.senderKey, body?.roomId) } + .fold( + { + // TODO + payloadJson["content"] = it.exportKeys() + ?: "" + }, + { + // TODO + } - olmDevice.getInboundGroupSession(body?.sessionId, body?.senderKey, body?.roomId) - .fold( - { - // TODO - }, - { - // TODO - payloadJson["content"] = it.exportKeys() ?: "" - } - ) + ) - val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) - val sendToDeviceMap = MXUsersDevicesMap() - sendToDeviceMap.setObject(userId, deviceId, encodedPayload) - Timber.v("## shareKeysWithDevice() : sending to $userId:$deviceId") - val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - sendToDeviceTask.execute(sendToDeviceParams) - } + val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) + val sendToDeviceMap = MXUsersDevicesMap() + sendToDeviceMap.setObject(userId, deviceId, encodedPayload) + Timber.v("## shareKeysWithDevice() : sending to $userId:$deviceId") + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) + sendToDeviceTask.execute(sendToDeviceParams) } } } } - } + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt index 43c485df..eb8df7b9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt @@ -24,7 +24,6 @@ import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevi import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 165fc736..b465cb6e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.crypto.algorithms.megolm import android.text.TextUtils -import arrow.core.Try import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.events.model.Content @@ -69,12 +68,10 @@ internal class MXMegolmEncryption( override suspend fun encryptEventContent(eventContent: Content, eventType: String, - userIds: List): Try { - return getDevicesInRoom(userIds) - .flatMap { ensureOutboundSession(it) } - .flatMap { - encryptContent(it, eventType, eventContent) - } + userIds: List): Content { + val devices = getDevicesInRoom(userIds) + val outboundSession = ensureOutboundSession(devices) + return encryptContent(outboundSession, eventType, eventContent) } /** @@ -101,7 +98,7 @@ internal class MXMegolmEncryption( * * @param devicesInRoom the devices list */ - private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap): Try { + private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap): MXOutboundSessionInfo { var session = outboundSession if (session == null // Need to make a brand new session? @@ -126,7 +123,8 @@ internal class MXMegolmEncryption( } } } - return shareKey(safeSession, shareMap).map { safeSession!! } + shareKey(safeSession, shareMap) + return safeSession } /** @@ -136,11 +134,11 @@ internal class MXMegolmEncryption( * @param devicesByUsers the devices map */ private suspend fun shareKey(session: MXOutboundSessionInfo, - devicesByUsers: Map>): Try { + devicesByUsers: Map>) { // nothing to send, the task is done if (devicesByUsers.isEmpty()) { Timber.v("## shareKey() : nothing more to do") - return Try.just(Unit) + return } // reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user) val subMap = HashMap>() @@ -157,11 +155,9 @@ internal class MXMegolmEncryption( } } Timber.v("## shareKey() ; userId $userIds") - return shareUserDevicesKey(session, subMap) - .flatMap { - val remainingDevices = devicesByUsers.filterKeys { userIds.contains(it).not() } - shareKey(session, remainingDevices) - } + shareUserDevicesKey(session, subMap) + val remainingDevices = devicesByUsers.filterKeys { userIds.contains(it).not() } + shareKey(session, remainingDevices) } /** @@ -172,7 +168,7 @@ internal class MXMegolmEncryption( * @param callback the asynchronous callback */ private suspend fun shareUserDevicesKey(session: MXOutboundSessionInfo, - devicesByUser: Map>): Try { + devicesByUser: Map>) { val sessionKey = olmDevice.getSessionKey(session.sessionId) val chainIndex = olmDevice.getMessageIndex(session.sessionId) @@ -190,94 +186,86 @@ internal class MXMegolmEncryption( var t0 = System.currentTimeMillis() Timber.v("## shareUserDevicesKey() : starts") - return ensureOlmSessionsForDevicesAction.handle(devicesByUser) - .flatMap { - Timber.v("## shareUserDevicesKey() : ensureOlmSessionsForDevices succeeds after " - + (System.currentTimeMillis() - t0) + " ms") - val contentMap = MXUsersDevicesMap() - var haveTargets = false - val userIds = it.userIds - for (userId in userIds) { - val devicesToShareWith = devicesByUser[userId] - for ((deviceID) in devicesToShareWith!!) { - val sessionResult = it.getObject(userId, deviceID) - if (sessionResult?.sessionId == null) { - // no session with this device, probably because there - // were no one-time keys. - // - // we could send them a to_device message anyway, as a - // signal that they have missed out on the key sharing - // message because of the lack of keys, but there's not - // much point in that really; it will mostly serve to clog - // up to_device inboxes. - // - // ensureOlmSessionsForUsers has already done the logging, - // so just skip it. - continue - } - Timber.v("## shareUserDevicesKey() : Sharing keys with device $userId:$deviceID") - //noinspection ArraysAsListWithZeroOrOneArgument,ArraysAsListWithZeroOrOneArgument - contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, Arrays.asList(sessionResult.deviceInfo))) - haveTargets = true - } - } - if (haveTargets) { - t0 = System.currentTimeMillis() - Timber.v("## shareUserDevicesKey() : has target") - val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap) - sendToDeviceTask.execute(sendToDeviceParams) - .map { - Timber.v("## shareUserDevicesKey() : sendToDevice succeeds after " - + (System.currentTimeMillis() - t0) + " ms") - - // Add the devices we have shared with to session.sharedWithDevices. - // we deliberately iterate over devicesByUser (ie, the devices we - // attempted to share with) rather than the contentMap (those we did - // share with), because we don't want to try to claim a one-time-key - // for dead devices on every message. - for (userId in devicesByUser.keys) { - val devicesToShareWith = devicesByUser[userId] - for ((deviceId) in devicesToShareWith!!) { - session.sharedWithDevices.setObject(userId, deviceId, chainIndex) - } - } - Unit - } - } else { - Timber.v("## shareUserDevicesKey() : no need to sharekey") - Try.just(Unit) - } + val results = ensureOlmSessionsForDevicesAction.handle(devicesByUser) + Timber.v("## shareUserDevicesKey() : ensureOlmSessionsForDevices succeeds after " + + (System.currentTimeMillis() - t0) + " ms") + val contentMap = MXUsersDevicesMap() + var haveTargets = false + val userIds = results.userIds + for (userId in userIds) { + val devicesToShareWith = devicesByUser[userId] + for ((deviceID) in devicesToShareWith!!) { + val sessionResult = results.getObject(userId, deviceID) + if (sessionResult?.sessionId == null) { + // no session with this device, probably because there + // were no one-time keys. + // + // we could send them a to_device message anyway, as a + // signal that they have missed out on the key sharing + // message because of the lack of keys, but there's not + // much point in that really; it will mostly serve to clog + // up to_device inboxes. + // + // ensureOlmSessionsForUsers has already done the logging, + // so just skip it. + continue } + Timber.v("## shareUserDevicesKey() : Sharing keys with device $userId:$deviceID") + //noinspection ArraysAsListWithZeroOrOneArgument,ArraysAsListWithZeroOrOneArgument + contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, Arrays.asList(sessionResult.deviceInfo))) + haveTargets = true + } + } + if (haveTargets) { + t0 = System.currentTimeMillis() + Timber.v("## shareUserDevicesKey() : has target") + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap) + sendToDeviceTask.execute(sendToDeviceParams) + Timber.v("## shareUserDevicesKey() : sendToDevice succeeds after " + + (System.currentTimeMillis() - t0) + " ms") + + // Add the devices we have shared with to session.sharedWithDevices. + // we deliberately iterate over devicesByUser (ie, the devices we + // attempted to share with) rather than the contentMap (those we did + // share with), because we don't want to try to claim a one-time-key + // for dead devices on every message. + for (userId in devicesByUser.keys) { + val devicesToShareWith = devicesByUser[userId] + for ((deviceId) in devicesToShareWith!!) { + session.sharedWithDevices.setObject(userId, deviceId, chainIndex) + } + } + } else { + Timber.v("## shareUserDevicesKey() : no need to sharekey") + } } /** * process the pending encryptions */ - private fun encryptContent(session: MXOutboundSessionInfo, eventType: String, eventContent: Content): Try { - return Try { - // Everything is in place, encrypt all pending events - val payloadJson = HashMap() - payloadJson["room_id"] = roomId - payloadJson["type"] = eventType - payloadJson["content"] = eventContent + private fun encryptContent(session: MXOutboundSessionInfo, eventType: String, eventContent: Content): Content { + // Everything is in place, encrypt all pending events + val payloadJson = HashMap() + payloadJson["room_id"] = roomId + payloadJson["type"] = eventType + payloadJson["content"] = eventContent - // Get canonical Json from + // Get canonical Json from - val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson)) - val ciphertext = olmDevice.encryptGroupMessage(session.sessionId, payloadString!!) + val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson)) + val ciphertext = olmDevice.encryptGroupMessage(session.sessionId, payloadString!!) - val map = HashMap() - map["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM - map["sender_key"] = olmDevice.deviceCurve25519Key!! - map["ciphertext"] = ciphertext!! - map["session_id"] = session.sessionId + val map = HashMap() + map["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM + map["sender_key"] = olmDevice.deviceCurve25519Key!! + map["ciphertext"] = ciphertext!! + map["session_id"] = session.sessionId - // Include our device ID so that recipients can send us a - // m.new_device message if they don't have our session key. - map["device_id"] = credentials.deviceId!! - session.useCount++ - map - } + // Include our device ID so that recipients can send us a + // m.new_device message if they don't have our session key. + map["device_id"] = credentials.deviceId!! + session.useCount++ + return map } /** @@ -287,50 +275,47 @@ internal class MXMegolmEncryption( * @param userIds the user ids whose devices must be checked. * @param callback the asynchronous callback */ - private suspend fun getDevicesInRoom(userIds: List): Try> { + private suspend fun getDevicesInRoom(userIds: List): MXUsersDevicesMap { // We are happy to use a cached version here: we assume that if we already // have a list of the user's devices, then we already share an e2e room // with them, which means that they will have announced any new devices via // an m.new_device. - return deviceListManager - .downloadKeys(userIds, false) - .flatMap { - val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices() - || cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId) + val keys = deviceListManager.downloadKeys(userIds, false) + val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices() + || cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId) - val devicesInRoom = MXUsersDevicesMap() - val unknownDevices = MXUsersDevicesMap() + val devicesInRoom = MXUsersDevicesMap() + val unknownDevices = MXUsersDevicesMap() - for (userId in it.userIds) { - val deviceIds = it.getUserDeviceIds(userId) ?: continue - for (deviceId in deviceIds) { - val deviceInfo = it.getObject(userId, deviceId) ?: continue - if (warnOnUnknownDevicesRepository.warnOnUnknownDevices() && deviceInfo.isUnknown) { - // The device is not yet known by the user - unknownDevices.setObject(userId, deviceId, deviceInfo) - continue - } - if (deviceInfo.isBlocked) { - // Remove any blocked devices - continue - } - - if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly) { - continue - } - - if (TextUtils.equals(deviceInfo.identityKey(), olmDevice.deviceCurve25519Key)) { - // Don't bother sending to ourself - continue - } - devicesInRoom.setObject(userId, deviceId, deviceInfo) - } - } - if (unknownDevices.isEmpty) { - Try.just(devicesInRoom) - } else { - Try.Failure(MXCryptoError.UnknownDevice(unknownDevices)) - } + for (userId in keys.userIds) { + val deviceIds = keys.getUserDeviceIds(userId) ?: continue + for (deviceId in deviceIds) { + val deviceInfo = keys.getObject(userId, deviceId) ?: continue + if (warnOnUnknownDevicesRepository.warnOnUnknownDevices() && deviceInfo.isUnknown) { + // The device is not yet known by the user + unknownDevices.setObject(userId, deviceId, deviceInfo) + continue } + if (deviceInfo.isBlocked) { + // Remove any blocked devices + continue + } + + if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly) { + continue + } + + if (TextUtils.equals(deviceInfo.identityKey(), olmDevice.deviceCurve25519Key)) { + // Don't bother sending to ourself + continue + } + devicesInRoom.setObject(userId, deviceId, deviceInfo) + } + } + if (unknownDevices.isEmpty) { + return devicesInRoom + } else { + throw MXCryptoError.UnknownDevice(unknownDevices) + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt index 7b5a8cd4..b57486c1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.crypto.algorithms.olm -import arrow.core.Try import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.events.model.Event @@ -41,29 +40,28 @@ internal class MXOlmDecryption( private val credentials: Credentials) : IMXDecrypting { - override suspend fun decryptEvent(event: Event, timeline: String): Try { + override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { val olmEventContent = event.content.toModel() ?: run { Timber.e("## decryptEvent() : bad event format") - return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_EVENT_FORMAT, - MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON)) + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_EVENT_FORMAT, + MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON) } val cipherText = olmEventContent.ciphertext ?: run { Timber.e("## decryptEvent() : missing cipher text") - return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_CIPHER_TEXT, - MXCryptoError.MISSING_CIPHER_TEXT_REASON)) + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_CIPHER_TEXT, + MXCryptoError.MISSING_CIPHER_TEXT_REASON) } val senderKey = olmEventContent.senderKey ?: run { Timber.e("## decryptEvent() : missing sender key") - return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, - MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON)) + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, + MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON) } val messageAny = cipherText[olmDevice.deviceCurve25519Key] ?: run { Timber.e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients") - return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.NOT_INCLUDE_IN_RECIPIENTS, - MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON)) + throw MXCryptoError.Base(MXCryptoError.ErrorType.NOT_INCLUDE_IN_RECIPIENTS, MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON) } // The message for myUser @@ -73,14 +71,12 @@ internal class MXOlmDecryption( if (decryptedPayload == null) { Timber.e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey") - return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, - MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)) + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) } val payloadString = convertFromUTF8(decryptedPayload) if (payloadString == null) { Timber.e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey") - return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, - MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)) + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) } val adapter = MoshiProvider.providesMoshi().adapter(JSON_DICT_PARAMETERIZED_TYPE) @@ -88,73 +84,70 @@ internal class MXOlmDecryption( if (payload == null) { Timber.e("## decryptEvent failed : null payload") - return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, - MXCryptoError.MISSING_CIPHER_TEXT_REASON)) + throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON) } val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString) ?: run { Timber.e("## decryptEvent() : bad olmPayloadContent format") - return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, - MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)) + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON) } if (olmPayloadContent.recipient.isNullOrBlank()) { val reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient") Timber.e("## decryptEvent() : $reason") - return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, - reason)) + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, reason) } if (olmPayloadContent.recipient != credentials.userId) { Timber.e("## decryptEvent() : Event ${event.eventId}:" + " Intended recipient ${olmPayloadContent.recipient} does not match our id ${credentials.userId}") - return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT, - String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient))) + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT, + String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient)) } val recipientKeys = olmPayloadContent.recipient_keys ?: run { Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys' property; cannot prevent unknown-key attack") - return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, - String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys"))) + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, + String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys")) } val ed25519 = recipientKeys["ed25519"] if (ed25519 != olmDevice.deviceEd25519Key) { Timber.e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours") - return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT_KEY, - MXCryptoError.BAD_RECIPIENT_KEY_REASON)) + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT_KEY, + MXCryptoError.BAD_RECIPIENT_KEY_REASON) } if (olmPayloadContent.sender.isNullOrBlank()) { Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack") - return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, - String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender"))) + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, + String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender")) } if (olmPayloadContent.sender != event.senderId) { Timber.e("Event ${event.eventId}: original sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}") - return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.FORWARDED_MESSAGE, - String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender))) + throw MXCryptoError.Base(MXCryptoError.ErrorType.FORWARDED_MESSAGE, + String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender)) } if (olmPayloadContent.room_id != event.roomId) { Timber.e("## decryptEvent() : Event ${event.eventId}: original room ${olmPayloadContent.room_id} does not match reported room ${event.roomId}") - return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ROOM, - String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.room_id))) + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ROOM, + String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.room_id)) } val keys = olmPayloadContent.keys ?: run { Timber.e("## decryptEvent failed : null keys") - return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, - MXCryptoError.MISSING_CIPHER_TEXT_REASON)) + throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, + MXCryptoError.MISSING_CIPHER_TEXT_REASON) } - return Try.just(MXEventDecryptionResult( + return MXEventDecryptionResult( clearEvent = payload, senderCurve25519Key = senderKey, claimedEd25519Key = keys["ed25519"] - )) + ) } /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt index a8b4b1a0..afe4b36e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt @@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.crypto.algorithms.olm import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.crypto.MXOlmDevice -import im.vector.matrix.android.internal.session.SessionScope import javax.inject.Inject internal class MXOlmDecryptionFactory @Inject constructor(private val olmDevice: MXOlmDevice, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt index ad0680a4..10d481dc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt @@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.crypto.algorithms.olm import android.text.TextUtils -import arrow.core.Try import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.internal.crypto.DeviceListManager @@ -40,37 +39,35 @@ internal class MXOlmEncryption( private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction) : IMXEncrypting { - override suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List): Try { + override suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List): Content { // pick the list of recipients based on the membership list. // // TODO: there is a race condition here! What if a new user turns up - return ensureSession(userIds) - .map { - val deviceInfos = ArrayList() - for (userId in userIds) { - val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList() - for (device in devices) { - val key = device.identityKey() - if (TextUtils.equals(key, olmDevice.deviceCurve25519Key)) { - // Don't bother setting up session to ourself - continue - } - if (device.isBlocked) { - // Don't bother setting up sessions with blocked users - continue - } - deviceInfos.add(device) - } - } - - val messageMap = HashMap() - messageMap["room_id"] = roomId - messageMap["type"] = eventType - messageMap["content"] = eventContent - - messageEncrypter.encryptMessage(messageMap, deviceInfos) - messageMap.toContent()!! + ensureSession(userIds) + val deviceInfos = ArrayList() + for (userId in userIds) { + val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList() + for (device in devices) { + val key = device.identityKey() + if (TextUtils.equals(key, olmDevice.deviceCurve25519Key)) { + // Don't bother setting up session to ourself + continue } + if (device.isBlocked) { + // Don't bother setting up sessions with blocked users + continue + } + deviceInfos.add(device) + } + } + + val messageMap = HashMap() + messageMap["room_id"] = roomId + messageMap["type"] = eventType + messageMap["content"] = eventContent + + messageEncrypter.encryptMessage(messageMap, deviceInfos) + return messageMap.toContent()!! } @@ -78,13 +75,9 @@ internal class MXOlmEncryption( * Ensure that the session * * @param users the user ids list - * @param callback the asynchronous callback */ - private suspend fun ensureSession(users: List): Try { - return deviceListManager - .downloadKeys(users, false) - .flatMap { ensureOlmSessionsForUsersAction.handle(users) } - .map { Unit } - + private suspend fun ensureSession(users: List) { + deviceListManager.downloadKeys(users, false) + ensureOlmSessionsForUsersAction.handle(users) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt index 99ab8eda..eff833cf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt @@ -21,7 +21,6 @@ import im.vector.matrix.android.internal.crypto.MXOlmDevice import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForUsersAction import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt index 23e5b466..263cfef2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt @@ -18,8 +18,6 @@ package im.vector.matrix.android.internal.crypto.api import im.vector.matrix.android.internal.crypto.model.rest.* -import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadBody -import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceBody import im.vector.matrix.android.internal.network.NetworkConstants import retrofit2.Call import retrofit2.http.* diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt index 88d9aec9..a69a7325 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt @@ -51,7 +51,6 @@ import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEnt import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.session.SessionScope -import im.vector.matrix.android.internal.task.* import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskThread @@ -204,31 +203,32 @@ internal class KeysBackup @Inject constructor( keysBackupStateManager.state = KeysBackupState.Enabling createKeysBackupVersionTask - .configureWith(createKeysBackupVersionBody) - .dispatchTo(object : MatrixCallback { - override fun onSuccess(info: KeysVersion) { - // Reset backup markers. - cryptoStore.resetBackupMarkers() + .configureWith(createKeysBackupVersionBody) { + this.callback = object : MatrixCallback { + override fun onSuccess(info: KeysVersion) { + // Reset backup markers. + cryptoStore.resetBackupMarkers() - val keyBackupVersion = KeysVersionResult() - keyBackupVersion.algorithm = createKeysBackupVersionBody.algorithm - keyBackupVersion.authData = createKeysBackupVersionBody.authData - keyBackupVersion.version = info.version + val keyBackupVersion = KeysVersionResult() + keyBackupVersion.algorithm = createKeysBackupVersionBody.algorithm + keyBackupVersion.authData = createKeysBackupVersionBody.authData + keyBackupVersion.version = info.version - // We can consider that the server does not have keys yet - keyBackupVersion.count = 0 - keyBackupVersion.hash = null + // We can consider that the server does not have keys yet + keyBackupVersion.count = 0 + keyBackupVersion.hash = null - enableKeysBackup(keyBackupVersion) + enableKeysBackup(keyBackupVersion) - callback.onSuccess(info) + callback.onSuccess(info) + } + + override fun onFailure(failure: Throwable) { + keysBackupStateManager.state = KeysBackupState.Disabled + callback.onFailure(failure) + } } - - override fun onFailure(failure: Throwable) { - keysBackupStateManager.state = KeysBackupState.Disabled - callback.onFailure(failure) - } - }) + } .executeBy(taskExecutor) } @@ -243,27 +243,29 @@ internal class KeysBackup @Inject constructor( keysBackupStateManager.state = KeysBackupState.Unknown } - deleteBackupTask.configureWith(DeleteBackupTask.Params(version)) - .dispatchTo(object : MatrixCallback { - private fun eventuallyRestartBackup() { - // Do not stay in KeysBackupState.Unknown but check what is available on the homeserver - if (state == KeysBackupState.Unknown) { - checkAndStartKeysBackup() + deleteBackupTask + .configureWith(DeleteBackupTask.Params(version)) { + this.callback = object : MatrixCallback { + private fun eventuallyRestartBackup() { + // Do not stay in KeysBackupState.Unknown but check what is available on the homeserver + if (state == KeysBackupState.Unknown) { + checkAndStartKeysBackup() + } + } + + override fun onSuccess(data: Unit) { + eventuallyRestartBackup() + + uiHandler.post { callback?.onSuccess(Unit) } + } + + override fun onFailure(failure: Throwable) { + eventuallyRestartBackup() + + uiHandler.post { callback?.onFailure(failure) } } } - - override fun onSuccess(data: Unit) { - eventuallyRestartBackup() - - uiHandler.post { callback?.onSuccess(Unit) } - } - - override fun onFailure(failure: Throwable) { - eventuallyRestartBackup() - - uiHandler.post { callback?.onFailure(failure) } - } - }) + } .executeBy(taskExecutor) } } @@ -355,15 +357,14 @@ internal class KeysBackup @Inject constructor( callback: MatrixCallback) { // TODO Validate with François that this is correct object : Task { - override suspend fun execute(params: KeysVersionResult): Try { - return Try { - getKeysBackupTrustBg(params) - } + override suspend fun execute(params: KeysVersionResult): KeysBackupVersionTrust { + return getKeysBackupTrustBg(params) } } - .configureWith(keysBackupVersion) - .dispatchTo(callback) - .executeOn(TaskThread.COMPUTATION) + .configureWith(keysBackupVersion) { + this.callback = callback + this.executionThread = TaskThread.COMPUTATION + } .executeBy(taskExecutor) } @@ -454,7 +455,8 @@ internal class KeysBackup @Inject constructor( val myUserId = credentials.userId // Get current signatures, or create an empty set - val myUserSignatures = authData.signatures?.get(myUserId)?.toMutableMap() ?: HashMap() + val myUserSignatures = authData.signatures?.get(myUserId)?.toMutableMap() + ?: HashMap() if (trust) { // Add current device signature @@ -492,27 +494,28 @@ internal class KeysBackup @Inject constructor( // And send it to the homeserver updateKeysBackupVersionTask - .configureWith(UpdateKeysBackupVersionTask.Params(keysBackupVersion.version!!, updateKeysBackupVersionBody)) - .dispatchTo(object : MatrixCallback { - override fun onSuccess(data: Unit) { - // Relaunch the state machine on this updated backup version - val newKeysBackupVersion = KeysVersionResult() + .configureWith(UpdateKeysBackupVersionTask.Params(keysBackupVersion.version!!, updateKeysBackupVersionBody)) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + // Relaunch the state machine on this updated backup version + val newKeysBackupVersion = KeysVersionResult() - newKeysBackupVersion.version = keysBackupVersion.version - newKeysBackupVersion.algorithm = keysBackupVersion.algorithm - newKeysBackupVersion.count = keysBackupVersion.count - newKeysBackupVersion.hash = keysBackupVersion.hash - newKeysBackupVersion.authData = updateKeysBackupVersionBody.authData + newKeysBackupVersion.version = keysBackupVersion.version + newKeysBackupVersion.algorithm = keysBackupVersion.algorithm + newKeysBackupVersion.count = keysBackupVersion.count + newKeysBackupVersion.hash = keysBackupVersion.hash + newKeysBackupVersion.authData = updateKeysBackupVersionBody.authData - checkAndStartWithKeysBackupVersion(newKeysBackupVersion) + checkAndStartWithKeysBackupVersion(newKeysBackupVersion) - callback.onSuccess(data) + callback.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } } - - override fun onFailure(failure: Throwable) { - callback.onFailure(failure) - } - }) + } .executeBy(taskExecutor) } } @@ -758,49 +761,52 @@ internal class KeysBackup @Inject constructor( if (roomId != null && sessionId != null) { // Get key for the room and for the session getRoomSessionDataTask - .configureWith(GetRoomSessionDataTask.Params(roomId, sessionId, version)) - .dispatchTo(object : MatrixCallback { - override fun onSuccess(data: KeyBackupData) { - // Convert to KeysBackupData - val keysBackupData = KeysBackupData() - keysBackupData.roomIdToRoomKeysBackupData = HashMap() - val roomKeysBackupData = RoomKeysBackupData() - roomKeysBackupData.sessionIdToKeyBackupData = HashMap() - roomKeysBackupData.sessionIdToKeyBackupData[sessionId] = data - keysBackupData.roomIdToRoomKeysBackupData[roomId] = roomKeysBackupData + .configureWith(GetRoomSessionDataTask.Params(roomId, sessionId, version)) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: KeyBackupData) { + // Convert to KeysBackupData + val keysBackupData = KeysBackupData() + keysBackupData.roomIdToRoomKeysBackupData = HashMap() + val roomKeysBackupData = RoomKeysBackupData() + roomKeysBackupData.sessionIdToKeyBackupData = HashMap() + roomKeysBackupData.sessionIdToKeyBackupData[sessionId] = data + keysBackupData.roomIdToRoomKeysBackupData[roomId] = roomKeysBackupData - callback.onSuccess(keysBackupData) - } + callback.onSuccess(keysBackupData) + } - override fun onFailure(failure: Throwable) { - callback.onFailure(failure) + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } } - }) + } .executeBy(taskExecutor) } else if (roomId != null) { // Get all keys for the room getRoomSessionsDataTask - .configureWith(GetRoomSessionsDataTask.Params(roomId, version)) - .dispatchTo(object : MatrixCallback { - override fun onSuccess(data: RoomKeysBackupData) { - // Convert to KeysBackupData - val keysBackupData = KeysBackupData() - keysBackupData.roomIdToRoomKeysBackupData = HashMap() - keysBackupData.roomIdToRoomKeysBackupData[roomId] = data + .configureWith(GetRoomSessionsDataTask.Params(roomId, version)) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: RoomKeysBackupData) { + // Convert to KeysBackupData + val keysBackupData = KeysBackupData() + keysBackupData.roomIdToRoomKeysBackupData = HashMap() + keysBackupData.roomIdToRoomKeysBackupData[roomId] = data - callback.onSuccess(keysBackupData) - } + callback.onSuccess(keysBackupData) + } - override fun onFailure(failure: Throwable) { - callback.onFailure(failure) + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } } - }) + } .executeBy(taskExecutor) } else { // Get all keys getSessionsDataTask - .configureWith(GetSessionsDataTask.Params(version)) - .dispatchTo(callback) + .configureWith(GetSessionsDataTask.Params(version)) { + this.callback = callback + } .executeBy(taskExecutor) } } @@ -855,45 +861,47 @@ internal class KeysBackup @Inject constructor( override fun getVersion(version: String, callback: MatrixCallback) { getKeysBackupVersionTask - .configureWith(version) - .dispatchTo(object : MatrixCallback { - override fun onSuccess(data: KeysVersionResult) { - callback.onSuccess(data) - } + .configureWith(version) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: KeysVersionResult) { + callback.onSuccess(data) + } - override fun onFailure(failure: Throwable) { - if (failure is Failure.ServerError - && failure.error.code == MatrixError.NOT_FOUND) { - // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup - callback.onSuccess(null) - } else { - // Transmit the error - callback.onFailure(failure) + override fun onFailure(failure: Throwable) { + if (failure is Failure.ServerError + && failure.error.code == MatrixError.NOT_FOUND) { + // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup + callback.onSuccess(null) + } else { + // Transmit the error + callback.onFailure(failure) + } } } - }) + } .executeBy(taskExecutor) } override fun getCurrentVersion(callback: MatrixCallback) { getKeysBackupLastVersionTask - .toConfigurableTask() - .dispatchTo(object : MatrixCallback { - override fun onSuccess(data: KeysVersionResult) { - callback.onSuccess(data) - } + .configureWith { + this.callback = object : MatrixCallback { + override fun onSuccess(data: KeysVersionResult) { + callback.onSuccess(data) + } - override fun onFailure(failure: Throwable) { - if (failure is Failure.ServerError - && failure.error.code == MatrixError.NOT_FOUND) { - // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup - callback.onSuccess(null) - } else { - // Transmit the error - callback.onFailure(failure) + override fun onFailure(failure: Throwable) { + if (failure is Failure.ServerError + && failure.error.code == MatrixError.NOT_FOUND) { + // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup + callback.onSuccess(null) + } else { + // Transmit the error + callback.onFailure(failure) + } } } - }) + } .executeBy(taskExecutor) } @@ -1236,69 +1244,72 @@ internal class KeysBackup @Inject constructor( Timber.v("backupKeys: 4 - Sending request") - // Make the request - storeSessionDataTask - .configureWith(StoreSessionsDataTask.Params(keysBackupVersion!!.version!!, keysBackupData)) - .dispatchTo(object : MatrixCallback { - override fun onSuccess(data: BackupKeysResult) { - uiHandler.post { - Timber.v("backupKeys: 5a - Request complete") + val sendingRequestCallback = object : MatrixCallback { + override fun onSuccess(data: BackupKeysResult) { + uiHandler.post { + Timber.v("backupKeys: 5a - Request complete") - // Mark keys as backed up - cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers) + // Mark keys as backed up + cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers) - if (olmInboundGroupSessionWrappers.size < KEY_BACKUP_SEND_KEYS_MAX_COUNT) { - Timber.v("backupKeys: All keys have been backed up") - onServerDataRetrieved(data.count, data.hash) + if (olmInboundGroupSessionWrappers.size < KEY_BACKUP_SEND_KEYS_MAX_COUNT) { + Timber.v("backupKeys: All keys have been backed up") + onServerDataRetrieved(data.count, data.hash) - // Note: Changing state will trigger the call to backupAllGroupSessionsCallback.onSuccess() - keysBackupStateManager.state = KeysBackupState.ReadyToBackUp - } else { - Timber.v("backupKeys: Continue to back up keys") - keysBackupStateManager.state = KeysBackupState.WillBackUp + // Note: Changing state will trigger the call to backupAllGroupSessionsCallback.onSuccess() + keysBackupStateManager.state = KeysBackupState.ReadyToBackUp + } else { + Timber.v("backupKeys: Continue to back up keys") + keysBackupStateManager.state = KeysBackupState.WillBackUp - backupKeys() - } - } + backupKeys() } + } + } - override fun onFailure(failure: Throwable) { - if (failure is Failure.ServerError) { - uiHandler.post { - Timber.e(failure, "backupKeys: backupKeys failed.") + override fun onFailure(failure: Throwable) { + if (failure is Failure.ServerError) { + uiHandler.post { + Timber.e(failure, "backupKeys: backupKeys failed.") - when (failure.error.code) { - MatrixError.NOT_FOUND, - MatrixError.WRONG_ROOM_KEYS_VERSION -> { - // Backup has been deleted on the server, or we are not using the last backup version - keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion - backupAllGroupSessionsCallback?.onFailure(failure) - resetBackupAllGroupSessionsListeners() - resetKeysBackupData() - keysBackupVersion = null - - // Do not stay in KeysBackupState.WrongBackUpVersion but check what is available on the homeserver - checkAndStartKeysBackup() - } - else -> - // Come back to the ready state so that we will retry on the next received key - keysBackupStateManager.state = KeysBackupState.ReadyToBackUp - } - } - } else { - uiHandler.post { + when (failure.error.code) { + MatrixError.NOT_FOUND, + MatrixError.WRONG_ROOM_KEYS_VERSION -> { + // Backup has been deleted on the server, or we are not using the last backup version + keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion backupAllGroupSessionsCallback?.onFailure(failure) resetBackupAllGroupSessionsListeners() + resetKeysBackupData() + keysBackupVersion = null - Timber.e("backupKeys: backupKeys failed.") - - // Retry a bit later - keysBackupStateManager.state = KeysBackupState.ReadyToBackUp - maybeBackupKeys() + // Do not stay in KeysBackupState.WrongBackUpVersion but check what is available on the homeserver + checkAndStartKeysBackup() } + else -> + // Come back to the ready state so that we will retry on the next received key + keysBackupStateManager.state = KeysBackupState.ReadyToBackUp } } - }) + } else { + uiHandler.post { + backupAllGroupSessionsCallback?.onFailure(failure) + resetBackupAllGroupSessionsListeners() + + Timber.e("backupKeys: backupKeys failed.") + + // Retry a bit later + keysBackupStateManager.state = KeysBackupState.ReadyToBackUp + maybeBackupKeys() + } + } + } + } + + // Make the request + storeSessionDataTask + .configureWith(StoreSessionsDataTask.Params(keysBackupVersion!!.version!!, keysBackupData)){ + this.callback = sendingRequestCallback + } .executeBy(taskExecutor) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupPassword.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupPassword.kt index 51346c89..e1ada61b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupPassword.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupPassword.kt @@ -142,12 +142,11 @@ private fun deriveKey(password: String, * Generate a 32 chars salt */ private fun generateSalt(): String { - var salt = "" - - do { - salt += UUID.randomUUID().toString() - } while (salt.length < SALT_LENGTH) - + val salt = buildString { + do { + append(UUID.randomUUID().toString()) + } while (length < SALT_LENGTH) + } return salt.substring(0, SALT_LENGTH) } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt index f0c5037f..5aaaaea5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.crypto.keysbackup.tasks -import arrow.core.Try import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion @@ -30,7 +29,7 @@ internal class DefaultCreateKeysBackupVersionTask @Inject constructor(private va : CreateKeysBackupVersionTask { - override suspend fun execute(params: CreateKeysBackupVersionBody): Try { + override suspend fun execute(params: CreateKeysBackupVersionBody): KeysVersion { return executeRequest { apiCall = roomKeysApi.createKeysBackupVersion(params) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt index a1ad84ab..6c966dd2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt @@ -16,10 +16,8 @@ package im.vector.matrix.android.internal.crypto.keysbackup.tasks -import arrow.core.Try import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -33,10 +31,9 @@ internal interface DeleteBackupTask : Task { internal class DefaultDeleteBackupTask @Inject constructor(private val roomKeysApi: RoomKeysApi) : DeleteBackupTask { - override suspend fun execute(params: DeleteBackupTask.Params): Try { + override suspend fun execute(params: DeleteBackupTask.Params) { return executeRequest { - apiCall = roomKeysApi.deleteBackup( - params.version) + apiCall = roomKeysApi.deleteBackup(params.version) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt index c24d314c..ccb3645e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt @@ -16,10 +16,8 @@ package im.vector.matrix.android.internal.crypto.keysbackup.tasks -import arrow.core.Try import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -34,7 +32,7 @@ internal interface DeleteRoomSessionDataTask : Task { + override suspend fun execute(params: DeleteRoomSessionDataTask.Params) { return executeRequest { apiCall = roomKeysApi.deleteRoomSessionData( params.roomId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteRoomSessionsDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteRoomSessionsDataTask.kt index 7476e2d2..98836654 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteRoomSessionsDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteRoomSessionsDataTask.kt @@ -16,10 +16,8 @@ package im.vector.matrix.android.internal.crypto.keysbackup.tasks -import arrow.core.Try import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -33,7 +31,7 @@ internal interface DeleteRoomSessionsDataTask : Task { + override suspend fun execute(params: DeleteRoomSessionsDataTask.Params) { return executeRequest { apiCall = roomKeysApi.deleteRoomSessionsData( params.roomId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteSessionsDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteSessionsDataTask.kt index 14030775..12fc1357 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteSessionsDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteSessionsDataTask.kt @@ -16,10 +16,8 @@ package im.vector.matrix.android.internal.crypto.keysbackup.tasks -import arrow.core.Try import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -32,10 +30,9 @@ internal interface DeleteSessionsDataTask : Task { + override suspend fun execute(params: DeleteSessionsDataTask.Params) { return executeRequest { - apiCall = roomKeysApi.deleteSessionsData( - params.version) + apiCall = roomKeysApi.deleteSessionsData(params.version) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt index 48f405a0..30076226 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.crypto.keysbackup.tasks -import arrow.core.Try import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult import im.vector.matrix.android.internal.network.executeRequest @@ -29,7 +28,7 @@ internal class DefaultGetKeysBackupLastVersionTask @Inject constructor(private v : GetKeysBackupLastVersionTask { - override suspend fun execute(params: Unit): Try { + override suspend fun execute(params: Unit): KeysVersionResult { return executeRequest { apiCall = roomKeysApi.getKeysBackupLastVersion() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt index dfaeafa1..c36f039c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.crypto.keysbackup.tasks -import arrow.core.Try import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult import im.vector.matrix.android.internal.network.executeRequest @@ -29,7 +28,7 @@ internal class DefaultGetKeysBackupVersionTask @Inject constructor(private val r : GetKeysBackupVersionTask { - override suspend fun execute(params: String): Try { + override suspend fun execute(params: String): KeysVersionResult { return executeRequest { apiCall = roomKeysApi.getKeysBackupVersion(params) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt index 253a2a5a..a36850ba 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt @@ -16,11 +16,9 @@ package im.vector.matrix.android.internal.crypto.keysbackup.tasks -import arrow.core.Try import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeyBackupData import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -35,7 +33,7 @@ internal interface GetRoomSessionDataTask : Task { + override suspend fun execute(params: GetRoomSessionDataTask.Params): KeyBackupData { return executeRequest { apiCall = roomKeysApi.getRoomSessionData( params.roomId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetRoomSessionsDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetRoomSessionsDataTask.kt index c5a6d19a..e8888f25 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetRoomSessionsDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetRoomSessionsDataTask.kt @@ -16,11 +16,9 @@ package im.vector.matrix.android.internal.crypto.keysbackup.tasks -import arrow.core.Try import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.RoomKeysBackupData import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -35,7 +33,7 @@ internal interface GetRoomSessionsDataTask : Task { + override suspend fun execute(params: GetRoomSessionsDataTask.Params): RoomKeysBackupData { return executeRequest { apiCall = roomKeysApi.getRoomSessionsData( params.roomId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetSessionsDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetSessionsDataTask.kt index 837e3eb8..d78159d9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetSessionsDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetSessionsDataTask.kt @@ -16,11 +16,9 @@ package im.vector.matrix.android.internal.crypto.keysbackup.tasks -import arrow.core.Try import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysBackupData import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -33,10 +31,9 @@ internal interface GetSessionsDataTask : Task { + override suspend fun execute(params: GetSessionsDataTask.Params): KeysBackupData { return executeRequest { - apiCall = roomKeysApi.getSessionsData( - params.version) + apiCall = roomKeysApi.getSessionsData(params.version) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/StoreRoomSessionDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/StoreRoomSessionDataTask.kt index 748dd106..b25c327e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/StoreRoomSessionDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/StoreRoomSessionDataTask.kt @@ -16,12 +16,10 @@ package im.vector.matrix.android.internal.crypto.keysbackup.tasks -import arrow.core.Try import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi -import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeyBackupData import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.BackupKeysResult +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeyBackupData import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -37,7 +35,7 @@ internal interface StoreRoomSessionDataTask : Task { + override suspend fun execute(params: StoreRoomSessionDataTask.Params): BackupKeysResult { return executeRequest { apiCall = roomKeysApi.storeRoomSessionData( params.roomId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/StoreRoomSessionsDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/StoreRoomSessionsDataTask.kt index 1799df33..8290b2af 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/StoreRoomSessionsDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/StoreRoomSessionsDataTask.kt @@ -16,12 +16,10 @@ package im.vector.matrix.android.internal.crypto.keysbackup.tasks -import arrow.core.Try import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi -import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.RoomKeysBackupData import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.BackupKeysResult +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.RoomKeysBackupData import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -36,7 +34,7 @@ internal interface StoreRoomSessionsDataTask : Task { + override suspend fun execute(params: StoreRoomSessionsDataTask.Params): BackupKeysResult { return executeRequest { apiCall = roomKeysApi.storeRoomSessionsData( params.roomId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt index 977f8114..f3467edb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt @@ -16,12 +16,10 @@ package im.vector.matrix.android.internal.crypto.keysbackup.tasks -import arrow.core.Try import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi -import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysBackupData import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.BackupKeysResult +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysBackupData import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -35,7 +33,7 @@ internal interface StoreSessionsDataTask : Task { + override suspend fun execute(params: StoreSessionsDataTask.Params): BackupKeysResult { return executeRequest { apiCall = roomKeysApi.storeSessionsData( params.version, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/UpdateKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/UpdateKeysBackupVersionTask.kt index 74c39485..28aedaf8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/UpdateKeysBackupVersionTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/UpdateKeysBackupVersionTask.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.crypto.keysbackup.tasks -import arrow.core.Try import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody import im.vector.matrix.android.internal.network.executeRequest @@ -34,7 +33,7 @@ internal class DefaultUpdateKeysBackupVersionTask @Inject constructor(private va : UpdateKeysBackupVersionTask { - override suspend fun execute(params: UpdateKeysBackupVersionTask.Params): Try { + override suspend fun execute(params: UpdateKeysBackupVersionTask.Params) { return executeRequest { apiCall = roomKeysApi.updateKeysBackupVersion(params.version, params.keysBackupVersionBody) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXKey.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXKey.kt index db0f2652..69019fbb 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXKey.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXKey.kt @@ -63,9 +63,7 @@ data class MXKey( fun signatureForUserId(userId: String, signkey: String): String? { // sanity checks if (userId.isNotBlank() && signkey.isNotBlank()) { - if (signatures.containsKey(userId)) { - return signatures[userId]?.get(signkey) - } + return signatures[userId]?.get(signkey) } return null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt index 96d69b7b..ff2f6cc4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt @@ -16,8 +16,8 @@ package im.vector.matrix.android.internal.crypto.store.db -import io.realm.annotations.RealmModule import im.vector.matrix.android.internal.crypto.store.db.model.* +import io.realm.annotations.RealmModule /** * Realm module for Crypto store classes diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMetadataEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMetadataEntity.kt index 19cc06fa..2c96bb25 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMetadataEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMetadataEntity.kt @@ -16,10 +16,10 @@ package im.vector.matrix.android.internal.crypto.store.db.model -import io.realm.RealmObject -import io.realm.annotations.PrimaryKey import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey import org.matrix.olm.OlmAccount internal open class CryptoMetadataEntity( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/DeviceInfoEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/DeviceInfoEntity.kt index d690073a..2c321cc5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/DeviceInfoEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/DeviceInfoEntity.kt @@ -16,9 +16,9 @@ package im.vector.matrix.android.internal.crypto.store.db.model +import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm -import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import io.realm.RealmObject import io.realm.RealmResults import io.realm.annotations.LinkingObjects diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt index d446f42a..4835300e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt @@ -16,11 +16,11 @@ package im.vector.matrix.android.internal.crypto.store.db.model -import io.realm.RealmObject -import io.realm.annotations.PrimaryKey +import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm -import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey internal fun OlmInboundGroupSessionEntity.Companion.createPrimaryKey(sessionId: String?, senderKey: String?) = "$sessionId|$senderKey" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmSessionEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmSessionEntity.kt index 4425cf33..dbc860ef 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmSessionEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmSessionEntity.kt @@ -16,10 +16,10 @@ package im.vector.matrix.android.internal.crypto.store.db.model -import io.realm.RealmObject -import io.realm.annotations.PrimaryKey import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey import org.matrix.olm.OlmSession internal fun OlmSessionEntity.Companion.createPrimaryKey(sessionId: String, deviceKey: String) = "$sessionId|$deviceKey" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingRoomKeyRequestEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingRoomKeyRequestEntity.kt index cbbc2028..d4682858 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingRoomKeyRequestEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingRoomKeyRequestEntity.kt @@ -17,9 +17,9 @@ package im.vector.matrix.android.internal.crypto.store.db.model import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest +import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm -import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import io.realm.RealmObject import io.realm.annotations.PrimaryKey diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/DeviceInfoEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/DeviceInfoEntityQueries.kt index 2aea3cd2..706815ff 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/DeviceInfoEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/DeviceInfoEntityQueries.kt @@ -16,11 +16,11 @@ package im.vector.matrix.android.internal.crypto.store.db.query -import io.realm.Realm -import io.realm.kotlin.where import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.createPrimaryKey +import io.realm.Realm +import io.realm.kotlin.where /** * Get or create a device info diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/UserEntitiesQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/UserEntitiesQueries.kt index dd5278ce..3860d8d8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/UserEntitiesQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/UserEntitiesQueries.kt @@ -16,10 +16,10 @@ package im.vector.matrix.android.internal.crypto.store.db.query -import io.realm.Realm -import io.realm.kotlin.where import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields +import io.realm.Realm +import io.realm.kotlin.where /** * Get or create a user diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt index d60105e0..23b0f958 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.crypto.tasks -import arrow.core.Try import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.model.MXKey import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap @@ -37,35 +36,30 @@ internal interface ClaimOneTimeKeysForUsersDeviceTask : Task> { + override suspend fun execute(params: ClaimOneTimeKeysForUsersDeviceTask.Params): MXUsersDevicesMap { val body = KeysClaimBody(oneTimeKeys = params.usersDevicesKeyTypesMap.map) - return executeRequest { + val keysClaimResponse = executeRequest { apiCall = cryptoApi.claimOneTimeKeysForUsersDevices(body) - }.flatMap { keysClaimResponse -> - Try { - val map = MXUsersDevicesMap() + } + val map = MXUsersDevicesMap() + keysClaimResponse.oneTimeKeys?.let { oneTimeKeys -> + for (userId in oneTimeKeys.keys) { + val mapByUserId = oneTimeKeys[userId] - keysClaimResponse.oneTimeKeys?.let { oneTimeKeys -> - for (userId in oneTimeKeys.keys) { - val mapByUserId = oneTimeKeys[userId] + if (mapByUserId != null) { + for (deviceId in mapByUserId.keys) { + val mxKey = MXKey.from(mapByUserId[deviceId]) - if (mapByUserId != null) { - for (deviceId in mapByUserId.keys) { - val mxKey = MXKey.from(mapByUserId[deviceId]) - - if (mxKey != null) { - map.setObject(userId, deviceId, mxKey) - } else { - Timber.e("## claimOneTimeKeysForUsersDevices : fail to create a MXKey") - } - } + if (mxKey != null) { + map.setObject(userId, deviceId, mxKey) + } else { + Timber.e("## claimOneTimeKeysForUsersDevices : fail to create a MXKey") } } } - - map } } + return map } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceTask.kt index 0f93e213..3e6e4d8c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceTask.kt @@ -16,16 +16,12 @@ package im.vector.matrix.android.internal.crypto.tasks -import arrow.core.Try -import arrow.core.failure -import arrow.core.recoverWith import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -38,10 +34,12 @@ internal interface DeleteDeviceTask : Task { internal class DefaultDeleteDeviceTask @Inject constructor(private val cryptoApi: CryptoApi) : DeleteDeviceTask { - override suspend fun execute(params: DeleteDeviceTask.Params): Try { - return executeRequest { - apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams()) - }.recoverWith { throwable -> + override suspend fun execute(params: DeleteDeviceTask.Params) { + try { + executeRequest { + apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams()) + } + } catch (throwable: Throwable) { if (throwable is Failure.OtherServerError && throwable.httpCode == 401) { // Parse to get a RegistrationFlowResponse val registrationFlowResponse = try { @@ -51,17 +49,16 @@ internal class DefaultDeleteDeviceTask @Inject constructor(private val cryptoApi } catch (e: Exception) { null } - // check if the server response can be casted if (registrationFlowResponse != null) { - Failure.RegistrationFlowError(registrationFlowResponse).failure() + throw Failure.RegistrationFlowError(registrationFlowResponse) } else { - throwable.failure() + throw throwable } } else { // Other error - throwable.failure() + throw throwable } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt index 2476f242..060b57ba 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.crypto.tasks -import arrow.core.Try import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.crypto.api.CryptoApi @@ -38,7 +37,7 @@ internal class DefaultDeleteDeviceWithUserPasswordTask @Inject constructor(priva private val credentials: Credentials) : DeleteDeviceWithUserPasswordTask { - override suspend fun execute(params: DeleteDeviceWithUserPasswordTask.Params): Try { + override suspend fun execute(params: DeleteDeviceWithUserPasswordTask.Params) { return executeRequest { apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams() .apply { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt index 00ea7178..92908071 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt @@ -17,12 +17,10 @@ package im.vector.matrix.android.internal.crypto.tasks import android.text.TextUtils -import arrow.core.Try import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.model.rest.KeysQueryBody import im.vector.matrix.android.internal.crypto.model.rest.KeysQueryResponse import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.task.Task import java.util.* import javax.inject.Inject @@ -38,7 +36,7 @@ internal interface DownloadKeysForUsersTask : Task { + override suspend fun execute(params: DownloadKeysForUsersTask.Params): KeysQueryResponse { val downloadQuery = HashMap>() if (null != params.userIds) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDevicesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDevicesTask.kt index 3779a102..d6e82adb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDevicesTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDevicesTask.kt @@ -16,11 +16,9 @@ package im.vector.matrix.android.internal.crypto.tasks -import arrow.core.Try import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -29,7 +27,7 @@ internal interface GetDevicesTask : Task internal class DefaultGetDevicesTask @Inject constructor(private val cryptoApi: CryptoApi) : GetDevicesTask { - override suspend fun execute(params: Unit): Try { + override suspend fun execute(params: Unit): DevicesListResponse { return executeRequest { apiCall = cryptoApi.getDevices() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetKeyChangesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetKeyChangesTask.kt index 21eabc22..42c36bd1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetKeyChangesTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetKeyChangesTask.kt @@ -16,11 +16,9 @@ package im.vector.matrix.android.internal.crypto.tasks -import arrow.core.Try import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.model.rest.KeyChangesResponse import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -36,10 +34,9 @@ internal interface GetKeyChangesTask : Task { + override suspend fun execute(params: GetKeyChangesTask.Params): KeyChangesResponse { return executeRequest { - apiCall = cryptoApi.getKeyChanges(params.from, - params.to) + apiCall = cryptoApi.getKeyChanges(params.from, params.to) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendToDeviceTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendToDeviceTask.kt index 27559f74..1865c633 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendToDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendToDeviceTask.kt @@ -16,12 +16,10 @@ package im.vector.matrix.android.internal.crypto.tasks -import arrow.core.Try import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceBody import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.task.Task import java.util.* import javax.inject.Inject @@ -40,7 +38,7 @@ internal interface SendToDeviceTask : Task { internal class DefaultSendToDeviceTask @Inject constructor(private val cryptoApi: CryptoApi) : SendToDeviceTask { - override suspend fun execute(params: SendToDeviceTask.Params): Try { + override suspend fun execute(params: SendToDeviceTask.Params) { val sendToDeviceBody = SendToDeviceBody() sendToDeviceBody.messages = params.contentMap.map diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SetDeviceNameTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SetDeviceNameTask.kt index 04613ddd..1abef576 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SetDeviceNameTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SetDeviceNameTask.kt @@ -17,11 +17,9 @@ package im.vector.matrix.android.internal.crypto.tasks import android.text.TextUtils -import arrow.core.Try import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.model.rest.UpdateDeviceInfoBody import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -37,11 +35,10 @@ internal interface SetDeviceNameTask : Task { internal class DefaultSetDeviceNameTask @Inject constructor(private val cryptoApi: CryptoApi) : SetDeviceNameTask { - override suspend fun execute(params: SetDeviceNameTask.Params): Try { + override suspend fun execute(params: SetDeviceNameTask.Params) { val body = UpdateDeviceInfoBody( displayName = if (TextUtils.isEmpty(params.deviceName)) "" else params.deviceName ) - return executeRequest { apiCall = cryptoApi.updateDeviceInfo(params.deviceId, body) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt index 96c3a558..fd7a5e8e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt @@ -16,14 +16,12 @@ package im.vector.matrix.android.internal.crypto.tasks -import arrow.core.Try import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.crypto.api.CryptoApi -import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse import im.vector.matrix.android.internal.crypto.model.rest.DeviceKeys import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadBody +import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.convertToUTF8 import javax.inject.Inject @@ -41,7 +39,7 @@ internal interface UploadKeysTask : Task { + override suspend fun execute(params: UploadKeysTask.Params): KeysUploadResponse { val encodedDeviceId = convertToUTF8(params.deviceId) val body = KeysUploadBody() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt index 54a4e14d..6eea9ab5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt @@ -37,6 +37,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.* import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.task.TaskConstraints import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers @@ -219,17 +220,20 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre startReq: KeyVerificationStart, success: (MXUsersDevicesMap) -> Unit, error: () -> Unit) { - deviceListManager.downloadKeys(listOf(otherUserId), true) - .fold( - { error() }, - { - if (it.getUserDeviceIds(otherUserId)?.contains(startReq.fromDevice) == true) { - success(it) - } else { - error() - } - } - ) + runCatching { + deviceListManager.downloadKeys(listOf(otherUserId), true) + }.fold( + { + if (it.getUserDeviceIds(otherUserId)?.contains(startReq.fromDevice) == true) { + success(it) + } else { + error() + } + }, + { + error() + } + ) } private suspend fun onCancelReceived(event: Event) { @@ -412,16 +416,18 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre val contentMap = MXUsersDevicesMap() contentMap.setObject(userId, userDevice, cancelMessage) - sendToDeviceTask.configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap, transactionId)) - .dispatchTo(object : MatrixCallback { - override fun onSuccess(data: Unit) { - Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}") - } + sendToDeviceTask + .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap, transactionId)) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}") + } - override fun onFailure(failure: Throwable) { - Timber.e(failure, "## SAS verification [$transactionId] failed to cancel.") + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## SAS verification [$transactionId] failed to cancel.") + } } - }) + } .executeBy(taskExecutor) } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt index 8b179c1d..de4b9974 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt @@ -287,23 +287,25 @@ internal abstract class SASVerificationTransaction( val contentMap = MXUsersDevicesMap() contentMap.setObject(otherUserId, otherDeviceId, keyToDevice) - sendToDeviceTask.configureWith(SendToDeviceTask.Params(type, contentMap, transactionId)) - .dispatchTo(object : MatrixCallback { - override fun onSuccess(data: Unit) { - Timber.v("## SAS verification [$transactionId] toDevice type '$type' success.") - if (onDone != null) { - onDone() - } else { - state = nextState + sendToDeviceTask + .configureWith(SendToDeviceTask.Params(type, contentMap, transactionId)) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + Timber.v("## SAS verification [$transactionId] toDevice type '$type' success.") + if (onDone != null) { + onDone() + } else { + state = nextState + } + } + + override fun onFailure(failure: Throwable) { + Timber.e("## SAS verification [$transactionId] failed to send toDevice in state : $state") + + cancel(onErrorReason) } } - - override fun onFailure(failure: Throwable) { - Timber.e("## SAS verification [$transactionId] failed to send toDevice in state : $state") - - cancel(onErrorReason) - } - }) + } .executeBy(taskExecutor) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/AsyncTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/AsyncTransaction.kt new file mode 100644 index 00000000..e1beefc2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/AsyncTransaction.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.database + +import io.realm.Realm +import io.realm.RealmConfiguration +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext + + +suspend fun awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> Unit) = withContext(Dispatchers.IO) { + Realm.getInstance(config).use { bgRealm -> + bgRealm.beginTransaction() + try { + transaction(bgRealm) + if (isActive) { + bgRealm.commitTransaction() + } + } finally { + if (bgRealm.isInTransaction) { + bgRealm.cancelTransaction() + } + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt index af842c67..e78078f9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt @@ -18,12 +18,7 @@ package im.vector.matrix.android.internal.database import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.util.createBackgroundHandler -import io.realm.OrderedCollectionChangeSet -import io.realm.OrderedRealmCollectionChangeListener -import io.realm.Realm -import io.realm.RealmConfiguration -import io.realm.RealmObject -import io.realm.RealmResults +import io.realm.* import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt index 64afa3d4..3e3ffad4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt @@ -16,38 +16,46 @@ package im.vector.matrix.android.internal.database -import android.os.Handler -import android.os.HandlerThread +import im.vector.matrix.android.internal.util.createBackgroundHandler import io.realm.* import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference -private const val THREAD_NAME = "REALM_QUERY_LATCH" class RealmQueryLatch(private val realmConfiguration: RealmConfiguration, private val realmQueryBuilder: (Realm) -> RealmQuery) { - fun await() { - val latch = CountDownLatch(1) - val handlerThread = HandlerThread(THREAD_NAME + hashCode()) - handlerThread.start() - val handler = Handler(handlerThread.looper) - val runnable = Runnable { - val realm = Realm.getInstance(realmConfiguration) - val result = realmQueryBuilder(realm).findAllAsync() + private companion object { + val QUERY_LATCH_HANDLER = createBackgroundHandler("REALM_QUERY_LATCH") + } + @Throws(InterruptedException::class) + fun await(timeout: Long, timeUnit: TimeUnit) { + val realmRef = AtomicReference() + val latch = CountDownLatch(1) + QUERY_LATCH_HANDLER.post { + val realm = Realm.getInstance(realmConfiguration) + realmRef.set(realm) + val result = realmQueryBuilder(realm).findAllAsync() result.addChangeListener(object : RealmChangeListener> { override fun onChange(t: RealmResults) { if (t.isNotEmpty()) { result.removeChangeListener(this) - realm.close() latch.countDown() } } }) } - handler.post(runnable) - latch.await() - handlerThread.quit() + try { + latch.await(timeout, timeUnit) + } catch (exception: InterruptedException) { + throw exception + } finally { + QUERY_LATCH_HANDLER.post { + realmRef.getAndSet(null).close() + } + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt index d700fdf3..f6bf258b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt @@ -20,11 +20,7 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.internal.database.mapper.ContentMapper -import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.EventEntityFields -import im.vector.matrix.android.internal.database.model.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.model.* import im.vector.matrix.android.internal.database.query.next import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt index 30346f78..4375a19d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt @@ -20,13 +20,10 @@ import com.squareup.moshi.JsonDataException import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.UnsignedData -import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult -import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmDecryption import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.di.MoshiProvider import timber.log.Timber -import java.util.* internal object EventMapper { @@ -73,6 +70,7 @@ internal object EventMapper { unsignedData = ud, redacts = eventEntity.redacts ).also { + it.sendState = eventEntity.sendState eventEntity.decryptionResultJson?.let { json -> try { it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index 5f946e42..45328992 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -61,7 +61,8 @@ internal class RoomSummaryMapper @Inject constructor( highlightCount = roomSummaryEntity.highlightCount, notificationCount = roomSummaryEntity.notificationCount, tags = tags, - membership = roomSummaryEntity.membership + membership = roomSummaryEntity.membership, + versioningState = roomSummaryEntity.versioningState ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt index 92cbd4be..61d5a601 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.database.mapper import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.database.model.TimelineEventEntity @@ -33,8 +32,7 @@ internal object TimelineEventMapper { displayIndex = timelineEventEntity.root?.displayIndex ?: 0, senderName = timelineEventEntity.senderName, isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, - senderAvatar = timelineEventEntity.senderAvatar, - sendState = timelineEventEntity.root?.sendState ?: SendState.UNKNOWN + senderAvatar = timelineEventEntity.senderAvatar ) } 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 77a97f72..d8661984 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 @@ -33,6 +33,8 @@ internal open class ChunkEntity(@Index var prevToken: String? = null, var forwardsStateIndex: Int? = null ) : RealmObject() { + fun identifier() = "${prevToken}_${nextToken}" + @LinkingObjects("chunks") val room: RealmResults? = null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt index ab10297f..4401d394 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt @@ -24,8 +24,6 @@ import io.realm.RealmObject import io.realm.RealmResults import io.realm.annotations.Index import io.realm.annotations.LinkingObjects -import io.realm.annotations.PrimaryKey -import java.util.* internal open class EventEntity(@Index var eventId: String = "", @Index var roomId: String = "", diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/GroupEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/GroupEntity.kt index 35733d5f..dea36343 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/GroupEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/GroupEntity.kt @@ -18,20 +18,20 @@ package im.vector.matrix.android.internal.database.model import im.vector.matrix.android.api.session.room.model.Membership import io.realm.RealmObject -import io.realm.annotations.Ignore import io.realm.annotations.PrimaryKey -import kotlin.properties.Delegates internal open class GroupEntity(@PrimaryKey var groupId: String = "" ) : RealmObject() { private var membershipStr: String = Membership.NONE.name - - @delegate:Ignore - var membership: Membership by Delegates.observable(Membership.valueOf(membershipStr)) { _, _, newValue -> - membershipStr = newValue.name - } + var membership: Membership + get() { + return Membership.valueOf(membershipStr) + } + set(value) { + membershipStr = value.name + } companion object diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomEntity.kt index b9c110b5..a9f91390 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomEntity.kt @@ -19,9 +19,7 @@ package im.vector.matrix.android.internal.database.model import im.vector.matrix.android.api.session.room.model.Membership import io.realm.RealmList import io.realm.RealmObject -import io.realm.annotations.Ignore import io.realm.annotations.PrimaryKey -import kotlin.properties.Delegates internal open class RoomEntity(@PrimaryKey var roomId: String = "", var chunks: RealmList = RealmList(), @@ -31,11 +29,13 @@ internal open class RoomEntity(@PrimaryKey var roomId: String = "", ) : RealmObject() { private var membershipStr: String = Membership.NONE.name - - @delegate:Ignore - var membership: Membership by Delegates.observable(Membership.valueOf(membershipStr)) { _, _, newValue -> - membershipStr = newValue.name - } + var membership: Membership + get() { + return Membership.valueOf(membershipStr) + } + set(value) { + membershipStr = value.name + } companion object } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index c178711c..6fe81f4c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -17,11 +17,10 @@ package im.vector.matrix.android.internal.database.model import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.model.VersioningState import io.realm.RealmList import io.realm.RealmObject -import io.realm.annotations.Ignore import io.realm.annotations.PrimaryKey -import kotlin.properties.Delegates internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", var displayName: String? = "", @@ -32,6 +31,7 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", var joinedMembersCount: Int? = 0, var invitedMembersCount: Int? = 0, var isDirect: Boolean = false, + var directUserId: String? = null, var otherMemberIds: RealmList = RealmList(), var notificationCount: Int = 0, var highlightCount: Int = 0, @@ -39,11 +39,22 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", ) : RealmObject() { private var membershipStr: String = Membership.NONE.name + var membership: Membership + get() { + return Membership.valueOf(membershipStr) + } + set(value) { + membershipStr = value.name + } - @delegate:Ignore - var membership: Membership by Delegates.observable(Membership.valueOf(membershipStr)) { _, _, newValue -> - membershipStr = newValue.name - } + private var versioningStateStr: String = VersioningState.NONE.name + var versioningState: VersioningState + get() { + return VersioningState.valueOf(versioningStateStr) + } + set(value) { + versioningStateStr = value.name + } companion object diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt index 7cc0713f..f2c26042 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import io.realm.Realm import io.realm.RealmQuery +import io.realm.RealmResults import io.realm.kotlin.where internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery { @@ -29,3 +30,20 @@ internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = n } return query } + +internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm): RealmResults { + return RoomSummaryEntity.where(realm) + .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + .findAll() +} + +internal fun RoomSummaryEntity.Companion.isDirect(realm: Realm, roomId: String): Boolean { + return RoomSummaryEntity.where(realm) + .equalTo(RoomSummaryEntityFields.ROOM_ID, roomId) + .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + .findAll() + .isNotEmpty() +} + + + 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 3669ada7..182e58a3 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 @@ -16,16 +16,10 @@ package im.vector.matrix.android.internal.database.query -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.api.session.room.send.SendState +import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.* -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 io.realm.Realm -import io.realm.RealmList -import io.realm.RealmQuery -import io.realm.Sort +import io.realm.* import io.realm.kotlin.where internal fun TimelineEventEntity.Companion.where(realm: Realm, eventId: String): RealmQuery { @@ -65,11 +59,12 @@ internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm: internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm, roomId: String, + includesSending: Boolean, includedTypes: List = emptyList(), excludedTypes: List = emptyList()): TimelineEventEntity? { val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null - val eventList = if (roomEntity.sendingTimelineEvents.isNotEmpty()) { + val eventList = if (includesSending && roomEntity.sendingTimelineEvents.isNotEmpty()) { roomEntity.sendingTimelineEvents } else { ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)?.timelineEvents @@ -117,3 +112,15 @@ internal fun RealmList.find(eventId: String): TimelineEvent .equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, eventId) .findFirst() } + +internal fun TimelineEventEntity.Companion.findAllInRoomWithSendStates(realm: Realm, + roomId: String, + sendStates: List) + : RealmResults { + + val sendStatesStr = sendStates.map { it.name }.toTypedArray() + return realm.where() + .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) + .`in`(TimelineEventEntityFields.ROOT.SEND_STATE_STR,sendStatesStr) + .findAll() +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt index ad03bbca..1627266c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt @@ -27,13 +27,11 @@ import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.auth.AuthModule import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.network.NetworkConnectivityChecker -import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.BackgroundDetectionObserver import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import okhttp3.OkHttpClient import org.matrix.olm.OlmManager -import retrofit2.Retrofit @Component(modules = [MatrixModule::class, NetworkModule::class, AuthModule::class]) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/Result.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/Result.kt new file mode 100644 index 00000000..7d4ba7aa --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/Result.kt @@ -0,0 +1,25 @@ +/* + + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + + */ +package im.vector.matrix.android.internal.extensions + +import im.vector.matrix.android.api.MatrixCallback + +fun Result.foldToCallback(callback: MatrixCallback): Unit = fold( + { callback.onSuccess(it) }, + { callback.onFailure(it) } +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/Try.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/Try.kt index 18ab0e47..78529404 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/Try.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/Try.kt @@ -31,3 +31,11 @@ inline fun TryOf.onError(f: (Throwable) -> Unit): Try = fix() fun Try.foldToCallback(callback: MatrixCallback): Unit = fold( { callback.onFailure(it) }, { callback.onSuccess(it) }) + +/** + * Same as doOnNext for Observables + */ +inline fun Try.alsoDo(f: (A) -> Unit) = map { + f(it) + it +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt index 9502cb28..412cb73c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt @@ -19,35 +19,64 @@ package im.vector.matrix.android.internal.network import android.content.Context import com.novoda.merlin.Merlin import com.novoda.merlin.MerlinsBeard -import com.novoda.merlin.registerable.connection.Connectable import im.vector.matrix.android.internal.di.MatrixScope +import timber.log.Timber +import java.util.* import javax.inject.Inject +import kotlin.collections.ArrayList +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine @MatrixScope internal class NetworkConnectivityChecker @Inject constructor(context: Context) { - private val merlin = Merlin.Builder().withConnectableCallbacks().build(context) - private val merlinsBeard = MerlinsBeard.from(context) + private val merlin = Merlin.Builder() + .withConnectableCallbacks() + .withDisconnectableCallbacks() + .build(context) - private val listeners = ArrayList() + private val merlinsBeard = MerlinsBeard.Builder().build(context) + private val listeners = Collections.synchronizedList(ArrayList()) - fun register(listener: Listener) { - if (listeners.isEmpty()) { - merlin.bind() - } - listeners.add(listener) - val connectable = Connectable { - if (listeners.contains(listener)) { - listener.onConnect() + init { + merlin.bind() + merlin.registerDisconnectable { + Timber.v("On Disconnect") + val localListeners = listeners.toList() + localListeners.forEach { + it.onDisconnect() } } - merlin.registerConnectable(connectable) + merlin.registerConnectable { + Timber.v("On Connect") + val localListeners = listeners.toList() + localListeners.forEach { + it.onConnect() + } + } + } + + suspend fun waitUntilConnected() { + if (isConnected()) { + return + } else { + suspendCoroutine { continuation -> + register(object : Listener { + override fun onConnect() { + unregister(this) + continuation.resume(Unit) + } + }) + } + } + } + + fun register(listener: Listener) { + listeners.add(listener) } fun unregister(listener: Listener) { - if (listeners.remove(listener) && listeners.isEmpty()) { - merlin.unbind() - } + listeners.remove(listener) } fun isConnected(): Boolean { @@ -55,7 +84,13 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context) } interface Listener { - fun onConnect() + fun onConnect() { + + } + + fun onDisconnect() { + + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ProgressRequestBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ProgressRequestBody.kt index fabd9763..49655ab4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ProgressRequestBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ProgressRequestBody.kt @@ -18,11 +18,7 @@ package im.vector.matrix.android.internal.network import okhttp3.MediaType import okhttp3.RequestBody -import okio.Buffer -import okio.BufferedSink -import okio.ForwardingSink -import okio.Okio -import okio.Sink +import okio.* import java.io.IOException internal class ProgressRequestBody(private val delegate: RequestBody, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt index 4dfc5810..3d1e433b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt @@ -16,9 +16,6 @@ package im.vector.matrix.android.internal.network -import arrow.core.Try -import arrow.core.failure -import arrow.core.recoverWith import com.squareup.moshi.JsonDataException import com.squareup.moshi.Moshi import im.vector.matrix.android.api.failure.Failure @@ -36,8 +33,8 @@ internal class Request { private val moshi: Moshi = MoshiProvider.providesMoshi() lateinit var apiCall: Call - suspend fun execute(): Try { - return Try { + suspend fun execute(): DATA { + return try { val response = apiCall.awaitResponse() if (response.isSuccessful) { response.body() @@ -45,13 +42,13 @@ internal class Request { } else { throw manageFailure(response.errorBody(), response.code()) } - }.recoverWith { - when (it) { - is IOException -> Failure.NetworkConnection(it) + } catch (exception: Throwable) { + throw when (exception) { + is IOException -> Failure.NetworkConnection(exception) is Failure.ServerError, - is Failure.OtherServerError -> it - else -> Failure.Unknown(it) - }.failure() + is Failure.OtherServerError -> exception + else -> Failure.Unknown(exception) + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt index 7528dee2..824d74b3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt @@ -19,7 +19,9 @@ package im.vector.matrix.android.internal.network import kotlinx.coroutines.suspendCancellableCoroutine -import retrofit2.* +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitFactory.kt index 4e9ff6f9..70143f7c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitFactory.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.network import com.squareup.moshi.Moshi -import okhttp3.Interceptor import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory 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 09baebb2..2922b7d6 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 @@ -47,6 +47,7 @@ import im.vector.matrix.android.internal.session.sync.job.SyncWorker import im.vector.matrix.android.internal.worker.WorkManagerUtil import timber.log.Timber import javax.inject.Inject +import javax.inject.Provider @SessionScope internal class DefaultSession @Inject constructor(override val sessionParams: SessionParams, @@ -64,7 +65,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se private val pushersService: Lazy, private val cryptoService: Lazy, private val fileService: Lazy, - private val syncThread: SyncThread, + private val syncThreadProvider: Provider, private val contentUrlResolver: ContentUrlResolver, private val contentUploadProgressTracker: ContentUploadStateTracker, private val initialSyncProgressService: Lazy) @@ -84,6 +85,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se private var isOpen = false + private var syncThread: SyncThread? = null @MainThread override fun open() { @@ -105,21 +107,23 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se SyncWorker.stopAnyBackgroundSync(context) } - override fun startSync(fromForeground : Boolean) { + override fun startSync(fromForeground: Boolean) { Timber.i("Starting sync thread") assert(isOpen) - syncThread.setInitialForeground(fromForeground) - if (!syncThread.isAlive) { - syncThread.start() + val localSyncThread = getSyncThread() + localSyncThread.setInitialForeground(fromForeground) + if (!localSyncThread.isAlive) { + localSyncThread.start() } else { - syncThread.restart() + localSyncThread.restart() Timber.w("Attempt to start an already started thread") } } override fun stopSync() { assert(isOpen) - syncThread.kill() + syncThread?.kill() + syncThread = null } override fun close() { @@ -131,7 +135,13 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se } override fun syncState(): LiveData { - return syncThread.liveState() + return getSyncThread().liveState() + } + + private fun getSyncThread(): SyncThread { + return syncThread ?: syncThreadProvider.get().also { + syncThread = it + } } @MainThread @@ -139,21 +149,20 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se Timber.w("SIGN_OUT: start") assert(isOpen) - //Timber.w("SIGN_OUT: kill sync thread") - //syncThread.kill() Timber.w("SIGN_OUT: call webservice") return signOutService.get().signOut(object : MatrixCallback { override fun onSuccess(data: Unit) { Timber.w("SIGN_OUT: call webservice -> SUCCESS: clear cache") - + stopSync() + stopAnyBackgroundSync() // Clear the cache cacheService.get().clearCache(object : MatrixCallback { override fun onSuccess(data: Unit) { Timber.w("SIGN_OUT: clear cache -> SUCCESS: clear crypto cache") cryptoService.get().clearCryptoCache(MatrixCallbackDelegate(callback)) - WorkManagerUtil.cancelAllWorks(context) + callback.onSuccess(Unit) } override fun onFailure(failure: Throwable) { @@ -172,6 +181,22 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se }) } + override fun clearCache(callback: MatrixCallback) { + stopSync() + stopAnyBackgroundSync() + cacheService.get().clearCache(object : MatrixCallback { + override fun onSuccess(data: Unit) { + startSync(true) + callback.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + startSync(true) + callback.onFailure(failure) + } + }) + } + override fun contentUrlResolver() = contentUrlResolver override fun contentUploadProgressTracker() = contentUploadProgressTracker 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 1738cedd..26f43be0 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 @@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.session import dagger.BindsInstance import dagger.Component import im.vector.matrix.android.api.auth.data.SessionParams -import im.vector.matrix.android.api.session.InitialSyncProgressService import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.internal.crypto.CryptoModule import im.vector.matrix.android.internal.di.MatrixComponent @@ -43,23 +42,25 @@ import im.vector.matrix.android.internal.session.sync.SyncTask import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.session.sync.job.SyncWorker import im.vector.matrix.android.internal.session.user.UserModule +import im.vector.matrix.android.internal.session.user.accountdata.AccountDataModule import im.vector.matrix.android.internal.task.TaskExecutor @Component(dependencies = [MatrixComponent::class], - modules = [ - SessionModule::class, - RoomModule::class, - SyncModule::class, - SignOutModule::class, - GroupModule::class, - UserModule::class, - FilterModule::class, - GroupModule::class, - ContentModule::class, - CacheModule::class, - CryptoModule::class, - PushersModule::class - ] + modules = [ + SessionModule::class, + RoomModule::class, + SyncModule::class, + SignOutModule::class, + GroupModule::class, + UserModule::class, + FilterModule::class, + GroupModule::class, + ContentModule::class, + CacheModule::class, + CryptoModule::class, + PushersModule::class, + AccountDataModule::class + ] ) @SessionScope internal interface SessionComponent { 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 f2e61e8c..38637fb7 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 @@ -37,7 +37,9 @@ import im.vector.matrix.android.internal.network.AccessTokenInterceptor import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater +import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLiveObserver import im.vector.matrix.android.internal.session.room.prune.EventsPruner +import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventLiveObserver import im.vector.matrix.android.internal.util.md5 import io.realm.RealmConfiguration import okhttp3.OkHttpClient @@ -128,6 +130,14 @@ internal abstract class SessionModule { @IntoSet abstract fun bindEventRelationsAggregationUpdater(groupSummaryUpdater: EventRelationsAggregationUpdater): LiveEntityObserver + @Binds + @IntoSet + abstract fun bindRoomTombstoneEventLiveObserver(roomTombstoneEventLiveObserver: RoomTombstoneEventLiveObserver): LiveEntityObserver + + @Binds + @IntoSet + abstract fun bindRoomCreateEventLiveObserver(roomCreateEventLiveObserver: RoomCreateEventLiveObserver): LiveEntityObserver + @Binds abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cache/CacheModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cache/CacheModule.kt index b9449d0c..96f6fbd8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cache/CacheModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cache/CacheModule.kt @@ -21,7 +21,6 @@ import dagger.Module import dagger.Provides import im.vector.matrix.android.api.session.cache.CacheService import im.vector.matrix.android.internal.di.SessionDatabase -import im.vector.matrix.android.internal.session.SessionScope import io.realm.RealmConfiguration @Module diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cache/ClearCacheTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cache/ClearCacheTask.kt index 0dd39c10..5af4ee74 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cache/ClearCacheTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cache/ClearCacheTask.kt @@ -16,27 +16,18 @@ package im.vector.matrix.android.internal.session.cache -import arrow.core.Try -import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.database.awaitTransaction import im.vector.matrix.android.internal.task.Task -import io.realm.Realm import io.realm.RealmConfiguration import javax.inject.Inject -import javax.inject.Named internal interface ClearCacheTask : Task internal class RealmClearCacheTask @Inject constructor(private val realmConfiguration: RealmConfiguration) : ClearCacheTask { - override suspend fun execute(params: Unit): Try { - return Try { - val realm = Realm.getInstance(realmConfiguration) - - realm.executeTransaction { - it.deleteAll() - } - - realm.close() + override suspend fun execute(params: Unit) { + awaitTransaction(realmConfiguration) { + it.deleteAll() } } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cache/DefaultCacheService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cache/DefaultCacheService.kt index c23c9eea..12684965 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cache/DefaultCacheService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cache/DefaultCacheService.kt @@ -20,16 +20,19 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.cache.CacheService import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.task.toConfigurableTask +import im.vector.matrix.android.internal.task.configureWith import javax.inject.Inject -internal class DefaultCacheService @Inject constructor(@SessionDatabase private val clearCacheTask: ClearCacheTask, +internal class DefaultCacheService @Inject constructor(@SessionDatabase + private val clearCacheTask: ClearCacheTask, private val taskExecutor: TaskExecutor) : CacheService { override fun clearCache(callback: MatrixCallback) { + taskExecutor.cancelAll() clearCacheTask - .toConfigurableTask() - .dispatchTo(callback) + .configureWith { + this.callback = callback + } .executeBy(taskExecutor) } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index 8e1a0281..b015670d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -57,9 +57,11 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : override suspend fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) ?: return Result.success() + Timber.v("Starting upload media work with params $params") if (params.lastFailureMessage != null) { // Transmit the error + Timber.v("Stop upload media work due to input failure") return Result.success(inputData) } @@ -121,7 +123,11 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : val progressListener = object : ProgressRequestBody.Listener { override fun onProgress(current: Long, total: Long) { - contentUploadStateTracker.setProgress(eventId, current, total) + if (isStopped) { + contentUploadStateTracker.setFailure(eventId, Throwable("Cancelled")) + } else { + contentUploadStateTracker.setProgress(eventId, current, total) + } } } @@ -166,6 +172,7 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : encryptedFileInfo: EncryptedFileInfo?, thumbnailUrl: String?, thumbnailEncryptedFileInfo: EncryptedFileInfo?): Result { + Timber.v("handleSuccess $attachmentUrl, work is stopped $isStopped") contentUploadStateTracker.setSuccess(params.event.eventId!!) val event = updateEvent(params.event, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo) val sendParams = SendEventWorker.Params(params.userId, params.roomId, event) @@ -210,6 +217,7 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) : ) } + private fun MessageFileContent.update(url: String, encryptedFileInfo: EncryptedFileInfo?): MessageFileContent { return copy( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultSaveFilterTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultSaveFilterTask.kt index 5fa5d0a2..a5da026a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultSaveFilterTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultSaveFilterTask.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.session.filter -import arrow.core.Try import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task @@ -39,15 +38,12 @@ internal class DefaultSaveFilterTask @Inject constructor(private val sessionPara private val filterRepository: FilterRepository ) : SaveFilterTask { - override suspend fun execute(params: SaveFilterTask.Params): Try { - return executeRequest { + override suspend fun execute(params: SaveFilterTask.Params) { + val filterResponse = executeRequest { // TODO auto retry apiCall = filterAPI.uploadFilter(sessionParams.credentials.userId, params.filter) - }.flatMap { filterResponse -> - Try { - filterRepository.storeFilterId(params.filter, filterResponse.filterId) - } } + filterRepository.storeFilterId(params.filter, filterResponse.filterId) } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt index ed8c11bc..6964ccf8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt @@ -16,10 +16,6 @@ package im.vector.matrix.android.internal.session.group -import arrow.core.Try -import arrow.core.fix -import arrow.instances.`try`.monad.monad -import arrow.typeclasses.binding import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.database.model.GroupSummaryEntity import im.vector.matrix.android.internal.database.query.where @@ -28,8 +24,6 @@ import im.vector.matrix.android.internal.session.group.model.GroupRooms import im.vector.matrix.android.internal.session.group.model.GroupSummaryResponse import im.vector.matrix.android.internal.session.group.model.GroupUsers import im.vector.matrix.android.internal.task.Task -import im.vector.matrix.android.internal.util.tryTransactionSync -import io.realm.kotlin.createObject import javax.inject.Inject internal interface GetGroupDataTask : Task { @@ -43,7 +37,7 @@ internal class DefaultGetGroupDataTask @Inject constructor( private val monarchy: Monarchy ) : GetGroupDataTask { - override suspend fun execute(params: GetGroupDataTask.Params): Try { + override suspend fun execute(params: GetGroupDataTask.Params) { val groupId = params.groupId val groupSummary = executeRequest { apiCall = groupAPI.getSummary(groupId) @@ -54,20 +48,18 @@ internal class DefaultGetGroupDataTask @Inject constructor( val groupUsers = executeRequest { apiCall = groupAPI.getUsers(groupId) } - return Try.monad().binding { - insertInDb(groupSummary.bind(), groupRooms.bind(), groupUsers.bind(), groupId).bind() - }.fix() + insertInDb(groupSummary, groupRooms, groupUsers, groupId) } private fun insertInDb(groupSummary: GroupSummaryResponse, groupRooms: GroupRooms, groupUsers: GroupUsers, - groupId: String): Try { - return monarchy - .tryTransactionSync { realm -> + groupId: String) { + monarchy + .writeAsync { realm -> val groupSummaryEntity = GroupSummaryEntity.where(realm, groupId).findFirst() - ?: realm.createObject(groupId) + ?: realm.createObject(GroupSummaryEntity::class.java, groupId) groupSummaryEntity.avatarUrl = groupSummary.profile?.avatarUrl ?: "" val name = groupSummary.profile?.name @@ -82,7 +74,6 @@ internal class DefaultGetGroupDataTask @Inject constructor( val userIds = groupUsers.users.map { it.userId } groupSummaryEntity.userIds.clear() groupSummaryEntity.userIds.addAll(userIds) - } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt index ed0552da..a574df7c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt @@ -25,7 +25,6 @@ import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.GroupSummaryEntity import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.session.SessionScope import javax.inject.Inject internal class DefaultGroupService @Inject constructor(private val monarchy: Monarchy) : GroupService { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt index 081739cb..913a468b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt @@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.session.group import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters -import arrow.core.Try import com.squareup.moshi.JsonClass import im.vector.matrix.android.internal.worker.SessionWorkerParams import im.vector.matrix.android.internal.worker.WorkerParamsFactory @@ -43,16 +42,15 @@ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) : val sessionComponent = getSessionComponent(params.userId) ?: return Result.success() sessionComponent.inject(this) - val results = params.groupIds.map { groupId -> - fetchGroupData(groupId) + runCatching { fetchGroupData(groupId) } } - val isSuccessful = results.none { it.isFailure() } + val isSuccessful = results.none { it.isFailure } return if (isSuccessful) Result.success() else Result.retry() } - private suspend fun fetchGroupData(groupId: String): Try { - return getGroupDataTask.execute(GetGroupDataTask.Params(groupId)) + private suspend fun fetchGroupData(groupId: String) { + getGroupDataTask.execute(GetGroupDataTask.Params(groupId)) } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt index fcfe9f0a..47905ecc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt @@ -22,20 +22,15 @@ import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.database.RealmLiveEntityObserver -import im.vector.matrix.android.internal.database.mapper.asDomain -import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.GroupEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.SessionDatabase -import im.vector.matrix.android.internal.session.room.prune.PruneEventTask -import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.worker.WorkManagerUtil import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder import im.vector.matrix.android.internal.worker.WorkerParamsFactory import io.realm.OrderedCollectionChangeSet import io.realm.RealmConfiguration import io.realm.RealmResults -import timber.log.Timber import javax.inject.Inject private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt index 5b21f0e5..83b89701 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt @@ -20,12 +20,11 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.pushrules.Action import im.vector.matrix.android.api.pushrules.PushRuleService -import im.vector.matrix.android.api.pushrules.rest.GetPushRulesResponse import im.vector.matrix.android.api.pushrules.rest.PushRule import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.database.mapper.PushRulesMapper import im.vector.matrix.android.internal.database.model.PushRulesEntity -import im.vector.matrix.android.internal.database.model.PusherEntityFields import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.pushers.GetPushRulesTask @@ -82,11 +81,12 @@ internal class DefaultPushRuleService @Inject constructor( return contentRules + overrideRules + roomRules + senderRules + underrideRules } - override fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback) { - updatePushRuleEnableStatusTask - .configureWith(UpdatePushRuleEnableStatusTask.Params(kind, pushRule, enabled)) + override fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback): Cancelable { + return updatePushRuleEnableStatusTask + .configureWith(UpdatePushRuleEnableStatusTask.Params(kind, pushRule, enabled)) { + this.callback = callback + } // TODO Fetch the rules - .dispatchTo(callback) .executeBy(taskExecutor) } @@ -122,13 +122,23 @@ internal class DefaultPushRuleService @Inject constructor( } } + fun dispatchRoomLeft(roomid: String) { + try { + listeners.forEach { + it.onRoomLeft(roomid) + } + } catch (e: Throwable) { + Timber.e(e, "Error while dispatching room left") + } + } + fun dispatchFinish() { try { listeners.forEach { it.batchFinish() } } catch (e: Throwable) { - + Timber.e(e, "Error while dispatching finish") } } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/ProcessEventForPushTask.kt index fc6c8838..a434c6e9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/ProcessEventForPushTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/ProcessEventForPushTask.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.session.notification -import arrow.core.Try import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.pushrules.rest.PushRule import im.vector.matrix.android.api.session.events.model.Event @@ -41,44 +40,45 @@ internal class DefaultProcessEventForPushTask @Inject constructor( private val sessionParams: SessionParams ) : ProcessEventForPushTask { - - override suspend fun execute(params: ProcessEventForPushTask.Params): Try { - return Try { - val newJoinEvents = params.syncResponse.join - .map { entries -> - entries.value.timeline?.events?.map { it.copy(roomId = entries.key) } - } - .fold(emptyList(), { acc, next -> - acc + (next ?: emptyList()) - }) - val inviteEvents = params.syncResponse.invite - .map { entries -> - entries.value.inviteState?.events?.map { it.copy(roomId = entries.key) } - } - .fold(emptyList(), { acc, next -> - acc + (next ?: emptyList()) - }) - val allEvents = (newJoinEvents + inviteEvents).filter { event -> - when (event.type) { - EventType.MESSAGE, - EventType.REDACTION, - EventType.ENCRYPTED, - EventType.STATE_ROOM_MEMBER -> true - else -> false - } - }.filter { - it.senderId != sessionParams.credentials.userId - } - Timber.v("[PushRules] Found ${allEvents.size} out of ${(newJoinEvents + inviteEvents).size}" + - " to check for push rules with ${params.rules.size} rules") - allEvents.forEach { event -> - fulfilledBingRule(event, params.rules)?.let { - Timber.v("[PushRules] Rule $it match for event ${event.eventId}") - defaultPushRuleService.dispatchBing(event, it) - } - } - defaultPushRuleService.dispatchFinish() + override suspend fun execute(params: ProcessEventForPushTask.Params) { + // Handle left rooms + params.syncResponse.leave.keys.forEach { + defaultPushRuleService.dispatchRoomLeft(it) } + val newJoinEvents = params.syncResponse.join + .map { entries -> + entries.value.timeline?.events?.map { it.copy(roomId = entries.key) } + } + .fold(emptyList(), { acc, next -> + acc + (next ?: emptyList()) + }) + val inviteEvents = params.syncResponse.invite + .map { entries -> + entries.value.inviteState?.events?.map { it.copy(roomId = entries.key) } + } + .fold(emptyList(), { acc, next -> + acc + (next ?: emptyList()) + }) + val allEvents = (newJoinEvents + inviteEvents).filter { event -> + when (event.type) { + EventType.MESSAGE, + EventType.REDACTION, + EventType.ENCRYPTED, + EventType.STATE_ROOM_MEMBER -> true + else -> false + } + }.filter { + it.senderId != sessionParams.credentials.userId + } + Timber.v("[PushRules] Found ${allEvents.size} out of ${(newJoinEvents + inviteEvents).size}" + + " to check for push rules with ${params.rules.size} rules") + allEvents.forEach { event -> + fulfilledBingRule(event, params.rules)?.let { + Timber.v("[PushRules] Rule $it match for event ${event.eventId}") + defaultPushRuleService.dispatchBing(event, it) + } + } + defaultPushRuleService.dispatchFinish() } private fun fulfilledBingRule(event: Event, rules: List): PushRule? { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddHttpPusherWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddHttpPusherWorker.kt index 6ac59607..bc587d91 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddHttpPusherWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddHttpPusherWorker.kt @@ -26,6 +26,7 @@ import im.vector.matrix.android.internal.database.mapper.toEntity import im.vector.matrix.android.internal.database.model.PusherEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.util.awaitTransaction import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.getSessionComponent import javax.inject.Inject @@ -55,15 +56,14 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters) if (pusher.pushKey.isBlank()) { return Result.failure() } - - val result = executeRequest { - apiCall = pushersAPI.setPusher(pusher) - } - return result.fold({ - when (it) { + return try { + setPusher(pusher, params.userId) + Result.success() + } catch (exception: Throwable) { + when (exception) { is Failure.NetworkConnection -> Result.retry() else -> { - monarchy.runTransactionSync { realm -> + monarchy.awaitTransaction { realm -> PusherEntity.where(realm, params.userId, pusher.pushKey).findFirst()?.let { //update it it.state = PusherState.FAILED_TO_REGISTER @@ -73,28 +73,31 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters) Result.failure() } } - }, { - monarchy.runTransactionSync { realm -> - val echo = PusherEntity.where(realm, params.userId, pusher.pushKey).findFirst() - if (echo != null) { - //update it - echo.appDisplayName = pusher.appDisplayName - echo.appId = pusher.appId - echo.kind = pusher.kind - echo.lang = pusher.lang - echo.profileTag = pusher.profileTag - echo.data?.format = pusher.data?.format - echo.data?.url = pusher.data?.url - echo.state = PusherState.REGISTERED - } else { - pusher.toEntity(params.userId).also { - it.state = PusherState.REGISTERED - realm.insertOrUpdate(it) - } + } + } + + private suspend fun setPusher(pusher: JsonPusher, userId: String) { + executeRequest { + apiCall = pushersAPI.setPusher(pusher) + } + monarchy.awaitTransaction { realm -> + val echo = PusherEntity.where(realm, userId, pusher.pushKey).findFirst() + if (echo != null) { + //update it + echo.appDisplayName = pusher.appDisplayName + echo.appId = pusher.appId + echo.kind = pusher.kind + echo.lang = pusher.lang + echo.profileTag = pusher.profileTag + echo.data?.format = pusher.data?.format + echo.data?.url = pusher.data?.url + echo.state = PusherState.REGISTERED + } else { + pusher.toEntity(userId).also { + it.state = PusherState.REGISTERED + realm.insertOrUpdate(it) } } - Result.success() - - }) + } } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt index 5d9af0d3..1c03a4f7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt @@ -29,7 +29,6 @@ import im.vector.matrix.android.internal.database.model.PusherEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith -import im.vector.matrix.android.internal.task.toConfigurableTask import im.vector.matrix.android.internal.worker.WorkManagerUtil import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder import im.vector.matrix.android.internal.worker.WorkerParamsFactory @@ -50,7 +49,7 @@ internal class DefaultPusherService @Inject constructor( override fun refreshPushers() { getPusherTask - .toConfigurableTask() + .configureWith() .executeBy(taskExecutor) } @@ -85,8 +84,9 @@ internal class DefaultPusherService @Inject constructor( override fun removeHttpPusher(pushkey: String, appId: String, callback: MatrixCallback) { val params = RemovePusherTask.Params(sessionParam.credentials.userId, pushkey, appId) removePusherTask - .configureWith(params) - .dispatchTo(callback) + .configureWith(params) { + this.callback = callback + } //.enableRetry() ?? .executeBy(taskExecutor) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushRulesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushRulesTask.kt index cbd8dcac..627c6f89 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushRulesTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushRulesTask.kt @@ -15,7 +15,6 @@ */ package im.vector.matrix.android.internal.session.pushers -import arrow.core.Try import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.pushrules.rest.GetPushRulesResponse @@ -24,7 +23,7 @@ import im.vector.matrix.android.internal.database.model.PushRulesEntity import im.vector.matrix.android.internal.database.model.PusherEntityFields import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task -import im.vector.matrix.android.internal.util.tryTransactionSync +import im.vector.matrix.android.internal.util.awaitTransaction import javax.inject.Inject @@ -39,58 +38,57 @@ internal class DefaultGetPushRulesTask @Inject constructor(private val pushRules private val monarchy: Monarchy, private val sessionParams: SessionParams) : GetPushRulesTask { - override suspend fun execute(params: GetPushRulesTask.Params): Try { - return executeRequest { + override suspend fun execute(params: GetPushRulesTask.Params) { + val response = executeRequest { apiCall = pushRulesApi.getAllRules() - }.flatMap { response -> - val scope = params.scope - return monarchy.tryTransactionSync { realm -> - //clear existings? - //TODO - realm.where(PushRulesEntity::class.java) - .equalTo(PusherEntityFields.USER_ID, sessionParams.credentials.userId) - .findAll().deleteAllFromRealm() + } + val scope = params.scope + monarchy.awaitTransaction { realm -> + //clear existings? + //TODO + realm.where(PushRulesEntity::class.java) + .equalTo(PusherEntityFields.USER_ID, sessionParams.credentials.userId) + .findAll().deleteAllFromRealm() - val content = PushRulesEntity(sessionParams.credentials.userId, scope, "content") - response.global.content?.forEach { rule -> - PushRulesMapper.map(rule).also { - content.pushRules.add(it) - } + val content = PushRulesEntity(sessionParams.credentials.userId, scope, "content") + response.global.content?.forEach { rule -> + PushRulesMapper.map(rule).also { + content.pushRules.add(it) } - realm.insertOrUpdate(content) - - val override = PushRulesEntity(sessionParams.credentials.userId, scope, "override") - response.global.override?.forEach { rule -> - PushRulesMapper.map(rule).also { - override.pushRules.add(it) - } - } - realm.insertOrUpdate(override) - - val rooms = PushRulesEntity(sessionParams.credentials.userId, scope, "room") - response.global.room?.forEach { rule -> - PushRulesMapper.map(rule).also { - rooms.pushRules.add(it) - } - } - realm.insertOrUpdate(rooms) - - val senders = PushRulesEntity(sessionParams.credentials.userId, scope, "sender") - response.global.sender?.forEach { rule -> - PushRulesMapper.map(rule).also { - senders.pushRules.add(it) - } - } - realm.insertOrUpdate(senders) - - val underrides = PushRulesEntity(sessionParams.credentials.userId, scope, "underride") - response.global.underride?.forEach { rule -> - PushRulesMapper.map(rule).also { - underrides.pushRules.add(it) - } - } - realm.insertOrUpdate(underrides) } + realm.insertOrUpdate(content) + + val override = PushRulesEntity(sessionParams.credentials.userId, scope, "override") + response.global.override?.forEach { rule -> + PushRulesMapper.map(rule).also { + override.pushRules.add(it) + } + } + realm.insertOrUpdate(override) + + val rooms = PushRulesEntity(sessionParams.credentials.userId, scope, "room") + response.global.room?.forEach { rule -> + PushRulesMapper.map(rule).also { + rooms.pushRules.add(it) + } + } + realm.insertOrUpdate(rooms) + + val senders = PushRulesEntity(sessionParams.credentials.userId, scope, "sender") + response.global.sender?.forEach { rule -> + PushRulesMapper.map(rule).also { + senders.pushRules.add(it) + } + } + realm.insertOrUpdate(senders) + + val underrides = PushRulesEntity(sessionParams.credentials.userId, scope, "underride") + response.global.underride?.forEach { rule -> + PushRulesMapper.map(rule).also { + underrides.pushRules.add(it) + } + } + realm.insertOrUpdate(underrides) } } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushersTask.kt index 57ea8a38..b3199ea3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushersTask.kt @@ -15,7 +15,6 @@ */ package im.vector.matrix.android.internal.session.pushers -import arrow.core.Try import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.session.pushers.PusherState @@ -24,7 +23,7 @@ import im.vector.matrix.android.internal.database.model.PusherEntity import im.vector.matrix.android.internal.database.model.PusherEntityFields import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task -import im.vector.matrix.android.internal.util.tryTransactionSync +import im.vector.matrix.android.internal.util.awaitTransaction import javax.inject.Inject internal interface GetPushersTask : Task @@ -33,20 +32,19 @@ internal class DefaultGetPusherTask @Inject constructor(private val pushersAPI: private val monarchy: Monarchy, private val sessionParams: SessionParams) : GetPushersTask { - override suspend fun execute(params: Unit): Try { - return executeRequest { + override suspend fun execute(params: Unit) { + val response = executeRequest { apiCall = pushersAPI.getPushers() - }.flatMap { response -> - monarchy.tryTransactionSync { realm -> - //clear existings? - realm.where(PusherEntity::class.java) - .equalTo(PusherEntityFields.USER_ID, sessionParams.credentials.userId) - .findAll().deleteAllFromRealm() - response.pushers?.forEach { jsonPusher -> - jsonPusher.toEntity(sessionParams.credentials.userId).also { - it.state = PusherState.REGISTERED - realm.insertOrUpdate(it) - } + } + monarchy.awaitTransaction { realm -> + //clear existings? + realm.where(PusherEntity::class.java) + .equalTo(PusherEntityFields.USER_ID, sessionParams.credentials.userId) + .findAll().deleteAllFromRealm() + response.pushers?.forEach { jsonPusher -> + jsonPusher.toEntity(sessionParams.credentials.userId).also { + it.state = PusherState.REGISTERED + realm.insertOrUpdate(it) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/RemovePusherTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/RemovePusherTask.kt index 92f62fe0..0ed7175e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/RemovePusherTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/RemovePusherTask.kt @@ -16,16 +16,15 @@ package im.vector.matrix.android.internal.session.pushers -import arrow.core.Try import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.session.pushers.Pusher import im.vector.matrix.android.api.session.pushers.PusherState import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.PusherEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task -import im.vector.matrix.android.internal.util.tryTransactionSync +import im.vector.matrix.android.internal.util.awaitTransaction +import io.realm.Realm import javax.inject.Inject internal interface RemovePusherTask : Task { @@ -39,42 +38,33 @@ internal class DefaultRemovePusherTask @Inject constructor( private val monarchy: Monarchy ) : RemovePusherTask { - override suspend fun execute(params: RemovePusherTask.Params): Try { - return Try { - var existing: Pusher? = null - monarchy.runTransactionSync { - val existingEntity = PusherEntity.where(it, params.userId, params.pushKey).findFirst() - existingEntity?.state == PusherState.UNREGISTERING - existing = existingEntity?.asDomain() - } - if (existing == null) { - throw Exception("No existing pusher") - } else { - existing!! - } - }.flatMap { - executeRequest { - val deleteBody = JsonPusher( - pushKey = params.pushKey, - appId = params.pushAppId, - // kind null deletes the pusher - kind = null, - appDisplayName = it.appDisplayName ?: "", - deviceDisplayName = it.deviceDisplayName ?: "", - profileTag = it.profileTag ?: "", - lang = it.lang, - data = JsonPusherData(it.data.url, it.data.format), - append = false - ) - apiCall = pushersAPI.setPusher(deleteBody) - } - }.flatMap { - monarchy.tryTransactionSync { - val existing = PusherEntity.where(it, params.userId, params.pushKey).findFirst() - existing?.deleteFromRealm() - } + override suspend fun execute(params: RemovePusherTask.Params) { + monarchy.awaitTransaction { realm -> + val existingEntity = PusherEntity.where(realm, params.userId, params.pushKey).findFirst() + existingEntity?.state = PusherState.UNREGISTERING + } + + val existing = Realm.getInstance(monarchy.realmConfiguration).use { realm -> + PusherEntity.where(realm, params.userId, params.pushKey).findFirst()?.asDomain() + } ?: throw Exception("No existing pusher") + + val deleteBody = JsonPusher( + pushKey = params.pushKey, + appId = params.pushAppId, + // kind null deletes the pusher + kind = null, + appDisplayName = existing.appDisplayName ?: "", + deviceDisplayName = existing.deviceDisplayName ?: "", + profileTag = existing.profileTag ?: "", + lang = existing.lang, + data = JsonPusherData(existing.data.url, existing.data.format), + append = false + ) + executeRequest { + apiCall = pushersAPI.setPusher(deleteBody) + } + monarchy.awaitTransaction { + PusherEntity.where(it, params.userId, params.pushKey).findFirst()?.deleteFromRealm() } } - - } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt index 9af67a6b..828d9e71 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt @@ -15,7 +15,6 @@ */ package im.vector.matrix.android.internal.session.pushers -import arrow.core.Try import im.vector.matrix.android.api.pushrules.rest.PushRule import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task @@ -31,7 +30,7 @@ internal interface UpdatePushRuleEnableStatusTask : Task { + override suspend fun execute(params: UpdatePushRuleEnableStatusTask.Params) { return executeRequest { apiCall = pushRulesApi.updateEnableRuleStatus(params.kind, params.pushRule.ruleId, params.enabled) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt index 0b13fa3c..4aeace70 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt @@ -27,7 +27,6 @@ import im.vector.matrix.android.internal.session.room.directory.GetThirdPartyPro import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith -import im.vector.matrix.android.internal.task.toConfigurableTask import javax.inject.Inject internal class DefaultRoomDirectoryService @Inject constructor(private val getPublicRoomTask: GetPublicRoomTask, @@ -39,22 +38,25 @@ internal class DefaultRoomDirectoryService @Inject constructor(private val getPu publicRoomsParams: PublicRoomsParams, callback: MatrixCallback): Cancelable { return getPublicRoomTask - .configureWith(GetPublicRoomTask.Params(server, publicRoomsParams)) - .dispatchTo(callback) + .configureWith(GetPublicRoomTask.Params(server, publicRoomsParams)) { + this.callback = callback + } .executeBy(taskExecutor) } - override fun joinRoom(roomId: String, callback: MatrixCallback) { - joinRoomTask - .configureWith(JoinRoomTask.Params(roomId)) - .dispatchTo(callback) + override fun joinRoom(roomId: String, callback: MatrixCallback): Cancelable { + return joinRoomTask + .configureWith(JoinRoomTask.Params(roomId)) { + this.callback = callback + } .executeBy(taskExecutor) } - override fun getThirdPartyProtocol(callback: MatrixCallback>) { - getThirdPartyProtocolsTask - .toConfigurableTask() - .dispatchTo(callback) + override fun getThirdPartyProtocol(callback: MatrixCallback>): Cancelable { + return getThirdPartyProtocolsTask + .configureWith { + this.callback = callback + } .executeBy(taskExecutor) } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index 2c0f1ce9..bd5462b1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -22,40 +22,62 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.session.room.model.VersioningState import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams +import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.room.create.CreateRoomTask +import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith -import im.vector.matrix.android.internal.util.fetchManaged +import io.realm.Realm import javax.inject.Inject internal class DefaultRoomService @Inject constructor(private val monarchy: Monarchy, private val roomSummaryMapper: RoomSummaryMapper, private val createRoomTask: CreateRoomTask, + private val joinRoomTask: JoinRoomTask, private val roomFactory: RoomFactory, private val taskExecutor: TaskExecutor) : RoomService { - override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback) { - createRoomTask - .configureWith(createRoomParams) - .dispatchTo(callback) + override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback): Cancelable { + return createRoomTask + .configureWith(createRoomParams) { + this.callback = callback + } .executeBy(taskExecutor) } override fun getRoom(roomId: String): Room? { - monarchy.fetchManaged { RoomEntity.where(it, roomId).findFirst() } ?: return null - return roomFactory.create(roomId) + return Realm.getInstance(monarchy.realmConfiguration).use { + if (RoomEntity.where(it, roomId).findFirst() != null) { + roomFactory.create(roomId) + } else { + null + } + } } override fun liveRoomSummaries(): LiveData> { return monarchy.findAllMappedWithChanges( - { realm -> RoomSummaryEntity.where(realm).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) }, + { realm -> + RoomSummaryEntity.where(realm) + .isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) + .notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) + }, { roomSummaryMapper.map(it) } ) } + + override fun joinRoom(roomId: String, viaServers: List, callback: MatrixCallback): Cancelable { + return joinRoomTask + .configureWith(JoinRoomTask.Params(roomId, viaServers)) { + this.callback = callback + } + .executeBy(taskExecutor) + } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt index 867ca287..786ba168 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt @@ -15,7 +15,6 @@ */ package im.vector.matrix.android.internal.session.room -import arrow.core.Try import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.crypto.MXCryptoError @@ -31,7 +30,7 @@ import im.vector.matrix.android.internal.database.query.create import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.task.Task -import im.vector.matrix.android.internal.util.tryTransactionSync +import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm import timber.log.Timber import javax.inject.Inject @@ -54,10 +53,10 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( //OPT OUT serer aggregation until API mature enough private val SHOULD_HANDLE_SERVER_AGREGGATION = false - override suspend fun execute(params: EventRelationsAggregationTask.Params): Try { + override suspend fun execute(params: EventRelationsAggregationTask.Params) { val events = params.events val userId = params.userId - return monarchy.tryTransactionSync { realm -> + monarchy.awaitTransaction { realm -> Timber.v(">>> DefaultEventRelationsAggregationTask[${params.hashCode()}] called with ${events.size} events") update(realm, events, userId) Timber.v("<<< DefaultEventRelationsAggregationTask[${params.hashCode()}] finished") @@ -83,21 +82,21 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( if (event.unsignedData?.relations?.annotations != null) { Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}") handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm) - } else { - val content: MessageContent? = event.content.toModel() - if (content?.relatesTo?.type == RelationType.REPLACE) { - Timber.v("###REPLACE in room $roomId for event ${event.eventId}") - //A replace! - handleReplace(realm, event, content, roomId, isLocalEcho) + + EventAnnotationsSummaryEntity.where(realm, event.eventId + ?: "").findFirst()?.let { + TimelineEventEntity.where(realm, eventId = event.eventId + ?: "").findFirst()?.let { tet -> + tet.annotations = it + } } } - EventAnnotationsSummaryEntity.where(realm, event.eventId - ?: "").findFirst()?.let { - TimelineEventEntity.where(realm, eventId = event.eventId - ?: "").findFirst()?.let { tet -> - tet.annotations = it - } + val content: MessageContent? = event.content.toModel() + if (content?.relatesTo?.type == RelationType.REPLACE) { + Timber.v("###REPLACE in room $roomId for event ${event.eventId}") + //A replace! + handleReplace(realm, event, content, roomId, isLocalEcho) } @@ -178,11 +177,12 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( Timber.v("###REPLACE new edit summary for ${targetEventId}, creating one (localEcho:$isLocalEcho)") //create the edit summary val editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java) - editSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis() editSummary.aggregatedContent = ContentMapper.map(newContent) if (isLocalEcho) { + editSummary.lastEditTs = 0 editSummary.sourceLocalEchoEvents.add(eventId) } else { + editSummary.lastEditTs = event.originServerTs ?: 0 editSummary.sourceEvents.add(eventId) } @@ -200,13 +200,26 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( Timber.v("###REPLACE Receiving remote echo of edit (edit already done)") existingSummary.sourceLocalEchoEvents.remove(txId) existingSummary.sourceEvents.add(event.eventId) - } else if (event.originServerTs ?: 0 > existingSummary.lastEditTs) { + } else if ( + isLocalEcho // do not rely on ts for local echo, take it + || event.originServerTs ?: 0 >= existingSummary.lastEditTs + ) { Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)") - existingSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis() + if (!isLocalEcho) { + //Do not take local echo originServerTs here, could mess up ordering (keep old ts) + existingSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis() + } existingSummary.aggregatedContent = ContentMapper.map(newContent) - existingSummary.sourceEvents.add(eventId) + if (isLocalEcho) { + existingSummary.sourceLocalEchoEvents.add(eventId) + } else { + existingSummary.sourceEvents.add(eventId) + } } else { - //ignore this event for the summary + //ignore this event for the summary (back paginate) + if (!isLocalEcho) { + existingSummary.sourceEvents.add(eventId) + } Timber.v("###REPLACE ignoring event for summary, it's to old $eventId") } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index 361a935d..0a5dcea3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -218,6 +218,7 @@ internal interface RoomAPI { */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/join") fun join(@Path("roomId") roomId: String, + @Query("server_name") viaServers: List, @Body params: Map): Call /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index 98cf872b..1c64c91b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -63,7 +63,7 @@ internal class RoomFactory @Inject constructor(private val context: Context, fun create(roomId: String): Room { val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, cryptoService, paginationTask) val sendService = DefaultSendService(context, credentials, roomId, eventFactory, cryptoService, monarchy) - val stateService = DefaultStateService(roomId, taskExecutor, sendStateTask) + val stateService = DefaultStateService(roomId, monarchy.realmConfiguration, taskExecutor, sendStateTask) val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask) val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, credentials) val relationService = DefaultRelationService(context, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index 6bcac9b8..dda8b932 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -85,7 +85,7 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C roomSummaryEntity.membership = membership } - val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, includedTypes = PREVIEWABLE_TYPES) + val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, includedTypes = PREVIEWABLE_TYPES) val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain() val otherRoomMembers = RoomMembers(realm, roomId) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt index 73d9b6f2..93c2eb0b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt @@ -16,42 +16,77 @@ package im.vector.matrix.android.internal.session.room.create -import arrow.core.Try +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse import im.vector.matrix.android.internal.database.RealmQueryLatch -import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntityFields +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask +import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper +import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.RealmConfiguration +import java.util.concurrent.TimeUnit import javax.inject.Inject internal interface CreateRoomTask : Task internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: RoomAPI, - @SessionDatabase private val realmConfiguration: RealmConfiguration) : CreateRoomTask { + private val monarchy: Monarchy, + private val directChatsHelper: DirectChatsHelper, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val readMarkersTask: SetReadMarkersTask, + @SessionDatabase + private val realmConfiguration: RealmConfiguration) : CreateRoomTask { - override suspend fun execute(params: CreateRoomParams): Try { - return executeRequest { + override suspend fun execute(params: CreateRoomParams): String { + val createRoomResponse = executeRequest { apiCall = roomAPI.createRoom(params) - }.flatMap { createRoomResponse -> - val roomId = createRoomResponse.roomId!! - - // TODO Maybe do the same code for join room request ? - // Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before) - val rql = RealmQueryLatch(realmConfiguration) { realm -> - realm.where(RoomEntity::class.java) - .equalTo(RoomEntityFields.ROOM_ID, roomId) - } - - rql.await() - - return Try.just(roomId) } + val roomId = createRoomResponse.roomId!! + // Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before) + val rql = RealmQueryLatch(realmConfiguration) { realm -> + realm.where(RoomEntity::class.java) + .equalTo(RoomEntityFields.ROOM_ID, roomId) + } + try { + rql.await(timeout = 1L, timeUnit = TimeUnit.MINUTES) + } catch (exception: Exception) { + throw CreateRoomFailure.CreatedWithTimeout + } + if (params.isDirect()) { + handleDirectChatCreation(params, roomId) + } + setReadMarkers(roomId) + return roomId } + + private suspend fun handleDirectChatCreation(params: CreateRoomParams, roomId: String) { + val otherUserId = params.getFirstInvitedUserId() + ?: throw IllegalStateException("You can't create a direct room without an invitedUser") + + monarchy.awaitTransaction { realm -> + RoomSummaryEntity.where(realm, roomId).findFirst()?.apply { + this.directUserId = otherUserId + this.isDirect = true + } + } + val directChats = directChatsHelper.getLocalUserAccount() + updateUserAccountDataTask.execute(UpdateUserAccountDataTask.DirectChatParams(directMessages = directChats)) + } + + private suspend fun setReadMarkers(roomId: String) { + val setReadMarkerParams = SetReadMarkersTask.Params(roomId, markAllAsRead = true) + return readMarkersTask.execute(setReadMarkerParams) + } + } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt new file mode 100644 index 00000000..1bc6b965 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/RoomCreateEventLiveObserver.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.create + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.VersioningState +import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent +import im.vector.matrix.android.internal.database.RealmLiveEntityObserver +import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity +import im.vector.matrix.android.internal.database.query.types +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.SessionDatabase +import io.realm.OrderedCollectionChangeSet +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.RealmResults +import javax.inject.Inject + +internal class RoomCreateEventLiveObserver @Inject constructor(@SessionDatabase + realmConfiguration: RealmConfiguration) + : RealmLiveEntityObserver(realmConfiguration) { + + override val query = Monarchy.Query { + EventEntity.types(it, listOf(EventType.STATE_ROOM_CREATE)) + } + + override fun onChange(results: RealmResults, changeSet: OrderedCollectionChangeSet) { + changeSet.insertions + .asSequence() + .mapNotNull { + results[it]?.asDomain() + } + .toList() + .also { + handleRoomCreateEvents(it) + } + } + + private fun handleRoomCreateEvents(createEvents: List) = Realm.getInstance(realmConfiguration).use { + it.executeTransactionAsync { realm -> + for (event in createEvents) { + val createRoomContent = event.getClearContent().toModel() + val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: continue + + val predecessorRoomSummary = RoomSummaryEntity.where(realm, predecessorRoomId).findFirst() + ?: RoomSummaryEntity(predecessorRoomId) + predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_JOINED + realm.insertOrUpdate(predecessorRoomSummary) + + } + } + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/directory/GetPublicRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/directory/GetPublicRoomTask.kt index b2cc1a74..a24765e0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/directory/GetPublicRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/directory/GetPublicRoomTask.kt @@ -16,11 +16,9 @@ package im.vector.matrix.android.internal.session.room.directory -import arrow.core.Try import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -34,7 +32,7 @@ internal interface GetPublicRoomTask : Task { + override suspend fun execute(params: GetPublicRoomTask.Params): PublicRoomsResponse { return executeRequest { apiCall = roomAPI.publicRooms(params.server, params.publicRoomsParams) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/directory/GetThirdPartyProtocolsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/directory/GetThirdPartyProtocolsTask.kt index cd53c92d..4d3bc8fd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/directory/GetThirdPartyProtocolsTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/directory/GetThirdPartyProtocolsTask.kt @@ -16,19 +16,17 @@ package im.vector.matrix.android.internal.session.room.directory -import arrow.core.Try import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.task.Task import javax.inject.Inject internal interface GetThirdPartyProtocolsTask : Task> -internal class DefaultGetThirdPartyProtocolsTask @Inject constructor (private val roomAPI: RoomAPI) : GetThirdPartyProtocolsTask { +internal class DefaultGetThirdPartyProtocolsTask @Inject constructor(private val roomAPI: RoomAPI) : GetThirdPartyProtocolsTask { - override suspend fun execute(params: Unit): Try> { + override suspend fun execute(params: Unit): Map { return executeRequest { apiCall = roomAPI.thirdPartyProtocols() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt index 01fb7461..7fca1e42 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt @@ -42,9 +42,13 @@ internal class DefaultMembershipService @Inject constructor(private val roomId: private val leaveRoomTask: LeaveRoomTask ) : MembershipService { - override fun loadRoomMembersIfNeeded(): Cancelable { + override fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback): Cancelable { val params = LoadRoomMembersTask.Params(roomId, Membership.LEAVE) - return loadRoomMembersTask.configureWith(params).executeBy(taskExecutor) + return loadRoomMembersTask + .configureWith(params) { + this.callback = matrixCallback + } + .executeBy(taskExecutor) } override fun getRoomMember(userId: String): RoomMember? { @@ -73,24 +77,30 @@ internal class DefaultMembershipService @Inject constructor(private val roomId: return result } - override fun invite(userId: String, callback: MatrixCallback) { + override fun invite(userId: String, callback: MatrixCallback): Cancelable { val params = InviteTask.Params(roomId, userId) - inviteTask.configureWith(params) - .dispatchTo(callback) + return inviteTask + .configureWith(params) { + this.callback = callback + } .executeBy(taskExecutor) } - override fun join(callback: MatrixCallback) { - val params = JoinRoomTask.Params(roomId) - joinTask.configureWith(params) - .dispatchTo(callback) + override fun join(viaServers: List, callback: MatrixCallback): Cancelable { + val params = JoinRoomTask.Params(roomId, viaServers) + return joinTask + .configureWith(params) { + this.callback = callback + } .executeBy(taskExecutor) } - override fun leave(callback: MatrixCallback) { + override fun leave(callback: MatrixCallback): Cancelable { val params = LeaveRoomTask.Params(roomId) - leaveRoomTask.configureWith(params) - .dispatchTo(callback) + return leaveRoomTask + .configureWith(params) { + this.callback = callback + } .executeBy(taskExecutor) } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt index a3090605..709e8da9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt @@ -16,8 +16,6 @@ package im.vector.matrix.android.internal.session.room.membership -import arrow.core.Try -import com.squareup.moshi.JsonReader import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.internal.database.helper.addStateEvent @@ -30,14 +28,12 @@ import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.session.user.UserEntityFactory import im.vector.matrix.android.internal.task.Task -import im.vector.matrix.android.internal.util.tryTransactionSync +import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm import io.realm.kotlin.createObject -import okhttp3.ResponseBody -import okio.Okio import javax.inject.Inject -internal interface LoadRoomMembersTask : Task { +internal interface LoadRoomMembersTask : Task { data class Params( val roomId: String, @@ -51,39 +47,36 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAP private val roomSummaryUpdater: RoomSummaryUpdater ) : LoadRoomMembersTask { - override suspend fun execute(params: LoadRoomMembersTask.Params): Try { - return if (areAllMembersAlreadyLoaded(params.roomId)) { - Try.just(true) - } else { - val lastToken = syncTokenStore.getLastToken() - executeRequest { - apiCall = roomAPI.getMembers(params.roomId, lastToken, null, params.excludeMembership?.value) - }.flatMap { response -> - insertInDb(response, params.roomId) - }.map { true } + override suspend fun execute(params: LoadRoomMembersTask.Params) { + if (areAllMembersAlreadyLoaded(params.roomId)) { + return } + val lastToken = syncTokenStore.getLastToken() + val response = executeRequest { + apiCall = roomAPI.getMembers(params.roomId, lastToken, null, params.excludeMembership?.value) + } + insertInDb(response, params.roomId) } - private fun insertInDb(response: RoomMembersResponse, roomId: String): Try { - return monarchy - .tryTransactionSync { realm -> - // We ignore all the already known members - val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + private suspend fun insertInDb(response: RoomMembersResponse, roomId: String) { + monarchy.awaitTransaction { realm -> + // We ignore all the already known members + val roomEntity = RoomEntity.where(realm, roomId).findFirst() + ?: realm.createObject(roomId) - for (roomMemberEvent in response.roomMemberEvents) { - roomEntity.addStateEvent(roomMemberEvent) - UserEntityFactory.createOrNull(roomMemberEvent)?.also { - realm.insertOrUpdate(it) - } - } - roomEntity.chunks.flatMap { it.timelineEvents }.forEach { - it.updateSenderData() - } - roomEntity.areAllMembersLoaded = true - roomSummaryUpdater.update(realm, roomId) + for (roomMemberEvent in response.roomMemberEvents) { + roomEntity.addStateEvent(roomMemberEvent) + UserEntityFactory.createOrNull(roomMemberEvent)?.also { + realm.insertOrUpdate(it) } + } + roomEntity.chunks.flatMap { it.timelineEvents }.forEach { + it.updateSenderData() + } + roomEntity.areAllMembersLoaded = true + roomSummaryUpdater.update(realm, roomId) + } } private fun areAllMembersAlreadyLoaded(roomId: String): Boolean { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt index 948f1741..815fc96e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -22,11 +22,7 @@ import im.vector.matrix.android.R import im.vector.matrix.android.api.auth.data.Credentials 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.Membership -import im.vector.matrix.android.api.session.room.model.RoomAliasesContent -import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent -import im.vector.matrix.android.api.session.room.model.RoomMember -import im.vector.matrix.android.api.session.room.model.RoomNameContent +import im.vector.matrix.android.api.session.room.model.* import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntityFields @@ -34,7 +30,6 @@ import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where -import io.realm.RealmResults import javax.inject.Inject /** @@ -81,10 +76,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: val roomMembers = RoomMembers(realm, roomId) val loadedMembers = roomMembers.queryRoomMembersEvent().findAll() - val otherMembersSubset = loadedMembers.where() - .notEqualTo(EventEntityFields.STATE_KEY, credentials.userId) - .limit(3) - .findAll() + if (roomEntity?.membership == Membership.INVITE) { val inviteMeEvent = roomMembers.queryRoomMemberEvent(credentials.userId).findFirst() @@ -97,23 +89,29 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: } else { context.getString(R.string.room_displayname_room_invite) } - } else { + } else if (roomEntity?.membership == Membership.JOIN) { val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - val memberIds: List = if (roomSummary?.heroes?.isNotEmpty() == true) { - roomSummary.heroes + val otherMembersSubset: List = if (roomSummary?.heroes?.isNotEmpty() == true) { + roomSummary.heroes.mapNotNull { + roomMembers.getStateEvent(it) + } } else { - otherMembersSubset.mapNotNull { it.stateKey } + loadedMembers.where() + .notEqualTo(EventEntityFields.STATE_KEY, credentials.userId) + .limit(3) + .findAll() } - name = when (memberIds.size) { + val otherMembersCount = roomMembers.getNumberOfMembers() - 1 + name = when (otherMembersCount) { 0 -> context.getString(R.string.room_displayname_empty_room) - 1 -> resolveRoomMember(otherMembersSubset[0], roomMembers) + 1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers) 2 -> context.getString(R.string.room_displayname_two_members, - resolveRoomMember(otherMembersSubset[0], roomMembers), - resolveRoomMember(otherMembersSubset[1], roomMembers) + resolveRoomMemberName(otherMembersSubset[0], roomMembers), + resolveRoomMemberName(otherMembersSubset[1], roomMembers) ) else -> context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members, roomMembers.getNumberOfJoinedMembers() - 1, - resolveRoomMember(otherMembersSubset[0], roomMembers), + resolveRoomMemberName(otherMembersSubset[0], roomMembers), roomMembers.getNumberOfJoinedMembers() - 1) } } @@ -122,8 +120,8 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: return name ?: roomId } - private fun resolveRoomMember(eventEntity: EventEntity?, - roomMembers: RoomMembers): String? { + private fun resolveRoomMemberName(eventEntity: EventEntity?, + roomMembers: RoomMembers): String? { if (eventEntity == null) return null val roomMember = eventEntity.toRoomMember() ?: return null val isUnique = roomMembers.isUniqueDisplayName(roomMember.displayName) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt index fb8326f2..8db3f170 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt @@ -42,12 +42,16 @@ internal class RoomMembers(private val realm: Realm, RoomSummaryEntity.where(realm, roomId).findFirst() } - fun get(userId: String): RoomMember? { + fun getStateEvent(userId: String): EventEntity? { return EventEntity .where(realm, roomId, EventType.STATE_ROOM_MEMBER) .sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING) .equalTo(EventEntityFields.STATE_KEY, userId) .findFirst() + } + + fun get(userId: String): RoomMember? { + return getStateEvent(userId) ?.let { it.asDomain().content?.toModel() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt index 5f656c01..68d72c6b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt @@ -16,9 +16,7 @@ package im.vector.matrix.android.internal.session.room.membership.joining -import arrow.core.Try import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -33,7 +31,7 @@ internal interface InviteTask : Task { internal class DefaultInviteTask @Inject constructor(private val roomAPI: RoomAPI) : InviteTask { - override suspend fun execute(params: InviteTask.Params): Try { + override suspend fun execute(params: InviteTask.Params) { return executeRequest { val body = InviteBody(params.userId) apiCall = roomAPI.invite(params.roomId, body) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt index 96454cbf..5ca23a0d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt @@ -16,25 +16,52 @@ package im.vector.matrix.android.internal.session.room.membership.joining -import arrow.core.Try +import im.vector.matrix.android.api.session.room.failure.JoinRoomFailure +import im.vector.matrix.android.internal.database.RealmQueryLatch +import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.model.RoomEntityFields +import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask import im.vector.matrix.android.internal.task.Task +import io.realm.RealmConfiguration +import java.util.concurrent.TimeUnit import javax.inject.Inject internal interface JoinRoomTask : Task { data class Params( - val roomId: String + val roomId: String, + val viaServers: List = emptyList() ) } -internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: RoomAPI) : JoinRoomTask { +internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: RoomAPI, + private val readMarkersTask: SetReadMarkersTask, + @SessionDatabase + private val realmConfiguration: RealmConfiguration) : JoinRoomTask { - override suspend fun execute(params: JoinRoomTask.Params): Try { - return executeRequest { - apiCall = roomAPI.join(params.roomId, HashMap()) + override suspend fun execute(params: JoinRoomTask.Params) { + executeRequest { + apiCall = roomAPI.join(params.roomId, params.viaServers, HashMap()) } + val roomId = params.roomId + // Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before) + val rql = RealmQueryLatch(realmConfiguration) { realm -> + realm.where(RoomEntity::class.java) + .equalTo(RoomEntityFields.ROOM_ID, roomId) + } + try { + rql.await(timeout = 1L, timeUnit = TimeUnit.MINUTES) + } catch (exception: Exception) { + throw JoinRoomFailure.JoinedWithTimeout + } + setReadMarkers(roomId) + } + + private suspend fun setReadMarkers(roomId: String) { + val setReadMarkerParams = SetReadMarkersTask.Params(roomId, markAllAsRead = true) + readMarkersTask.execute(setReadMarkerParams) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt index fe055e99..d4ff169c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt @@ -16,9 +16,7 @@ package im.vector.matrix.android.internal.session.room.membership.leaving -import arrow.core.Try import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -31,7 +29,7 @@ internal interface LeaveRoomTask : Task { internal class DefaultLeaveRoomTask @Inject constructor(private val roomAPI: RoomAPI) : LeaveRoomTask { - override suspend fun execute(params: LeaveRoomTask.Params): Try { + override suspend fun execute(params: LeaveRoomTask.Params) { return executeRequest { apiCall = roomAPI.leave(params.roomId, HashMap()) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt index 91d3c4e7..c63733f3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt @@ -23,10 +23,7 @@ import im.vector.matrix.android.internal.database.RealmLiveEntityObserver import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.types -import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.SessionDatabase -import im.vector.matrix.android.internal.session.SessionScope -import im.vector.matrix.android.internal.session.room.EventRelationsAggregationTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import io.realm.OrderedCollectionChangeSet diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt index 24dc14a7..35df20df 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt @@ -15,12 +15,10 @@ */ package im.vector.matrix.android.internal.session.room.prune -import arrow.core.Try import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.UnsignedData -import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.database.helper.updateSenderData import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.mapper.EventMapper @@ -29,8 +27,9 @@ import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.findWithSenderMembershipEvent import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.task.Task -import im.vector.matrix.android.internal.util.tryTransactionSync +import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm import timber.log.Timber import javax.inject.Inject @@ -47,8 +46,8 @@ internal interface PruneEventTask : Task { internal class DefaultPruneEventTask @Inject constructor(private val monarchy: Monarchy) : PruneEventTask { - override suspend fun execute(params: PruneEventTask.Params): Try { - return monarchy.tryTransactionSync { realm -> + override suspend fun execute(params: PruneEventTask.Params) { + monarchy.awaitTransaction { realm -> params.redactionEvents.forEach { event -> pruneEvent(realm, event, params.userId) } @@ -63,7 +62,7 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M val redactionEventEntity = EventEntity.where(realm, eventId = redactionEvent.eventId ?: "").findFirst() ?: return - val isLocalEcho = redactionEventEntity.sendState == SendState.UNSENT + val isLocalEcho = LocalEchoEventFactory.isLocalEchoId(redactionEvent.eventId ?: "") Timber.v("Redact event for ${redactionEvent.redacts} localEcho=$isLocalEcho") val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt index ff899968..2e30c12e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt @@ -22,14 +22,11 @@ import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntity 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.latestEvent import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith -import im.vector.matrix.android.internal.util.fetchCopied import javax.inject.Inject internal class DefaultReadService @Inject constructor(private val roomId: String, @@ -39,37 +36,44 @@ internal class DefaultReadService @Inject constructor(private val roomId: String private val credentials: Credentials) : ReadService { override fun markAllAsRead(callback: MatrixCallback) { - //TODO shouldn't it be latest synced event? - val latestEvent = getLatestEvent() - val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = latestEvent?.eventId, readReceiptEventId = latestEvent?.eventId) - setReadMarkersTask.configureWith(params).dispatchTo(callback).executeBy(taskExecutor) + val params = SetReadMarkersTask.Params(roomId, markAllAsRead = true) + setReadMarkersTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) } override fun setReadReceipt(eventId: String, callback: MatrixCallback) { val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = null, readReceiptEventId = eventId) - setReadMarkersTask.configureWith(params).dispatchTo(callback).executeBy(taskExecutor) + setReadMarkersTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) } override fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback) { val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = fullyReadEventId, readReceiptEventId = null) - setReadMarkersTask.configureWith(params).dispatchTo(callback).executeBy(taskExecutor) + setReadMarkersTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) } - private fun getLatestEvent(): TimelineEventEntity? { - return monarchy.fetchCopied { TimelineEventEntity.latestEvent(it, roomId) } - } override fun isEventRead(eventId: String): Boolean { var isEventRead = false monarchy.doWithRealm { val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst() - ?: return@doWithRealm + ?: return@doWithRealm val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId) - ?: return@doWithRealm + ?: return@doWithRealm val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex - ?: Int.MIN_VALUE + ?: Int.MIN_VALUE val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex - ?: Int.MAX_VALUE + ?: Int.MAX_VALUE isEventRead = eventToCheckIndex <= readReceiptIndex } return isEventRead diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index 2106ab55..41c9cca5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -16,11 +16,9 @@ package im.vector.matrix.android.internal.session.room.read -import arrow.core.Try import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity @@ -32,7 +30,7 @@ import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.task.Task -import im.vector.matrix.android.internal.util.tryTransactionAsync +import io.realm.Realm import timber.log.Timber import javax.inject.Inject @@ -40,8 +38,9 @@ internal interface SetReadMarkersTask : Task { data class Params( val roomId: String, - val fullyReadEventId: String?, - val readReceiptEventId: String? + val markAllAsRead: Boolean = false, + val fullyReadEventId: String? = null, + val readReceiptEventId: String? = null ) } @@ -53,40 +52,53 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI private val monarchy: Monarchy ) : SetReadMarkersTask { - override suspend fun execute(params: SetReadMarkersTask.Params): Try { + override suspend fun execute(params: SetReadMarkersTask.Params) { val markers = HashMap() - if (params.fullyReadEventId != null) { - if (LocalEchoEventFactory.isLocalEchoId(params.fullyReadEventId)) { - Timber.w("Can't set read marker for local event ${params.fullyReadEventId}") - } else { - markers[READ_MARKER] = params.fullyReadEventId - } - } - if (params.readReceiptEventId != null - && !isEventRead(params.roomId, params.readReceiptEventId)) { + val fullyReadEventId: String? + val readReceiptEventId: String? - if (LocalEchoEventFactory.isLocalEchoId(params.readReceiptEventId)) { + if (params.markAllAsRead) { + val latestSyncedEventId = Realm.getInstance(monarchy.realmConfiguration).use { realm -> + TimelineEventEntity.latestEvent(realm, roomId = params.roomId, includesSending = false)?.eventId + } + fullyReadEventId = latestSyncedEventId + readReceiptEventId = latestSyncedEventId + } else { + fullyReadEventId = params.fullyReadEventId + readReceiptEventId = params.readReceiptEventId + } + + if (fullyReadEventId != null) { + if (LocalEchoEventFactory.isLocalEchoId(fullyReadEventId)) { Timber.w("Can't set read marker for local event ${params.fullyReadEventId}") } else { - updateNotificationCountIfNecessary(params.roomId, params.readReceiptEventId) - markers[READ_RECEIPT] = params.readReceiptEventId + markers[READ_MARKER] = fullyReadEventId } } - return if (markers.isEmpty()) { - Try.just(Unit) - } else { - executeRequest { - apiCall = roomAPI.sendReadMarker(params.roomId, markers) + if (readReceiptEventId != null + && !isEventRead(params.roomId, readReceiptEventId)) { + + if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) { + Timber.w("Can't set read receipt for local event ${params.fullyReadEventId}") + } else { + updateNotificationCountIfNecessary(params.roomId, readReceiptEventId) + markers[READ_RECEIPT] = readReceiptEventId } } + if (markers.isEmpty()) { + return + } + executeRequest { + apiCall = roomAPI.sendReadMarker(params.roomId, markers) + } } private fun updateNotificationCountIfNecessary(roomId: String, eventId: String) { - monarchy.tryTransactionAsync { realm -> - val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId)?.eventId == eventId + monarchy.writeAsync { realm -> + val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == eventId if (isLatestReceived) { val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: return@tryTransactionAsync + ?: return@writeAsync roomSummary.notificationCount = 0 roomSummary.highlightCount = 0 } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index 183f2a6c..cd31c978 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt @@ -43,7 +43,6 @@ import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEvent import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.CancelableWork -import im.vector.matrix.android.internal.util.tryTransactionAsync import im.vector.matrix.android.internal.worker.WorkerParamsFactory import timber.log.Timber import javax.inject.Inject @@ -81,28 +80,30 @@ internal class DefaultRelationService @Inject constructor(private val context: C reaction, myUserId ) - findReactionEventForUndoTask.configureWith(params) - .enableRetry() - .dispatchTo(object : MatrixCallback { - override fun onSuccess(data: FindReactionEventForUndoTask.Result) { - if (data.redactEventId == null) { - Timber.w("Cannot find reaction to undo (not yet synced?)") - //TODO? - } - data.redactEventId?.let { toRedact -> + val callback = object : MatrixCallback { + override fun onSuccess(data: FindReactionEventForUndoTask.Result) { + if (data.redactEventId == null) { + Timber.w("Cannot find reaction to undo (not yet synced?)") + //TODO? + } + data.redactEventId?.let { toRedact -> - val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null).also { - saveLocalEcho(it) - } - val redactWork = createRedactEventWork(redactEvent, toRedact, null) - - TimelineSendEventWorkCommon.postWork(context, roomId, redactWork) - - } + val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null).also { + saveLocalEcho(it) } - }) - .executeBy(taskExecutor) + val redactWork = createRedactEventWork(redactEvent, toRedact, null) + TimelineSendEventWorkCommon.postWork(context, roomId, redactWork) + + } + } + } + findReactionEventForUndoTask + .configureWith(params) { + this.retryCount = Int.MAX_VALUE + this.callback = callback + } + .executeBy(taskExecutor) } //TODO duplicate with send service? @@ -114,7 +115,7 @@ internal class DefaultRelationService @Inject constructor(private val context: C eventId, reason) val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - return TimelineSendEventWorkCommon.createWork(redactWorkData) + return TimelineSendEventWorkCommon.createWork(redactWorkData, true) } override fun editTextMessage(targetEventId: String, @@ -168,8 +169,10 @@ internal class DefaultRelationService @Inject constructor(private val context: C override fun fetchEditHistory(eventId: String, callback: MatrixCallback>) { val params = FetchEditHistoryTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), eventId) - fetchEditHistoryTask.configureWith(params) - .dispatchTo(callback) + fetchEditHistoryTask + .configureWith(params) { + this.callback = callback + } .executeBy(taskExecutor) } @@ -196,14 +199,13 @@ internal class DefaultRelationService @Inject constructor(private val context: C // Same parameter val params = EncryptEventWorker.Params(credentials.userId, roomId, event, keepKeys) val sendWorkData = WorkerParamsFactory.toData(params) - return TimelineSendEventWorkCommon.createWork(sendWorkData) + return TimelineSendEventWorkCommon.createWork(sendWorkData, true) } private fun createSendEventWork(event: Event): OneTimeWorkRequest { val sendContentWorkerParams = SendEventWorker.Params(credentials.userId, roomId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - val workRequest = TimelineSendEventWorkCommon.createWork(sendWorkData) - return workRequest + return TimelineSendEventWorkCommon.createWork(sendWorkData, true) } override fun getEventSummaryLive(eventId: String): LiveData { @@ -223,9 +225,9 @@ internal class DefaultRelationService @Inject constructor(private val context: C * the same transaction id is received (in unsigned data) */ private fun saveLocalEcho(event: Event) { - monarchy.tryTransactionAsync { realm -> + monarchy.writeAsync { realm -> val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() - ?: return@tryTransactionAsync + ?: return@writeAsync roomEntity.addSendingEvent(event) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FetchEditHistoryTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FetchEditHistoryTask.kt index 7afbe288..792df3b1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FetchEditHistoryTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FetchEditHistoryTask.kt @@ -15,7 +15,6 @@ */ package im.vector.matrix.android.internal.session.room.relation -import arrow.core.Try 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 @@ -39,16 +38,16 @@ internal class DefaultFetchEditHistoryTask @Inject constructor( private val roomAPI: RoomAPI ) : FetchEditHistoryTask { - override suspend fun execute(params: FetchEditHistoryTask.Params): Try> { - return executeRequest { + override suspend fun execute(params: FetchEditHistoryTask.Params): List { + val response = executeRequest { apiCall = roomAPI.getRelations(params.roomId, params.eventId, RelationType.REPLACE, if (params.isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE) - }.map { resp -> - val events = resp.chunks.toMutableList() - resp.originalEvent?.let { events.add(it) } - events } + + val events = response.chunks.toMutableList() + response.originalEvent?.let { events.add(it) } + return events } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt index 8841819e..f74a7ebb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt @@ -15,13 +15,11 @@ */ package im.vector.matrix.android.internal.session.room.relation -import arrow.core.Try import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.task.Task import io.realm.Realm import javax.inject.Inject @@ -44,14 +42,11 @@ internal interface FindReactionEventForUndoTask : Task { - return Try { - var eventId: String? = null - monarchy.doWithRealm { realm -> - eventId = getReactionToRedact(realm, params.reaction, params.eventId, params.myUserId)?.eventId - } - FindReactionEventForUndoTask.Result(eventId) + override suspend fun execute(params: FindReactionEventForUndoTask.Params): FindReactionEventForUndoTask.Result { + val eventId = Realm.getInstance(monarchy.realmConfiguration).use { realm -> + getReactionToRedact(realm, params.reaction, params.eventId, params.myUserId)?.eventId } + return FindReactionEventForUndoTask.Result(eventId) } private fun getReactionToRedact(realm: Realm, reaction: String, eventId: String, userId: String): EventEntity? { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt index 81a888ee..5df995d9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt @@ -66,18 +66,11 @@ internal class SendRelationWorker(context: Context, params: WorkerParameters) : val relatedEventId = relationContent.relatesTo?.eventId ?: return Result.failure() val relationType = (relationContent.relatesTo as? ReactionInfo)?.type ?: params.relationType ?: return Result.failure() - - val result = executeRequest { - apiCall = roomAPI.sendRelation( - roomId = params.roomId, - parent_id = relatedEventId, - relationType = relationType, - eventType = localEvent.type, - content = localEvent.content - ) - } - return result.fold({ - when (it) { + return try { + sendRelation(params.roomId, relationType, relatedEventId, localEvent) + Result.success() + } catch (exception: Throwable) { + when (exception) { is Failure.NetworkConnection -> Result.retry() else -> { //TODO mark as failed to send? @@ -85,7 +78,19 @@ internal class SendRelationWorker(context: Context, params: WorkerParameters) : Result.success() } } - }, { Result.success() }) + } + } + + private suspend fun sendRelation(roomId: String, relationType: String, relatedEventId: String, localEvent: Event) { + executeRequest { + apiCall = roomAPI.sendRelation( + roomId = roomId, + parent_id = relatedEventId, + relationType = relationType, + eventType = localEvent.type, + content = localEvent.content + ) + } } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/UpdateQuickReactionTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/UpdateQuickReactionTask.kt index 66e02ce9..fd081fbe 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/UpdateQuickReactionTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/UpdateQuickReactionTask.kt @@ -15,13 +15,11 @@ */ package im.vector.matrix.android.internal.session.room.relation -import arrow.core.Try import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.task.Task import io.realm.Realm import javax.inject.Inject @@ -44,14 +42,13 @@ internal interface UpdateQuickReactionTask : Task { - return Try { - var res: Pair?>? = null - monarchy.doWithRealm { realm -> - res = updateQuickReaction(realm, params.reaction, params.oppositeReaction, params.eventId, params.myUserId) - } - UpdateQuickReactionTask.Result(res?.first, res?.second ?: emptyList()) + + override suspend fun execute(params: UpdateQuickReactionTask.Params): UpdateQuickReactionTask.Result { + var res: Pair?>? = null + monarchy.doWithRealm { realm -> + res = updateQuickReaction(realm, params.reaction, params.oppositeReaction, params.eventId, params.myUserId) } + return UpdateQuickReactionTask.Result(res?.first, res?.second ?: emptyList()) } 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 9a94b05b..d822e949 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 @@ -17,25 +17,35 @@ package im.vector.matrix.android.internal.session.room.send import android.content.Context -import androidx.work.BackoffPolicy -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager +import androidx.work.* import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.crypto.CryptoService -import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.* +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.send.SendService +import im.vector.matrix.android.api.session.room.send.SendState +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.CancelableBag +import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates +import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.content.UploadContentWorker import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon import im.vector.matrix.android.internal.util.CancelableWork +import im.vector.matrix.android.internal.worker.AlwaysSuccessfulWorker import im.vector.matrix.android.internal.worker.WorkManagerUtil import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder import im.vector.matrix.android.internal.worker.WorkerParamsFactory +import im.vector.matrix.android.internal.worker.startChain import timber.log.Timber +import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -50,6 +60,7 @@ internal class DefaultSendService @Inject constructor(private val context: Conte private val monarchy: Monarchy) : SendService { + private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor() override fun sendTextMessage(text: String, msgType: String, autoMarkdown: Boolean): Cancelable { val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also { saveLocalEcho(it) @@ -70,12 +81,12 @@ internal class DefaultSendService @Inject constructor(private val context: Conte // Encrypted room handling return if (cryptoService.isRoomEncrypted(roomId)) { Timber.v("Send event in encrypted room") - val encryptWork = createEncryptEventWork(event) - val sendWork = createSendEventWork(event) + val encryptWork = createEncryptEventWork(event, true) + val sendWork = createSendEventWork(event, false) TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, sendWork) CancelableWork(context, encryptWork.id) } else { - val sendWork = createSendEventWork(event) + val sendWork = createSendEventWork(event, true) TimelineSendEventWorkCommon.postWork(context, roomId, sendWork) CancelableWork(context, sendWork.id) } @@ -94,28 +105,167 @@ internal class DefaultSendService @Inject constructor(private val context: Conte return CancelableWork(context, redactWork.id) } + override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? { + if (localEcho.root.isTextMessage() && localEcho.root.sendState.hasFailed()) { + return sendEvent(localEcho.root) + } + return null + + } + + override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? { + if (localEcho.root.isImageMessage() && localEcho.root.sendState.hasFailed()) { + //TODO this need a refactoring of attachement sending +// val clearContent = localEcho.root.getClearContent() +// val messageContent = clearContent?.toModel() ?: return null +// when (messageContent.type) { +// MessageType.MSGTYPE_IMAGE -> { +// val imageContent = clearContent.toModel() ?: return null +// val url = imageContent.url ?: return null +// if (url.startsWith("mxc://")) { +// //TODO +// } else { +// //The image has not yet been sent +// val attachmentData = ContentAttachmentData( +// size = imageContent.info!!.size.toLong(), +// mimeType = imageContent.info.mimeType!!, +// width = imageContent.info.width.toLong(), +// height = imageContent.info.height.toLong(), +// name = imageContent.body, +// path = imageContent.url, +// type = ContentAttachmentData.Type.IMAGE +// ) +// monarchy.runTransactionSync { +// EventEntity.where(it,eventId = localEcho.root.eventId ?: "").findFirst()?.let { +// it.sendState = SendState.UNSENT +// } +// } +// return internalSendMedia(localEcho.root,attachmentData) +// } +// } +// } + return null + } + return null + } + + override fun deleteFailedEcho(localEcho: TimelineEvent) { + monarchy.writeAsync { realm -> + TimelineEventEntity.where(realm, eventId = localEcho.root.eventId + ?: "").findFirst()?.let { + it.deleteFromRealm() + } + EventEntity.where(realm, eventId = localEcho.root.eventId + ?: "").findFirst()?.let { + it.deleteFromRealm() + } + } + } + + override fun clearSendingQueue() { + TimelineSendEventWorkCommon.cancelAllWorks(context, roomId) + WorkManager.getInstance(context).cancelUniqueWork(buildWorkName(UPLOAD_WORK)) + + // Replace the worker chains with a AlwaysSuccessfulWorker, to ensure the queues are well emptied + matrixOneTimeWorkRequestBuilder() + .build().let { + TimelineSendEventWorkCommon.postWork(context, roomId, it, ExistingWorkPolicy.REPLACE) + + //need to clear also image sending queue + WorkManager.getInstance(context) + .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it) + .enqueue() + } + + monarchy.writeAsync { realm -> + RoomEntity.where(realm, roomId).findFirst()?.let { room -> + room.sendingTimelineEvents.forEach { + it.root?.sendState = SendState.UNDELIVERED + } + } + } + + } + + override fun resendAllFailedMessages() { + monarchy.writeAsync { realm -> + TimelineEventEntity + .findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES) + .sortedBy { it.root?.originServerTs ?: 0 } + .forEach { timelineEventEntity -> + timelineEventEntity.root?.let { + val event = it.asDomain() + when (event.getClearType()) { + EventType.MESSAGE, + EventType.REDACTION, + EventType.REACTION -> { + val content = event.getClearContent().toModel() + if (content != null) { + when (content.type) { + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_NOTICE, + MessageType.MSGTYPE_LOCATION, + MessageType.MSGTYPE_TEXT -> { + it.sendState = SendState.UNSENT + sendEvent(event) + } + MessageType.MSGTYPE_FILE, + MessageType.MSGTYPE_VIDEO, + MessageType.MSGTYPE_IMAGE, + MessageType.MSGTYPE_AUDIO -> { + //need to resend the attachement + } + else -> { + Timber.e("Cannot resend message ${event.type} / ${content.type}") + } + + } + } else { + Timber.e("Unsupported message to resend ${event.type}") + } + } + else -> { + Timber.e("Unsupported message to resend ${event.type}") + } + } + } + } + } + } + override fun sendMedia(attachment: ContentAttachmentData): Cancelable { // Create an event with the media file path val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also { saveLocalEcho(it) } + return internalSendMedia(event, attachment) + } + + private fun internalSendMedia(localEcho: Event, attachment: ContentAttachmentData): CancelableWork { val isRoomEncrypted = cryptoService.isRoomEncrypted(roomId) - val uploadWork = createUploadMediaWork(event, attachment, isRoomEncrypted) - val sendWork = createSendEventWork(event) + val uploadWork = createUploadMediaWork(localEcho, attachment, isRoomEncrypted, startChain = true) + val sendWork = createSendEventWork(localEcho, false) if (isRoomEncrypted) { - val encryptWork = createEncryptEventWork(event) + val encryptWork = createEncryptEventWork(localEcho, false /*not start of chain, take input error*/) - WorkManager.getInstance(context) - .beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) + val op: Operation = WorkManager.getInstance(context) + .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) .then(encryptWork) .then(sendWork) .enqueue() + op.result.addListener(Runnable { + if (op.result.isCancelled) { + Timber.e("CHAIN WAS CANCELLED") + } else if (op.state.value is Operation.State.FAILURE) { + Timber.e("CHAIN DID FAIL") + } + }, workerFutureListenerExecutor) } else { WorkManager.getInstance(context) - .beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) + .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) .then(sendWork) .enqueue() } @@ -127,11 +277,11 @@ internal class DefaultSendService @Inject constructor(private val context: Conte localEchoEventFactory.saveLocalEcho(monarchy, event) } - private fun buildWorkIdentifier(identifier: String): String { + private fun buildWorkName(identifier: String): String { return "${roomId}_$identifier" } - private fun createEncryptEventWork(event: Event): OneTimeWorkRequest { + private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { // Same parameter val params = EncryptEventWorker.Params(credentials.userId, roomId, event) val sendWorkData = WorkerParamsFactory.toData(params) @@ -139,15 +289,16 @@ internal class DefaultSendService @Inject constructor(private val context: Conte return matrixOneTimeWorkRequestBuilder() .setConstraints(WorkManagerUtil.workConstraints) .setInputData(sendWorkData) + .startChain(startChain) .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) .build() } - private fun createSendEventWork(event: Event): OneTimeWorkRequest { + private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { val sendContentWorkerParams = SendEventWorker.Params(credentials.userId, roomId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - return TimelineSendEventWorkCommon.createWork(sendWorkData) + return TimelineSendEventWorkCommon.createWork(sendWorkData, startChain) } private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest { @@ -156,18 +307,23 @@ internal class DefaultSendService @Inject constructor(private val context: Conte } val sendContentWorkerParams = RedactEventWorker.Params(credentials.userId, redactEvent.eventId!!, roomId, event.eventId, reason) val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - return TimelineSendEventWorkCommon.createWork(redactWorkData) + return TimelineSendEventWorkCommon.createWork(redactWorkData, true) } - private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData, isRoomEncrypted: Boolean): OneTimeWorkRequest { + private fun createUploadMediaWork(event: Event, + attachment: ContentAttachmentData, + isRoomEncrypted: Boolean, + startChain: Boolean): OneTimeWorkRequest { val uploadMediaWorkerParams = UploadContentWorker.Params(credentials.userId, roomId, event, attachment, isRoomEncrypted) val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) return matrixOneTimeWorkRequestBuilder() .setConstraints(WorkManagerUtil.workConstraints) + .startChain(startChain) .setInputData(uploadWorkData) .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) .build() } } + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt index 118fa7cc..031ceb16 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt @@ -17,23 +17,23 @@ package im.vector.matrix.android.internal.session.room.send import android.content.Context -import androidx.work.Worker +import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass -import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult +import im.vector.matrix.android.internal.util.awaitCallback import im.vector.matrix.android.internal.worker.SessionWorkerParams import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.getSessionComponent -import java.util.concurrent.CountDownLatch +import timber.log.Timber import javax.inject.Inject internal class EncryptEventWorker(context: Context, params: WorkerParameters) - : Worker(context, params) { + : CoroutineWorker(context, params) { @JsonClass(generateAdapter = true) internal data class Params( @@ -48,11 +48,14 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) @Inject lateinit var crypto: CryptoService @Inject lateinit var localEchoUpdater: LocalEchoUpdater - override fun doWork(): Result { - + override suspend fun doWork(): Result { + Timber.v("Start Encrypt work") val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.success() + ?: return Result.success().also { + Timber.v("Work cancelled due to input error from parent") + } + Timber.v("Start Encrypt work for event ${params.event.eventId}") if (params.lastFailureMessage != null) { // Transmit the error return Result.success(inputData) @@ -67,44 +70,30 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) } localEchoUpdater.updateSendState(localEvent.eventId, SendState.ENCRYPTING) - // TODO Better async handling - val latch = CountDownLatch(1) - - var result: MXEncryptEventContentResult? = null - var error: Throwable? = null val localMutableContent = HashMap(localEvent.content) params.keepKeys?.forEach { localMutableContent.remove(it) } + var error: Throwable? = null + var result: MXEncryptEventContentResult? = null try { - crypto.encryptEventContent(localMutableContent, localEvent.type, params.roomId, object : MatrixCallback { - override fun onSuccess(data: MXEncryptEventContentResult) { - result = data - latch.countDown() - } - - override fun onFailure(failure: Throwable) { - error = failure - latch.countDown() - } - }) - } catch (e: Throwable) { - error = e - latch.countDown() + result = awaitCallback { + crypto.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it) + } + } catch (throwable: Throwable) { + error = throwable } - latch.await() - if (result != null) { - var modifiedContent = HashMap(result?.eventContent) + val modifiedContent = HashMap(result.eventContent) params.keepKeys?.forEach { toKeep -> localEvent.content?.get(toKeep)?.let { //put it back in the encrypted thing modifiedContent[toKeep] = it } } - val safeResult = result!!.copy(eventContent = modifiedContent) + val safeResult = result.copy(eventContent = modifiedContent) val encryptedEvent = localEvent.copy( type = safeResult.eventType, content = safeResult.eventContent @@ -118,7 +107,8 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) } localEchoUpdater.updateSendState(localEvent.eventId, sendState) //always return success, or the chain will be stuck for ever! - val nextWorkerParams = SendEventWorker.Params(params.userId, params.roomId, localEvent, error?.localizedMessage ?: "Error") + val nextWorkerParams = SendEventWorker.Params(params.userId, params.roomId, localEvent, error?.localizedMessage + ?: "Error") return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index c6b68647..6d97da84 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -36,7 +36,6 @@ import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.content.ThumbnailExtractor import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.util.StringProvider -import im.vector.matrix.android.internal.util.tryTransactionAsync import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer import java.util.* @@ -306,12 +305,12 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials private fun buildReplyFallback(body: TextContent, originalSenderId: String?, newBodyText: String): String { val lines = body.text.split("\n") - val replyFallback = StringBuffer("><$originalSenderId>") + val replyFallback = StringBuffer("> <$originalSenderId>") lines.forEachIndexed { index, s -> if (index == 0) { replyFallback.append(" $s") } else { - replyFallback.append("\n>$s") + replyFallback.append("\n> $s") } } replyFallback.append("\n\n").append(newBodyText) @@ -380,9 +379,9 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials fun saveLocalEcho(monarchy: Monarchy, event: Event) { if (event.roomId == null) throw IllegalStateException("Your event should have a roomId") - monarchy.tryTransactionAsync { realm -> + monarchy.writeAsync { realm -> val roomEntity = RoomEntity.where(realm, roomId = event.roomId).findFirst() - ?: return@tryTransactionAsync + ?: return@writeAsync roomEntity.addSendingEvent(event) roomSummaryUpdater.update(realm, event.roomId) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt index 7f22fb20..c4242239 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt @@ -20,16 +20,22 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.util.tryTransactionAsync +import im.vector.matrix.android.internal.util.awaitTransaction +import timber.log.Timber import javax.inject.Inject internal class LocalEchoUpdater @Inject constructor(private val monarchy: Monarchy) { - fun updateSendState(eventId: String, sendState: SendState) { - monarchy.tryTransactionAsync { realm -> + suspend fun updateSendState(eventId: String, sendState: SendState) { + Timber.v("Update local state of $eventId to ${sendState.name}") + monarchy.awaitTransaction { realm -> val sendingEventEntity = EventEntity.where(realm, eventId).findFirst() if (sendingEventEntity != null) { - sendingEventEntity.sendState = sendState + if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) { + //If already synced, do not put as sent + } else { + sendingEventEntity.sendState = sendState + } } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/NoMerger.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/NoMerger.kt new file mode 100644 index 00000000..c41c4bc0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/NoMerger.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.session.room.send + +import androidx.work.Data +import androidx.work.InputMerger + +/** + * InputMerger which takes only the first input, to ensure an appended work will only have the specified parameters + */ +internal class NoMerger : InputMerger() { + override fun merge(inputs: MutableList): Data { + return inputs.first() + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt index 5acc16e1..bac71cf7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt @@ -54,28 +54,32 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C sessionComponent.inject(this) val eventId = params.eventId - val result = executeRequest { - apiCall = roomAPI.redactEvent( - params.txID, - params.roomId, - eventId, - if (params.reason == null) emptyMap() else mapOf("reason" to params.reason) - ) - } - return result.fold({ - when (it) { - is Failure.NetworkConnection -> Result.retry() - else -> { - //TODO mark as failed to send? - //always return success, or the chain will be stuck for ever! - Result.success(WorkerParamsFactory.toData(params.copy( - lastFailureMessage = it.localizedMessage - ))) - } + return runCatching { + executeRequest { + apiCall = roomAPI.redactEvent( + params.txID, + params.roomId, + eventId, + if (params.reason == null) emptyMap() else mapOf("reason" to params.reason) + ) } - }, { - Result.success() - }) + }.fold( + { + Result.success() + }, + { + when (it) { + is Failure.NetworkConnection -> Result.retry() + else -> { + //TODO mark as failed to send? + //always return success, or the chain will be stuck for ever! + Result.success(WorkerParamsFactory.toData(params.copy( + lastFailureMessage = it.localizedMessage + ))) + } + } + } + ) } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt index 05cd56e3..442c6084 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt @@ -21,6 +21,8 @@ import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass 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.Content import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.network.executeRequest @@ -59,30 +61,39 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam if (params.lastFailureMessage != null) { localEchoUpdater.updateSendState(event.eventId, SendState.UNDELIVERED) - // Transmit the error return Result.success(inputData) } + return try { + sendEvent(event.eventId, event.type, event.content, params.roomId) + Result.success() + } catch (exception: Throwable) { + if (exception.shouldBeRetried()) { + Result.retry() + } else { + localEchoUpdater.updateSendState(event.eventId, SendState.UNDELIVERED) + //always return success, or the chain will be stuck for ever! + Result.success() + } + } + } - localEchoUpdater.updateSendState(event.eventId, SendState.SENDING) - val result = executeRequest { + private fun Throwable.shouldBeRetried(): Boolean { + return this is Failure.NetworkConnection + || (this is Failure.ServerError && this.error.code == MatrixError.LIMIT_EXCEEDED) + } + + private suspend fun sendEvent(eventId: String, eventType: String, content: Content?, roomId: String) { + localEchoUpdater.updateSendState(eventId, SendState.SENDING) + executeRequest { apiCall = roomAPI.send( - event.eventId, - params.roomId, - event.type, - event.content + eventId, + roomId, + eventType, + content ) } - return result.fold({ - when (it) { - is Failure.NetworkConnection -> Result.retry() - else -> { - localEchoUpdater.updateSendState(event.eventId, SendState.UNDELIVERED) - //always return success, or the chain will be stuck for ever! - Result.success() - } - } - }, { Result.success() }) + localEchoUpdater.updateSendState(eventId, SendState.SENT) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt index e51f9b46..60999b61 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt @@ -17,16 +17,32 @@ package im.vector.matrix.android.internal.session.room.state import im.vector.matrix.android.api.MatrixCallback +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.room.state.StateService +import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.query.prev +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith +import io.realm.Realm +import io.realm.RealmConfiguration import javax.inject.Inject internal class DefaultStateService @Inject constructor(private val roomId: String, + @SessionDatabase + private val realmConfiguration: RealmConfiguration, private val taskExecutor: TaskExecutor, private val sendStateTask: SendStateTask) : StateService { + override fun getStateEvent(eventType: String): Event? { + return Realm.getInstance(realmConfiguration).use { realm -> + EventEntity.where(realm, roomId, eventType).prev()?.asDomain() + } + } + override fun updateTopic(topic: String, callback: MatrixCallback) { val params = SendStateTask.Params(roomId, EventType.STATE_ROOM_TOPIC, @@ -35,8 +51,12 @@ internal class DefaultStateService @Inject constructor(private val roomId: Strin )) - sendStateTask.configureWith(params) - .dispatchTo(callback) + sendStateTask + .configureWith(params) { + this.callback = callback + } .executeBy(taskExecutor) } + + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/SendStateTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/SendStateTask.kt index 085340e6..39d606f5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/SendStateTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/SendStateTask.kt @@ -16,9 +16,7 @@ package im.vector.matrix.android.internal.session.room.state -import arrow.core.Try import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -32,7 +30,7 @@ internal interface SendStateTask : Task { } internal class DefaultSendStateTask @Inject constructor(private val roomAPI: RoomAPI) : SendStateTask { - override suspend fun execute(params: SendStateTask.Params): Try { + override suspend fun execute(params: SendStateTask.Params) { return executeRequest { apiCall = roomAPI.sendStateEvent(params.roomId, params.eventType, params.body) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt index a47bab62..e4f48d35 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt @@ -16,9 +16,7 @@ package im.vector.matrix.android.internal.session.room.timeline -import arrow.core.Try import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope 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 @@ -38,13 +36,12 @@ internal class DefaultGetContextOfEventTask @Inject constructor(private val room private val tokenChunkEventPersistor: TokenChunkEventPersistor ) : GetContextOfEventTask { - override suspend fun execute(params: GetContextOfEventTask.Params): Try { + override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result { val filter = filterRepository.getRoomFilter() - return executeRequest { + val response = executeRequest { apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter) - }.flatMap { response -> - tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS) } + return tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS) } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt index bc8a93b6..57efcdae 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt @@ -16,9 +16,7 @@ package im.vector.matrix.android.internal.session.room.timeline -import arrow.core.Try import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope 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 @@ -41,14 +39,12 @@ internal class DefaultPaginationTask @Inject constructor(private val roomAPI: Ro private val tokenChunkEventPersistor: TokenChunkEventPersistor ) : PaginationTask { - override suspend fun execute(params: PaginationTask.Params): Try { + override suspend fun execute(params: PaginationTask.Params): TokenChunkEventPersistor.Result { val filter = filterRepository.getRoomFilter() - return executeRequest { + val chunk = executeRequest { apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter) - }.flatMap { chunk -> - tokenChunkEventPersistor - .insertInDb(chunk, params.roomId, params.direction) } + return tokenChunkEventPersistor.insertInDb(chunk, params.roomId, params.direction) } } \ No newline at end of file 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 e1a8bdd7..10f4874f 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 @@ -19,16 +19,15 @@ package im.vector.matrix.android.internal.session.room.timeline import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.events.model.EventType +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.util.CancelableBag import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.query.findIncludingEvent -import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom -import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.database.query.whereInRoom +import im.vector.matrix.android.internal.database.query.* +import im.vector.matrix.android.internal.task.TaskConstraints import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.Debouncer @@ -133,7 +132,7 @@ internal class DefaultTimeline( builtEventsIdMap[eventId]?.let { builtIndex -> //Update the relation of existing event builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = eventEntity.asDomain() + builtEvents[builtIndex] = eventEntity.asDomain() hasChanged = true } } @@ -206,6 +205,17 @@ internal class DefaultTimeline( } } + override fun pendingEventCount(): Int { + return Realm.getInstance(realmConfiguration).use { + RoomEntity.where(it, roomId).findFirst()?.sendingTimelineEvents?.count() ?: 0 + } + } + + override fun failedToDeliverEventCount(): Int { + return Realm.getInstance(realmConfiguration).use { + TimelineEventEntity.findAllInRoomWithSendStates(it, roomId, SendState.HAS_FAILED_STATES).count() + } + } override fun start() { if (isStarted.compareAndSet(false, true)) { @@ -260,30 +270,32 @@ internal class DefaultTimeline( // Private methods ***************************************************************************** private fun hasMoreInCache(direction: Timeline.Direction): Boolean { - val localRealm = Realm.getInstance(realmConfiguration) - val timelineEventEntity = buildEventQuery(localRealm).findFirst(direction) ?: return false - val hasMoreInCache = if (direction == Timeline.Direction.FORWARDS) { - val firstEvent = builtEvents.firstOrNull() ?: return true - firstEvent.displayIndex < timelineEventEntity.root!!.displayIndex - } else { - val lastEvent = builtEvents.lastOrNull() ?: return true - lastEvent.displayIndex > timelineEventEntity.root!!.displayIndex + return Realm.getInstance(realmConfiguration).use { localRealm -> + val timelineEventEntity = buildEventQuery(localRealm).findFirst(direction) + ?: return false + if (direction == Timeline.Direction.FORWARDS) { + if (findCurrentChunk(localRealm)?.isLastForward == true) { + return false + } + val firstEvent = builtEvents.firstOrNull() ?: return true + firstEvent.displayIndex < timelineEventEntity.root!!.displayIndex + } else { + val lastEvent = builtEvents.lastOrNull() ?: return true + lastEvent.displayIndex > timelineEventEntity.root!!.displayIndex + } } - localRealm.close() - return hasMoreInCache } private fun hasReachedEnd(direction: Timeline.Direction): Boolean { - val localRealm = Realm.getInstance(realmConfiguration) - val currentChunk = findCurrentChunk(localRealm) ?: return false - val hasReachedEnd = if (direction == Timeline.Direction.FORWARDS) { - currentChunk.isLastForward - } else { - val eventEntity = buildEventQuery(localRealm).findFirst(direction) - currentChunk.isLastBackward || eventEntity?.root?.type == EventType.STATE_ROOM_CREATE + return Realm.getInstance(realmConfiguration).use { localRealm -> + val currentChunk = findCurrentChunk(localRealm) ?: return false + if (direction == Timeline.Direction.FORWARDS) { + currentChunk.isLastForward + } else { + val eventEntity = buildEventQuery(localRealm).findFirst(direction) + currentChunk.isLastBackward || eventEntity?.root?.type == EventType.STATE_ROOM_CREATE + } } - localRealm.close() - return hasReachedEnd } @@ -388,24 +400,27 @@ internal class DefaultTimeline( limit = limit) Timber.v("Should fetch $limit items $direction") - cancelableBag += paginationTask.configureWith(params) - .enableRetry() - .dispatchTo(object : MatrixCallback { - override fun onSuccess(data: TokenChunkEventPersistor.Result) { - if (data == TokenChunkEventPersistor.Result.SUCCESS) { - Timber.v("Success fetching $limit items $direction from pagination request") - } else { - // Database won't be updated, so we force pagination request - BACKGROUND_HANDLER.post { - executePaginationTask(direction, limit) + cancelableBag += paginationTask + .configureWith(params) { + this.retryCount = Int.MAX_VALUE + this.constraints = TaskConstraints(connectedToNetwork = true) + this.callback = object : MatrixCallback { + override fun onSuccess(data: TokenChunkEventPersistor.Result) { + if (data == TokenChunkEventPersistor.Result.SUCCESS) { + Timber.v("Success fetching $limit items $direction from pagination request") + } else { + // Database won't be updated, so we force pagination request + BACKGROUND_HANDLER.post { + executePaginationTask(direction, limit) + } } } - } - override fun onFailure(failure: Throwable) { - Timber.v("Failure fetching $limit items $direction from pagination request") + override fun onFailure(failure: Throwable) { + Timber.v("Failure fetching $limit items $direction from pagination request") + } } - }) + } .executeBy(taskExecutor) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetEventTask.kt index 5ce58e02..5fdee1b5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetEventTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetEventTask.kt @@ -16,12 +16,10 @@ package im.vector.matrix.android.internal.session.room.timeline -import arrow.core.Try import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.task.Task import javax.inject.Inject internal class GetEventTask @Inject constructor(private val roomAPI: RoomAPI @@ -32,7 +30,7 @@ internal class GetEventTask @Inject constructor(private val roomAPI: RoomAPI val eventId: String ) - override suspend fun execute(params: Params): Try { + override suspend fun execute(params: Params): Event { return executeRequest { apiCall = roomAPI.getEvent(params.roomId, params.eventId) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt index 53906fdd..575c0662 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt @@ -19,6 +19,7 @@ import android.content.Context import androidx.work.* import im.vector.matrix.android.internal.worker.WorkManagerUtil import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder +import im.vector.matrix.android.internal.worker.startChain import java.util.concurrent.TimeUnit @@ -41,7 +42,7 @@ internal object TimelineSendEventWorkCommon { else -> { val firstWork = workRequests.first() var continuation = WorkManager.getInstance(context) - .beginUniqueWork(buildWorkIdentifier(roomId), ExistingWorkPolicy.APPEND, firstWork) + .beginUniqueWork(buildWorkName(roomId), ExistingWorkPolicy.APPEND, firstWork) for (i in 1 until workRequests.size) { val workRequest = workRequests[i] continuation = continuation.then(workRequest) @@ -51,21 +52,26 @@ internal object TimelineSendEventWorkCommon { } } - fun postWork(context: Context, roomId: String, workRequest: OneTimeWorkRequest) { + fun postWork(context: Context, roomId: String, workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND) { WorkManager.getInstance(context) - .beginUniqueWork(buildWorkIdentifier(roomId), ExistingWorkPolicy.APPEND, workRequest) + .beginUniqueWork(buildWorkName(roomId), policy, workRequest) .enqueue() } - inline fun createWork(data: Data): OneTimeWorkRequest { + inline fun createWork(data: Data, startChain: Boolean): OneTimeWorkRequest { return matrixOneTimeWorkRequestBuilder() .setConstraints(WorkManagerUtil.workConstraints) + .startChain(startChain) .setInputData(data) .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) .build() } - private fun buildWorkIdentifier(roomId: String): String { + private fun buildWorkName(roomId: String): String { return "${roomId}_$SEND_WORK" } + + fun cancelAllWorks(context: Context, roomId: String) { + WorkManager.getInstance(context).cancelUniqueWork(buildWorkName(roomId)) + } } \ No newline at end of file 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 fb8b6271..af845040 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 @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.session.room.timeline -import arrow.core.Try import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.database.helper.* import im.vector.matrix.android.internal.database.model.ChunkEntity @@ -26,7 +25,7 @@ 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.where import im.vector.matrix.android.internal.session.user.UserEntityFactory -import im.vector.matrix.android.internal.util.tryTransactionSync +import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.kotlin.createObject import timber.log.Timber import javax.inject.Inject @@ -104,12 +103,12 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy SUCCESS } - fun insertInDb(receivedChunk: TokenChunkEvent, - roomId: String, - direction: PaginationDirection): Try { + suspend fun insertInDb(receivedChunk: TokenChunkEvent, + roomId: String, + direction: PaginationDirection): Result { - return monarchy - .tryTransactionSync { realm -> + monarchy + .awaitTransaction { realm -> Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction") val roomEntity = RoomEntity.where(realm, roomId).findFirst() @@ -127,7 +126,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy if (ChunkEntity.find(realm, roomId, nextToken = nextToken) != null || ChunkEntity.find(realm, roomId, prevToken = prevToken) != null) { Timber.v("Already inserted - SKIP") - return@tryTransactionSync + return@awaitTransaction } val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken) @@ -181,13 +180,11 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy currentChunk.updateSenderDataFor(eventIds) } } - .map { - if (receivedChunk.events.isEmpty() && receivedChunk.stateEvents.isEmpty() && receivedChunk.start != receivedChunk.end) { - Result.SHOULD_FETCH_MORE - } else { - Result.SUCCESS - } - } + return if (receivedChunk.events.isEmpty() && receivedChunk.stateEvents.isEmpty() && receivedChunk.start != receivedChunk.end) { + Result.SHOULD_FETCH_MORE + } else { + Result.SUCCESS + } } private fun handleMerge(roomEntity: RoomEntity, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt new file mode 100644 index 00000000..d71b32ef --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tombstone/RoomTombstoneEventLiveObserver.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.tombstone + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.VersioningState +import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent +import im.vector.matrix.android.internal.database.RealmLiveEntityObserver +import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity +import im.vector.matrix.android.internal.database.query.types +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.SessionDatabase +import io.realm.OrderedCollectionChangeSet +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.RealmResults +import javax.inject.Inject + +internal class RoomTombstoneEventLiveObserver @Inject constructor(@SessionDatabase + realmConfiguration: RealmConfiguration) + : RealmLiveEntityObserver(realmConfiguration) { + + override val query = Monarchy.Query { + EventEntity.types(it, listOf(EventType.STATE_ROOM_TOMBSTONE)) + } + + override fun onChange(results: RealmResults, changeSet: OrderedCollectionChangeSet) { + changeSet.insertions + .asSequence() + .mapNotNull { + results[it]?.asDomain() + } + .toList() + .also { + handleRoomTombstoneEvents(it) + } + } + + private fun handleRoomTombstoneEvents(tombstoneEvents: List) = Realm.getInstance(realmConfiguration).use { + it.executeTransactionAsync { realm -> + for (event in tombstoneEvents) { + if (event.roomId == null) continue + val createRoomContent = event.getClearContent().toModel() + if (createRoomContent?.replacementRoom == null) continue + + val predecessorRoomSummary = RoomSummaryEntity.where(realm, event.roomId).findFirst() + ?: RoomSummaryEntity(event.roomId) + if (predecessorRoomSummary.versioningState == VersioningState.NONE) { + predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_NOT_JOINED + } + realm.insertOrUpdate(predecessorRoomSummary) + + } + } + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt index fff75d14..2463a5ad 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt @@ -19,7 +19,7 @@ package im.vector.matrix.android.internal.session.signout import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.signout.SignOutService import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.task.toConfigurableTask +import im.vector.matrix.android.internal.task.configureWith import javax.inject.Inject internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask, @@ -27,8 +27,9 @@ internal class DefaultSignOutService @Inject constructor(private val signOutTask override fun signOut(callback: MatrixCallback) { signOutTask - .toConfigurableTask() - .dispatchTo(callback) + .configureWith { + this.callback = callback + } .executeBy(taskExecutor) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt index 903763e4..6f4441b1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.session.signout -import arrow.core.Try import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.auth.SessionParamsStore @@ -31,15 +30,11 @@ internal class DefaultSignOutTask @Inject constructor(private val credentials: C private val sessionManager: SessionManager, private val sessionParamsStore: SessionParamsStore) : SignOutTask { - override suspend fun execute(params: Unit): Try { - return executeRequest { + override suspend fun execute(params: Unit) { + executeRequest { apiCall = signOutAPI.signOut() - }.flatMap { - sessionParamsStore.delete(credentials.userId) - }.flatMap { - Try { - sessionManager.releaseSession(credentials.userId) - } } + sessionParamsStore.delete(credentials.userId) + sessionManager.releaseSession(credentials.userId) } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt index cb102d5d..013fc3ca 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt @@ -27,7 +27,6 @@ import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService -import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.sync.model.SyncResponse import im.vector.matrix.android.internal.session.sync.model.ToDeviceSyncResponse import timber.log.Timber diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt index 930f20f2..9ada6e71 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.session.sync import im.vector.matrix.android.internal.database.model.ReadReceiptEntity -import im.vector.matrix.android.internal.session.SessionScope import io.realm.Realm import timber.log.Timber import javax.inject.Inject 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 215321bd..9da3db76 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 @@ -28,7 +28,6 @@ import im.vector.matrix.android.internal.database.helper.* import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.database.model.UserEntity 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.where @@ -146,8 +145,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch roomEntity, roomSync.timeline.events, roomSync.timeline.prevToken, - roomSync.timeline.limited, - 0 + roomSync.timeline.limited ) roomEntity.addOrUpdate(chunkEntity) } @@ -195,15 +193,18 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch roomEntity: RoomEntity, eventList: List, prevToken: String? = null, - isLimited: Boolean = true, - stateIndexOffset: Int = 0): ChunkEntity { + isLimited: Boolean = true): ChunkEntity { val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId) + var stateIndexOffset = 0 val chunkEntity = if (!isLimited && lastChunk != null) { lastChunk } else { realm.createObject().apply { this.prevToken = prevToken } } + if (isLimited && lastChunk != null) { + stateIndexOffset = lastChunk.lastStateIndex(PaginationDirection.FORWARDS) + } lastChunk?.isLastForward = false chunkEntity.isLastForward = true diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomTagHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomTagHandler.kt index 49038eed..a3c00e43 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomTagHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomTagHandler.kt @@ -20,7 +20,6 @@ import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomTagEntity import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.session.SessionScope import io.realm.Realm import java.util.* import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt index d680a318..fafa758c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt @@ -88,9 +88,7 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl measureTimeMillis { reportSubtask(reporter, R.string.initial_sync_start_importing_account_data, 100, 0.1f) { Timber.v("Handle accountData") - if (syncResponse.accountData != null) { - userAccountDataSyncHandler.handle(syncResponse.accountData) - } + userAccountDataSyncHandler.handle(syncResponse.accountData, syncResponse.rooms?.invite) } }.also { Timber.v("Finish handling accountData in $it ms") @@ -98,7 +96,6 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl Timber.v("On sync completed") cryptoSyncHandler.onSyncCompleted(syncResponse) - } Timber.v("Finish handling sync in $measure ms") syncResponse diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt index 84490404..ea4efa47 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt @@ -16,9 +16,6 @@ package im.vector.matrix.android.internal.session.sync -import arrow.core.Try -import arrow.core.failure -import arrow.core.recoverWith import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R import im.vector.matrix.android.api.auth.data.Credentials @@ -30,7 +27,6 @@ import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressServi import im.vector.matrix.android.internal.session.filter.FilterRepository import im.vector.matrix.android.internal.session.sync.model.SyncResponse import im.vector.matrix.android.internal.task.Task -import im.vector.matrix.android.internal.util.tryTransactionAsync import javax.inject.Inject internal interface SyncTask : Task { @@ -50,7 +46,7 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI, ) : SyncTask { - override suspend fun execute(params: SyncTask.Params): Try { + override suspend fun execute(params: SyncTask.Params) { val requestParams = HashMap() var timeout = 0L val token = syncTokenStore.getLastToken() @@ -66,27 +62,22 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI, initialSyncProgressService.endAll() initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100) } - return executeRequest { - apiCall = syncAPI.sync(requestParams) - }.recoverWith { throwable -> + val syncResponse = try { + executeRequest { + apiCall = syncAPI.sync(requestParams) + } + } catch (throwable: Throwable) { // Intercept 401 if (throwable is Failure.ServerError && throwable.error.code == MatrixError.UNKNOWN_TOKEN) { sessionParamsStore.delete(credentials.userId) } - - // Transmit the throwable - throwable.failure() - }.flatMap { syncResponse -> - syncResponseHandler.handleResponse(syncResponse, token, false).also { - if (isInitialSync) { - monarchy.tryTransactionAsync { - initialSyncProgressService.endAll() - } - } - } - }.map { - syncTokenStore.saveToken(it.nextBatch) + throw throwable + } + syncResponseHandler.handleResponse(syncResponse, token, false) + syncTokenStore.saveToken(syncResponse.nextBatch) + if (isInitialSync) { + initialSyncProgressService.endAll() } } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt index 9c876049..6ea46931 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt @@ -17,41 +17,92 @@ package im.vector.matrix.android.internal.session.sync import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.RoomSummaryEntity -import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields +import im.vector.matrix.android.internal.database.query.getDirectRooms import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.room.membership.RoomMembers +import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync import im.vector.matrix.android.internal.session.sync.model.UserAccountDataDirectMessages import im.vector.matrix.android.internal.session.sync.model.UserAccountDataSync +import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper +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.configureWith +import io.realm.Realm +import timber.log.Timber import javax.inject.Inject -internal class UserAccountDataSyncHandler @Inject constructor(private val monarchy: Monarchy) { +internal class UserAccountDataSyncHandler @Inject constructor(private val monarchy: Monarchy, + private val credentials: Credentials, + private val directChatsHelper: DirectChatsHelper, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val taskExecutor: TaskExecutor) { - fun handle(accountData: UserAccountDataSync) { - accountData.list.forEach { + fun handle(accountData: UserAccountDataSync?, invites: Map?) { + accountData?.list?.forEach { when (it) { is UserAccountDataDirectMessages -> handleDirectChatRooms(it) else -> return@forEach } } + monarchy.doWithRealm { realm -> + synchronizeWithServerIfNeeded(realm, invites) + } } private fun handleDirectChatRooms(directMessages: UserAccountDataDirectMessages) { - val newDirectRoomIds = directMessages.content.values.flatten() monarchy.runTransactionSync { realm -> - - val oldDirectRooms = RoomSummaryEntity.where(realm) - .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) - .findAll() - oldDirectRooms.forEach { it.isDirect = false } - - newDirectRoomIds.forEach { roomId -> - val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() - if (roomSummaryEntity != null) { - roomSummaryEntity.isDirect = true - realm.insertOrUpdate(roomSummaryEntity) + val oldDirectRooms = RoomSummaryEntity.getDirectRooms(realm) + oldDirectRooms.forEach { + it.isDirect = false + it.directUserId = null + } + directMessages.content.forEach { + val userId = it.key + it.value.forEach { roomId -> + val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() + if (roomSummaryEntity != null) { + roomSummaryEntity.isDirect = true + roomSummaryEntity.directUserId = userId + realm.insertOrUpdate(roomSummaryEntity) + } } } } } + + // If we get some direct chat invites, we synchronize the user account data including those. + private fun synchronizeWithServerIfNeeded(realm: Realm, invites: Map?) { + if (invites.isNullOrEmpty()) return + val directChats = directChatsHelper.getLocalUserAccount() + var hasUpdate = false + invites.forEach { (roomId, _) -> + val myUserStateEvent = RoomMembers(realm, roomId).getStateEvent(credentials.userId) + val inviterId = myUserStateEvent?.sender + val myUserRoomMember: RoomMember? = myUserStateEvent?.let { it.asDomain().content?.toModel() } + val isDirect = myUserRoomMember?.isDirect + if (inviterId != null && inviterId != credentials.userId && isDirect == true) { + directChats + .getOrPut(inviterId, { arrayListOf() }) + .apply { + if (contains(roomId)) { + Timber.v("Direct chats already include room $roomId with user $inviterId") + } else { + add(roomId) + hasUpdate = true + } + } + } + } + if (hasUpdate) { + val updateUserAccountParams = UpdateUserAccountDataTask.DirectChatParams( + directMessages = directChats + ) + updateUserAccountDataTask.configureWith(updateUserAccountParams).executeBy(taskExecutor) + } + } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt index 1af5688b..148e25b3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt @@ -104,58 +104,57 @@ open class SyncService : Service() { } else { Timber.v("Execute sync request with timeout 0") val params = SyncTask.Params(TIME_OUT) - cancelableTask = syncTask.configureWith(params) - .callbackOn(TaskThread.SYNC) - .executeOn(TaskThread.SYNC) - .dispatchTo(object : MatrixCallback { - override fun onSuccess(data: Unit) { - cancelableTask = null - if (!once) { - timer.schedule(object : TimerTask() { - override fun run() { - doSync() - } - }, NEXT_BATCH_DELAY) - } else { - //stop - stopMe() + cancelableTask = syncTask + .configureWith(params) { + callbackThread = TaskThread.SYNC + executionThread = TaskThread.SYNC + callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + cancelableTask = null + if (!once) { + timer.schedule(object : TimerTask() { + override fun run() { + doSync() + } + }, NEXT_BATCH_DELAY) + } else { + //stop + stopMe() + } + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure) + cancelableTask = null + if (failure is Failure.NetworkConnection + && failure.cause is SocketTimeoutException) { + // Timeout are not critical + timer.schedule(object : TimerTask() { + override fun run() { + doSync() + } + }, 5_000L) + } + + if (failure !is Failure.NetworkConnection + || failure.cause is JsonEncodingException) { + // Wait 10s before retrying + timer.schedule(object : TimerTask() { + override fun run() { + doSync() + } + }, 5_000L) + } + + if (failure is Failure.ServerError + && (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) { + // No token or invalid token, stop the thread + stopSelf() + } } } - - override fun onFailure(failure: Throwable) { - Timber.e(failure) - cancelableTask = null - if (failure is Failure.NetworkConnection - && failure.cause is SocketTimeoutException) { - // Timeout are not critical - timer.schedule(object : TimerTask() { - override fun run() { - doSync() - } - }, 5_000L) - } - - if (failure !is Failure.NetworkConnection - || failure.cause is JsonEncodingException) { - // Wait 10s before retrying - timer.schedule(object : TimerTask() { - override fun run() { - doSync() - } - }, 5_000L) - } - - if (failure is Failure.ServerError - && (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) { - // No token or invalid token, stop the thread - stopSelf() - } - - } - - }) + } .executeBy(taskExecutor) - } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt index c08f6101..f6ff11c1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt @@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskThread import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.BackgroundDetectionObserver +import kotlinx.coroutines.CancellationException import timber.log.Timber import java.net.SocketTimeoutException import java.util.concurrent.CountDownLatch @@ -70,6 +71,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, if (state is SyncState.RUNNING) { Timber.v("Pause sync...") updateStateTo(SyncState.PAUSED) + cancelableTask?.cancel() + lock.notify() } } @@ -90,56 +93,68 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, backgroundDetectionObserver.register(this) while (state != SyncState.KILLING) { + Timber.v("Entering loop, state: $state") + if (!networkConnectivityChecker.isConnected() || state == SyncState.PAUSED) { - Timber.v("Sync is Paused. Waiting...") + Timber.v("No network or sync is Paused. Waiting...") synchronized(lock) { lock.wait() } + Timber.v("...unlocked") } else { if (state !is SyncState.RUNNING) { updateStateTo(SyncState.RUNNING(afterPause = true)) } - Timber.v("[$this] Execute sync request with timeout $DEFAULT_LONG_POOL_TIMEOUT") + + // No timeout after a pause + val timeout = state.let { if (it is SyncState.RUNNING && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT } + + Timber.v("Execute sync request with timeout $timeout") val latch = CountDownLatch(1) - val params = SyncTask.Params(DEFAULT_LONG_POOL_TIMEOUT) - cancelableTask = syncTask.configureWith(params) - .callbackOn(TaskThread.SYNC) - .executeOn(TaskThread.SYNC) - .dispatchTo(object : MatrixCallback { - override fun onSuccess(data: Unit) { - latch.countDown() - } + val params = SyncTask.Params(timeout) - override fun onFailure(failure: Throwable) { - if (failure is Failure.NetworkConnection - && failure.cause is SocketTimeoutException) { - // Timeout are not critical - Timber.v("Timeout") - } else { - Timber.e(failure) - } + cancelableTask = syncTask.configureWith(params) { + this.callbackThread = TaskThread.SYNC + this.executionThread = TaskThread.SYNC + this.callback = object : MatrixCallback { - if (failure !is Failure.NetworkConnection - || failure.cause is JsonEncodingException) { + override fun onSuccess(data: Unit) { + Timber.v("onSuccess") + latch.countDown() + } + + override fun onFailure(failure: Throwable) { + if (failure is Failure.NetworkConnection && failure.cause is SocketTimeoutException) { + // Timeout are not critical + Timber.v("Timeout") + } else if (failure is Failure.Unknown && failure.throwable is CancellationException) { + Timber.v("Cancelled") + } else if (failure is Failure.ServerError + && (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) { + // No token or invalid token, stop the thread + Timber.w(failure) + updateStateTo(SyncState.KILLING) + } else { + Timber.e(failure) + + if (failure !is Failure.NetworkConnection || failure.cause is JsonEncodingException) { // Wait 10s before retrying + Timber.v("Wait 10s") sleep(RETRY_WAIT_TIME_MS) } - - if (failure is Failure.ServerError - && (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) { - // No token or invalid token, stop the thread - updateStateTo(SyncState.KILLING) - } - - latch.countDown() } - }) + latch.countDown() + } + } + } .executeBy(taskExecutor) latch.await() - if (state is SyncState.RUNNING) { - updateStateTo(SyncState.RUNNING(afterPause = false)) + state.let { + if (it is SyncState.RUNNING && it.afterPause) { + updateStateTo(SyncState.RUNNING(afterPause = false)) + } } Timber.v("...Continue") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt index 570aa9c4..b5d7118b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt @@ -18,18 +18,15 @@ package im.vector.matrix.android.internal.session.sync.job import android.content.Context import androidx.work.* import com.squareup.moshi.JsonClass -import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.session.sync.SyncTask import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.task.TaskThread -import im.vector.matrix.android.internal.task.configureWith +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.worker.WorkManagerUtil import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.getSessionComponent +import kotlinx.coroutines.withContext import timber.log.Timber -import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -47,45 +44,26 @@ internal class SyncWorker(context: Context, val automaticallyRetry: Boolean = false ) - @Inject - lateinit var syncTask: SyncTask - @Inject - lateinit var taskExecutor: TaskExecutor + @Inject lateinit var syncTask: SyncTask + @Inject lateinit var taskExecutor: TaskExecutor + @Inject lateinit var coroutineDispatchers: MatrixCoroutineDispatchers override suspend fun doWork(): Result { Timber.i("Sync work starting") val params = WorkerParamsFactory.fromData(inputData) ?: return Result.success() val sessionComponent = getSessionComponent(params.userId) ?: return Result.success() sessionComponent.inject(this) - - - val latch = CountDownLatch(1) - val taskParams = SyncTask.Params(0) - cancelableTask = syncTask.configureWith(taskParams) - .callbackOn(TaskThread.SYNC) - .executeOn(TaskThread.SYNC) - .dispatchTo(object : MatrixCallback { - override fun onSuccess(data: Unit) { - latch.countDown() - } - - override fun onFailure(failure: Throwable) { - Timber.e(failure) - latch.countDown() - } - - }) - .executeBy(taskExecutor) - - latch.await() + runCatching { + withContext(coroutineDispatchers.sync) { + val taskParams = SyncTask.Params(0) + syncTask.execute(taskParams) + } + } return Result.success() } companion object { - - private var cancelableTask: Cancelable? = null - fun requireBackgroundSync(context: Context, userId: String, serverTimeout: Long = 0) { val data = WorkerParamsFactory.toData(Params(userId, serverTimeout, false)) val workRequest = matrixOneTimeWorkRequestBuilder() @@ -107,7 +85,6 @@ internal class SyncWorker(context: Context, } fun stopAnyBackgroundSync(context: Context) { - cancelableTask?.cancel() WorkManager.getInstance(context).cancelUniqueWork("BG_SYNCP") } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt index 477d5a78..29259973 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt @@ -18,27 +18,55 @@ package im.vector.matrix.android.internal.session.user import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations +import androidx.paging.DataSource +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.UserEntity +import im.vector.matrix.android.internal.database.model.UserEntityFields import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.user.model.SearchUserTask +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.fetchCopied import javax.inject.Inject -internal class DefaultUserService @Inject constructor(private val monarchy: Monarchy) : UserService { +internal class DefaultUserService @Inject constructor(private val monarchy: Monarchy, + private val searchUserTask: SearchUserTask, + private val taskExecutor: TaskExecutor) : UserService { + + private val realmDataSourceFactory: Monarchy.RealmDataSourceFactory by lazy { + monarchy.createDataSourceFactory { realm -> + realm.where(UserEntity::class.java) + .isNotEmpty(UserEntityFields.USER_ID) + .sort(UserEntityFields.DISPLAY_NAME) + } + } + + private val domainDataSourceFactory: DataSource.Factory by lazy { + realmDataSourceFactory.map { + it.asDomain() + } + } + + private val livePagedListBuilder: LivePagedListBuilder by lazy { + LivePagedListBuilder(domainDataSourceFactory, PagedList.Config.Builder().setPageSize(100).setEnablePlaceholders(false).build()) + } override fun getUser(userId: String): User? { val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() } - ?: return null + ?: return null return userEntity.asDomain() } - override fun observeUser(userId: String): LiveData { + override fun liveUser(userId: String): LiveData { val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> UserEntity.where(realm, userId) } @@ -48,4 +76,46 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona .firstOrNull() } } -} \ No newline at end of file + + override fun liveUsers(): LiveData> { + return monarchy.findAllMappedWithChanges( + { realm -> + realm.where(UserEntity::class.java) + .isNotEmpty(UserEntityFields.USER_ID) + .sort(UserEntityFields.DISPLAY_NAME) + }, + { it.asDomain() } + ) + } + + override fun livePagedUsers(filter: String?): LiveData> { + realmDataSourceFactory.updateQuery { realm -> + val query = realm.where(UserEntity::class.java) + if (filter.isNullOrEmpty()) { + query.isNotEmpty(UserEntityFields.USER_ID) + } else { + query + .beginGroup() + .contains(UserEntityFields.DISPLAY_NAME, filter) + .or() + .contains(UserEntityFields.USER_ID, filter) + .endGroup() + } + query.sort(UserEntityFields.DISPLAY_NAME) + } + return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder) + } + + + override fun searchUsersDirectory(search: String, + limit: Int, + excludedUserIds: Set, + callback: MatrixCallback>): Cancelable { + val params = SearchUserTask.Params(limit, search, excludedUserIds) + return searchUserTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt new file mode 100644 index 00000000..aa4d50df --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.user + +import im.vector.matrix.android.internal.network.NetworkConstants.URI_API_PREFIX_PATH_R0 +import im.vector.matrix.android.internal.session.user.model.SearchUsersParams +import im.vector.matrix.android.internal.session.user.model.SearchUsersRequestResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST + +internal interface SearchUserAPI { + + /** + * Perform a user search. + * + * @param searchUsersParams the search params. + */ + @POST(URI_API_PREFIX_PATH_R0 + "user_directory/search") + fun searchUsers(@Body searchUsersParams: SearchUsersParams): Call +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt index 188c7d84..7873bf2f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.user import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.internal.database.model.UserEntity @@ -29,9 +30,13 @@ internal object UserEntityFactory { return null } val roomMember = event.content.toModel() ?: return null + // We only use JOIN and INVITED memberships to create User data + if (roomMember.membership != Membership.JOIN && roomMember.membership != Membership.INVITE) { + return null + } return UserEntity(event.stateKey ?: "", - roomMember.displayName ?: "", - roomMember.avatarUrl ?: "" + roomMember.displayName ?: "", + roomMember.avatarUrl ?: "" ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt index 00368dfa..a31dc137 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt @@ -18,12 +18,30 @@ package im.vector.matrix.android.internal.session.user import dagger.Binds import dagger.Module +import dagger.Provides import im.vector.matrix.android.api.session.user.UserService +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.user.model.DefaultSearchUserTask +import im.vector.matrix.android.internal.session.user.model.SearchUserTask +import retrofit2.Retrofit @Module internal abstract class UserModule { + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesSearchUserAPI(retrofit: Retrofit): SearchUserAPI { + return retrofit.create(SearchUserAPI::class.java) + } + } + @Binds abstract fun bindUserService(userService: DefaultUserService): UserService + @Binds + abstract fun bindSearchUserTask(searchUserTask: DefaultSearchUserTask): SearchUserTask + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataAPI.kt new file mode 100644 index 00000000..824af2d1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataAPI.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.user.accountdata + +import im.vector.matrix.android.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path + +interface AccountDataAPI { + + /** + * Set some account_data for the client. + * + * @param userId the user id + * @param type the type + * @param params the put params + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/account_data/{type}") + fun setAccountData(@Path("userId") userId: String, @Path("type") type: String, @Body params: Any): Call + + /** + * Gets a bearer token from the homeserver that the user can + * present to a third party in order to prove their ownership + * of the Matrix account they are logged into. + * + * @param userId the user id + * @param body the body content + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token") + fun openIdToken(@Path("userId") userId: String, @Body body: Map): Call> +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataModule.kt new file mode 100644 index 00000000..850312d8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataModule.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.user.accountdata + +import dagger.Binds +import dagger.Module +import dagger.Provides +import retrofit2.Retrofit + +@Module +internal abstract class AccountDataModule { + + @Module + companion object { + + @JvmStatic + @Provides + fun providesAccountDataAPI(retrofit: Retrofit): AccountDataAPI { + return retrofit.create(AccountDataAPI::class.java) + } + + } + + @Binds + abstract fun bindUpdateUserAccountDataTask(updateUserAccountDataTask: DefaultUpdateUserAccountDataTask): UpdateUserAccountDataTask + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DirectChatsHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DirectChatsHelper.kt new file mode 100644 index 00000000..b4b14387 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DirectChatsHelper.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.user.accountdata + +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity +import im.vector.matrix.android.internal.database.query.getDirectRooms +import im.vector.matrix.android.internal.di.SessionDatabase +import io.realm.Realm +import io.realm.RealmConfiguration +import javax.inject.Inject + +internal class DirectChatsHelper @Inject constructor(@SessionDatabase + private val realmConfiguration: RealmConfiguration) { + + /** + * @return a map of userId <-> list of roomId + */ + fun getLocalUserAccount(filterRoomId: String? = null): MutableMap> { + return Realm.getInstance(realmConfiguration).use { realm -> + val currentDirectRooms = RoomSummaryEntity.getDirectRooms(realm) + val directChatsMap = mutableMapOf>() + for (directRoom in currentDirectRooms) { + if (directRoom.roomId == filterRoomId) continue + val directUserId = directRoom.directUserId ?: continue + directChatsMap + .getOrPut(directUserId, { arrayListOf() }) + .apply { + add(directRoom.roomId) + } + } + directChatsMap + } + } + + +} \ No newline at end of file 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 new file mode 100644 index 00000000..80fc4cc3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.user.accountdata + +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.sync.model.UserAccountData +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject + +internal interface UpdateUserAccountDataTask : Task { + + interface Params { + val type: String + fun getData(): Any + } + + data class DirectChatParams(override val type: String = UserAccountData.TYPE_DIRECT_MESSAGES, + private val directMessages: Map> + ) : Params { + + override fun getData(): Any { + return directMessages + } + } + + +} + +internal class DefaultUpdateUserAccountDataTask @Inject constructor(private val accountDataApi: AccountDataAPI, + private val credentials: Credentials) : UpdateUserAccountDataTask { + + override suspend fun execute(params: UpdateUserAccountDataTask.Params) { + return executeRequest { + apiCall = accountDataApi.setAccountData(credentials.userId, params.type, params.getData()) + } + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUser.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUser.kt new file mode 100644 index 00000000..da447830 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUser.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.user.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class SearchUser( + @Json(name = "user_id") val userId: String, + @Json(name = "display_name") val displayName: String? = null, + @Json(name = "avatar_url") val avatarUrl: String? = null +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUserTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUserTask.kt new file mode 100644 index 00000000..4472bcf8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUserTask.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.user.model + +import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.user.SearchUserAPI +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject + +internal interface SearchUserTask : Task> { + + data class Params( + val limit: Int, + val search: String, + val excludedUserIds: Set + ) +} + +internal class DefaultSearchUserTask @Inject constructor(private val searchUserAPI: SearchUserAPI) : SearchUserTask { + + override suspend fun execute(params: SearchUserTask.Params): List { + val response = executeRequest { + apiCall = searchUserAPI.searchUsers(SearchUsersParams(params.search, params.limit)) + } + return response.users.map { + User(it.userId, it.displayName, it.avatarUrl) + } + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUsersParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUsersParams.kt new file mode 100644 index 00000000..6ea689e5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUsersParams.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.user.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an user search parameters + */ +@JsonClass(generateAdapter = true) +internal data class SearchUsersParams( + // the searched term + @Json(name = "search_term") val searchTerm: String, + // set a limit to the request response + @Json(name = "limit") val limit: Int +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUsersResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUsersResponse.kt new file mode 100644 index 00000000..b0a8f937 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/model/SearchUsersResponse.kt @@ -0,0 +1,14 @@ +package im.vector.matrix.android.internal.session.user.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an users search response + */ +@JsonClass(generateAdapter = true) +internal data class SearchUsersRequestResponse( + @Json(name = "limited") val limited: Boolean = false, + @Json(name = "results") val users: List = emptyList() +) + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt index 955ccc67..6896fe68 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt @@ -16,49 +16,54 @@ package im.vector.matrix.android.internal.task -import arrow.core.Try import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.util.Cancelable +import java.util.* -internal fun Task.configureWith(params: PARAMS): ConfigurableTask { - return ConfigurableTask(this, params) +internal fun Task.configureWith(params: PARAMS, + init: (ConfigurableTask.Builder.() -> Unit) = {} +): ConfigurableTask { + return ConfigurableTask.Builder(this, params).apply(init).build() } -/** - * Convert a Task to a ConfigurableTask without parameter - */ -internal fun Task.toConfigurableTask(): ConfigurableTask { - return ConfigurableTask(this, Unit) +internal fun Task.configureWith(init: (ConfigurableTask.Builder.() -> Unit) = {}): ConfigurableTask { + return configureWith(Unit, init) } internal data class ConfigurableTask( val task: Task, val params: PARAMS, - val callbackThread: TaskThread = TaskThread.MAIN, - val executionThread: TaskThread = TaskThread.IO, - val retryCount: Int = 0, - val callback: MatrixCallback = object : MatrixCallback {} -) : Task { + val id: UUID, + val callbackThread: TaskThread, + val executionThread: TaskThread, + val constraints: TaskConstraints, + val retryCount: Int, + val callback: MatrixCallback + +) : Task by task { - override suspend fun execute(params: PARAMS): Try { - return task.execute(params) - } + class Builder( + private val task: Task, + private val params: PARAMS, + var id: UUID = UUID.randomUUID(), + var callbackThread: TaskThread = TaskThread.MAIN, + var executionThread: TaskThread = TaskThread.IO, + var constraints: TaskConstraints = TaskConstraints(), + var retryCount: Int = 0, + var callback: MatrixCallback = object : MatrixCallback {} + ) { - fun callbackOn(thread: TaskThread): ConfigurableTask { - return copy(callbackThread = thread) - } - - fun executeOn(thread: TaskThread): ConfigurableTask { - return copy(executionThread = thread) - } - - fun dispatchTo(matrixCallback: MatrixCallback): ConfigurableTask { - return copy(callback = matrixCallback) - } - - fun enableRetry(retryCount: Int = Int.MAX_VALUE): ConfigurableTask { - return copy(retryCount = retryCount) + fun build() = ConfigurableTask( + task = task, + params = params, + id = id, + callbackThread = callbackThread, + executionThread = executionThread, + constraints = constraints, + retryCount = retryCount, + callback = callback + ) } fun executeBy(taskExecutor: TaskExecutor): Cancelable { @@ -66,7 +71,7 @@ internal data class ConfigurableTask( } override fun toString(): String { - return task.javaClass.name + return "${task.javaClass.name} with ID: $id" } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/Task.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/Task.kt index 81a2add8..be761fdb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/Task.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/Task.kt @@ -16,11 +16,9 @@ package im.vector.matrix.android.internal.task -import arrow.core.Try - internal interface Task { - suspend fun execute(params: PARAMS): Try + suspend fun execute(params: PARAMS): RESULT } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskConstraints.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskConstraints.kt new file mode 100644 index 00000000..18733d6e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskConstraints.kt @@ -0,0 +1,22 @@ +/* + + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + + */ +package im.vector.matrix.android.internal.task + +data class TaskConstraints( + val connectedToNetwork: Boolean = false +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt index 31de5ae7..c3f08b15 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt @@ -17,33 +17,41 @@ package im.vector.matrix.android.internal.task -import arrow.core.Try import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.di.MatrixScope import im.vector.matrix.android.internal.extensions.foldToCallback -import im.vector.matrix.android.internal.extensions.onError +import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.util.CancelableCoroutine import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* import timber.log.Timber import javax.inject.Inject import kotlin.coroutines.EmptyCoroutineContext -internal class TaskExecutor @Inject constructor(private val coroutineDispatchers: MatrixCoroutineDispatchers) { +@MatrixScope +internal class TaskExecutor @Inject constructor(private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val networkConnectivityChecker: NetworkConnectivityChecker) { + + private val executorScope = CoroutineScope(SupervisorJob()) fun execute(task: ConfigurableTask): Cancelable { - val job = GlobalScope.launch(task.callbackThread.toDispatcher()) { - val resultOrFailure = withContext(task.executionThread.toDispatcher()) { - Timber.v("Executing $task on ${Thread.currentThread().name}") - retry(task.retryCount) { - task.execute(task.params) + val job = executorScope.launch(task.callbackThread.toDispatcher()) { + val resultOrFailure = runCatching { + withContext(task.executionThread.toDispatcher()) { + Timber.v("Enqueue task $task") + retry(task.retryCount) { + if (task.constraints.connectedToNetwork) { + Timber.v("Waiting network for $task") + networkConnectivityChecker.waitUntilConnected() + } + Timber.v("Execute task $task on ${Thread.currentThread().name}") + task.execute(task.params) + } } } resultOrFailure - .onError { + .onFailure { Timber.d(it, "Task failed") } .foldToCallback(task.callback) @@ -51,19 +59,22 @@ internal class TaskExecutor @Inject constructor(private val coroutineDispatchers return CancelableCoroutine(job) } + fun cancelAll() = executorScope.coroutineContext.cancelChildren() + + private suspend fun retry( times: Int = Int.MAX_VALUE, initialDelay: Long = 100, // 0.1 second maxDelay: Long = 10_000, // 10 second factor: Double = 2.0, - block: suspend () -> Try): Try { + block: suspend () -> T): T { var currentDelay = initialDelay repeat(times - 1) { - val blockResult = block() - if (blockResult.isSuccess()) { - return blockResult - } else { + try { + return block() + } catch (e: Exception) { + Timber.v("Retry task after $currentDelay ms") delay(currentDelay) currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CancelableCoroutine.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CancelableCoroutine.kt index 0ad14b91..97b8cd0e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CancelableCoroutine.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CancelableCoroutine.kt @@ -22,7 +22,9 @@ import kotlinx.coroutines.Job internal class CancelableCoroutine(private val job: Job) : Cancelable { override fun cancel() { - job.cancel() + if (!job.isCancelled) { + job.cancel() + } } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/FileSaver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/FileSaver.kt index 9654f5e0..4f695d2c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/FileSaver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/FileSaver.kt @@ -26,11 +26,8 @@ import java.io.InputStream */ @WorkerThread fun writeToFile(inputStream: InputStream, outputFile: File) { - val source = Okio.buffer(Okio.source(inputStream)) - val sink = Okio.buffer(Okio.sink(outputFile)) - - source.use { input -> - sink.use { output -> + Okio.buffer(Okio.source(inputStream)).use { input -> + Okio.buffer(Okio.sink(outputFile)).use { output -> output.writeAll(input) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Monarchy.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Monarchy.kt index 6b547db1..fc999922 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Monarchy.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Monarchy.kt @@ -16,30 +16,25 @@ package im.vector.matrix.android.internal.util -import arrow.core.Try import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.internal.database.awaitTransaction import io.realm.Realm import io.realm.RealmModel import java.util.concurrent.atomic.AtomicReference -internal fun Monarchy.tryTransactionSync(transaction: (realm: Realm) -> Unit): Try { - return Try { - this.runTransactionSync(transaction) - } -} - -internal fun Monarchy.tryTransactionAsync(transaction: (realm: Realm) -> Unit): Try { - return Try { - this.writeAsync(transaction) - } -} - -fun Monarchy.fetchManaged(query: (Realm) -> T?): T? { - return fetch(query, false) +internal suspend fun Monarchy.awaitTransaction(transaction: suspend (realm: Realm) -> Unit) { + awaitTransaction(realmConfiguration, transaction) } fun Monarchy.fetchCopied(query: (Realm) -> T?): T? { - return fetch(query, true) + val ref = AtomicReference() + doWithRealm { realm -> + val result = query.invoke(realm)?.let { + realm.copyFromRealm(it) + } + ref.set(result) + } + return ref.get() } fun Monarchy.fetchCopyMap(query: (Realm) -> T?, map: (T, realm: Realm) -> U): U? { @@ -52,18 +47,3 @@ fun Monarchy.fetchCopyMap(query: (Realm) -> T?, map: (T, rea } return ref.get() } - -private fun Monarchy.fetch(query: (Realm) -> T?, copyFromRealm: Boolean): T? { - val ref = AtomicReference() - doWithRealm { realm -> - val result = query.invoke(realm)?.let { - if (copyFromRealm) { - realm.copyFromRealm(it) - } else { - it - } - } - ref.set(result) - } - return ref.get() -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringProvider.kt index 479412cf..a9053865 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringProvider.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringProvider.kt @@ -20,9 +20,7 @@ import android.content.res.Resources import androidx.annotation.NonNull import androidx.annotation.StringRes import dagger.Reusable -import im.vector.matrix.android.internal.di.MatrixScope import javax.inject.Inject -import javax.inject.Singleton @Reusable internal class StringProvider @Inject constructor(private val resources: Resources) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt index a83ab013..a2774985 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.util +import im.vector.matrix.android.api.MatrixPatterns import timber.log.Timber /** @@ -49,3 +50,10 @@ fun convertFromUTF8(s: String): String? { null } } + +fun String?.firstLetterOfDisplayName(): String { + if (this.isNullOrEmpty()) return "" + val isUserId = MatrixPatterns.isUserId(this) + val firstLetterIndex = if (isUserId) 1 else 0 + return this[firstLetterIndex].toString().toUpperCase() +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/SuspendMatrixCallback.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/SuspendMatrixCallback.kt new file mode 100644 index 00000000..801578ac --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/SuspendMatrixCallback.kt @@ -0,0 +1,35 @@ +/* + + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + + */ +package im.vector.matrix.android.internal.util + +import im.vector.matrix.android.api.MatrixCallback +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +suspend inline fun awaitCallback(crossinline callback: (MatrixCallback) -> Unit) = suspendCoroutine { cont -> + callback(object : MatrixCallback { + override fun onFailure(failure: Throwable) { + cont.resumeWithException(failure) + } + + override fun onSuccess(data: T) { + cont.resume(data) + } + }) +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/AlwaysSuccessfulWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/AlwaysSuccessfulWorker.kt new file mode 100644 index 00000000..c0cae10d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/AlwaysSuccessfulWorker.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.worker + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters + +internal class AlwaysSuccessfulWorker(context: Context, params: WorkerParameters) + : Worker(context, params) { + + override fun doWork(): Result { + return Result.success() + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/Extensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/Extensions.kt new file mode 100644 index 00000000..dab71625 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/Extensions.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.worker + +import androidx.work.OneTimeWorkRequest +import im.vector.matrix.android.internal.session.room.send.NoMerger + +/** + * If startChain parameter is true, the builder will have a inputMerger set to [NoMerger] + */ +internal fun OneTimeWorkRequest.Builder.startChain(startChain: Boolean): OneTimeWorkRequest.Builder { + if (startChain) { + setInputMerger(NoMerger::class.java) + } + return this +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/MatrixWorkerFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/MatrixWorkerFactory.kt index ca0ddd04..3a676d30 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/MatrixWorkerFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/MatrixWorkerFactory.kt @@ -20,7 +20,6 @@ import android.content.Context import androidx.work.ListenableWorker import androidx.work.WorkerFactory import androidx.work.WorkerParameters -import im.vector.matrix.android.internal.di.MatrixScope import javax.inject.Inject import javax.inject.Provider diff --git a/matrix-sdk-android/src/main/res/values-bg/strings.xml b/matrix-sdk-android/src/main/res/values-bg/strings.xml index 106febb9..743f38e6 100644 --- a/matrix-sdk-android/src/main/res/values-bg/strings.xml +++ b/matrix-sdk-android/src/main/res/values-bg/strings.xml @@ -150,4 +150,21 @@ Папка Карфица + Начална синхронизация: +\nИмпортиране на профил… + Начална синхронизация: +\nИмпортиране на данни за шифроване + Начална синхронизация: +\nИмпортиране на стаи + Начална синхронизация: +\nИмпортиране на стаи, от които съм част + Начална синхронизация: +\nИмпортиране на стаи, към които съм поканен + Начална синхронизация: +\nИмпортиране на стаи, които съм напуснал + Начална синхронизация: +\nИмпортиране на общности + Начална синхронизация: +\nИмпортиране на данни за профила + diff --git a/matrix-sdk-android/src/main/res/values-eu/strings.xml b/matrix-sdk-android/src/main/res/values-eu/strings.xml index a211ea04..5339cf01 100644 --- a/matrix-sdk-android/src/main/res/values-eu/strings.xml +++ b/matrix-sdk-android/src/main/res/values-eu/strings.xml @@ -150,4 +150,21 @@ Karpeta Txintxeta + Hasierako sinkronizazioa: +\nKontua inportatzen… + Hasierako sinkronizazioa: +\nZifratzea inportatzen + Hasierako sinkronizazioa: +\nGelak inportatzen + Hasierako sinkronizazioa: +\nElkartutako gelak inportatzen + Hasierako sinkronizazioa: +\nGonbidatutako gelak inportatzen + Hasierako sinkronizazioa: +\nUtzitako gelak inportatzen + Hasierako sinkronizazioa: +\nKomunitateak inportatzen + Hasierako sinkronizazioa: +\nKontuaren datuak inportatzen + diff --git a/matrix-sdk-android/src/main/res/values-fi/strings.xml b/matrix-sdk-android/src/main/res/values-fi/strings.xml index a405c14c..2f3e73c7 100644 --- a/matrix-sdk-android/src/main/res/values-fi/strings.xml +++ b/matrix-sdk-android/src/main/res/values-fi/strings.xml @@ -2,7 +2,7 @@ %1$s lähetti kuvan. - %s:n kutsu + Käyttäjän %s kutsu %1$s kutsui käyttäjän %2$s %1$s kutsui sinut %1$s liittyi @@ -16,15 +16,15 @@ %1$s asetti näyttönimekseen %2$s %1$s muutti näyttönimensä nimestä %2$s nimeen %3$s %1$s poisti näyttönimensä (%2$s) - %1$s muutti aiheeksi %2$s - %1$s muutti huoneen nimeksi %2$s + %1$s vaihtoi aiheeksi %2$s + %1$s vaihtoi huoneen nimeksi %2$s %s soitti videopuhelun. %s soitti äänipuhelun. %s vastasi puheluun. %s lopetti puhelun. %1$s muutti tulevan huonehistorian näkyväksi käyttäjälle %2$s - kaikki huoneen jäsenet, heidän kutsumisestaan asti. - kaikki huoneen jäsenet, heidän liittymisestään asti. + kaikki huoneen jäsenet, kutsumisestaan asti. + kaikki huoneen jäsenet, liittymisestään asti. kaikki huoneen jäsenet. kaikki. tuntematon (%s). @@ -34,7 +34,7 @@ VoIP-konferenssi alkoi VoIP-konferenssi päättyi - (profiilikuva muuttui myös) + (myös profiilikuva vaihdettiin) %1$s poisti huoneen nimen %1$s poisti huoneen aiheen %1$s päivitti profiilinsa %2$s @@ -151,4 +151,21 @@ Kansio Nuppineula + Alkusynkronointi: +\nTuodaan tiliä… + Alkusynkronointi: +\nTuodaan kryptoa + Alkusynkronointi: +\nTuodaan huoneita + Alkusynkronointi: +\nTuodaan liityttyjä huoneita + Alkusynkronointi: +\nTuodaan kutsuttuja huoneita + Alkusynkronointi: +\nTuodaan poistuttuja huoneita + Alkusynkronointi: +\nTuodaan yhteisöjä + Alkusynkronointi: +\nTuodaan tilin tietoja + diff --git a/matrix-sdk-android/src/main/res/values-fr/strings.xml b/matrix-sdk-android/src/main/res/values-fr/strings.xml index 542f0174..5d211e01 100644 --- a/matrix-sdk-android/src/main/res/values-fr/strings.xml +++ b/matrix-sdk-android/src/main/res/values-fr/strings.xml @@ -150,4 +150,21 @@ Dossier Épingle + Synchronisation initiale : +\nImportation du compte… + Synchronisation initiale : +\nImportation de la cryptographie + Synchronisation initiale : +\nImportation des salons + Synchronisation initiale : +\nImportation des salons que vous avez rejoints + Synchronisation initiale : +\nImportation des salons où vous avez été invités + Synchronisation initiale : +\nImportation des salons que vous avez quittés + Synchronisation initiale : +\nImportation des communautés + Synchronisation initiale : +\nImportation des données du compte + diff --git a/matrix-sdk-android/src/main/res/values-hu/strings.xml b/matrix-sdk-android/src/main/res/values-hu/strings.xml index cf257cab..b183a1b4 100644 --- a/matrix-sdk-android/src/main/res/values-hu/strings.xml +++ b/matrix-sdk-android/src/main/res/values-hu/strings.xml @@ -149,4 +149,21 @@ Mappa + Induló szinkronizáció: +\nFiók betöltése… + Induló szinkronizáció: +\nTitkosítás betöltése + Induló szinkronizáció: +\nSzobák betöltése + Induló szinkronizáció: +\nCsatlakozott szobák betöltése + Induló szinkronizáció: +\nMeghívott szobák betöltése + Induló szinkronizáció: +\nElhagyott szobák betöltése + Induló szinkronizáció: +\nKözösségek betöltése + Induló szinkronizáció: +\nFiók adatok betöltése + diff --git a/matrix-sdk-android/src/main/res/values-it/strings.xml b/matrix-sdk-android/src/main/res/values-it/strings.xml index 7adc3638..3f067c2a 100644 --- a/matrix-sdk-android/src/main/res/values-it/strings.xml +++ b/matrix-sdk-android/src/main/res/values-it/strings.xml @@ -150,4 +150,21 @@ Cartella Spillo + Sync iniziale: +\nImportazione account… + Sync iniziale: +\nImportazione cifratura + Sync iniziale: +\nImportazione stanze + Sync iniziale: +\nImportazione stanze partecipate + Sync iniziale: +\nImportazione stanze invitate + Sync iniziale: +\nImportazione stanze lasciate + Sync iniziale: +\nImportazione comunità + Sync iniziale: +\nImportazione dati account + diff --git a/matrix-sdk-android/src/main/res/values-nl/strings.xml b/matrix-sdk-android/src/main/res/values-nl/strings.xml index c20124d1..b46b79ea 100644 --- a/matrix-sdk-android/src/main/res/values-nl/strings.xml +++ b/matrix-sdk-android/src/main/res/values-nl/strings.xml @@ -159,4 +159,21 @@ Map Speld + Initiële synchronisatie: +\nAccount wordt geïmporteerd… + Initiële synchronisatie: +\nCrypto wordt geïmporteerd + Initiële synchronisatie: +\nGesprekken worden geïmporteerd + Initiële synchronisatie: +\nDeelgenomen gesprekken worden geïmporteerd + Initiële synchronisatie: +\nUitgenodigde gesprekken worden geïmporteerd + Initiële synchronisatie: +\nVerlaten gesprekken worden geïmporteerd + Initiële synchronisatie: +\nGemeenschappen worden geïmporteerd + Initiële synchronisatie: +\nAccountgegevens worden geïmporteerd + diff --git a/matrix-sdk-android/src/main/res/values-ru/strings.xml b/matrix-sdk-android/src/main/res/values-ru/strings.xml index 63a45655..4563fa89 100644 --- a/matrix-sdk-android/src/main/res/values-ru/strings.xml +++ b/matrix-sdk-android/src/main/res/values-ru/strings.xml @@ -159,8 +159,25 @@ Труба Колокол Якорь - Гарнитура + Наушники Папка Булавка + Начальная синхронизация: +\nИмпорт учетной записи… + Начальная синхронизация: +\nИмпорт криптографии + Начальная синхронизация: +\nИмпорт комнат + Начальная синхронизация: +\nИмпорт присоединенных комнат + Начальная синхронизация: +\nИмпорт приглашенных комнат + Начальная синхронизация: +\nИмпорт покинутых комнат + Начальная синхронизация: +\nИмпорт сообществ + Начальная синхронизация: +\nИмпорт данных учетной записи + diff --git a/matrix-sdk-android/src/main/res/values-vls/strings.xml b/matrix-sdk-android/src/main/res/values-vls/strings.xml index 61da5173..29e66940 100644 --- a/matrix-sdk-android/src/main/res/values-vls/strings.xml +++ b/matrix-sdk-android/src/main/res/values-vls/strings.xml @@ -150,4 +150,21 @@ Mappe Pinne + Initiële synchronisoatie: +\nAccount wor geïmporteerd… + Initiële synchronisoatie: +\nCrypto wor geïmporteerd + Initiële synchronisoatie: +\nGesprekkn wordn geïmporteerd + Initiële synchronisoatie: +\nDeelgenoomn gesprekken wordn geïmporteerd + Initiële synchronisoatie: +\nUutgenodigde gesprekkn wordn geïmporteerd + Initiële synchronisoatie: +\nVerloatn gesprekkn wordn geïmporteerd + Initiële synchronisoatie: +\nGemeenschappn wordn geïmporteerd + Initiële synchronisoatie: +\nAccountgegeevns wordn geïmporteerd + diff --git a/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml index b92c72e5..70c99089 100644 --- a/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml +++ b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml @@ -82,4 +82,84 @@ 消息已被 %1$s 移除 消息已被移除 [原因: %1$s] 消息已被 %1$s 移除 [原因: %2$s] + + + 狮子 + + 独角兽 + + 大象 + 兔子 + 熊猫 + 公鸡 + 企鹅 + 乌龟 + + 章鱼 + 蝴蝶 + + + 仙人掌 + 蘑菇 + 地球 + 月亮 + + + 香蕉 + 苹果 + 草莓 + 玉米 + 披萨 + 蛋糕 + + 微笑 + 机器人 + 帽子 + 眼镜 + 扳手 + 圣诞老人 + 点赞 + 雨伞 + 沙漏 + + 礼物 + 灯泡 + + 铅笔 + 回形针 + 剪刀 + + 钥匙 + 锤子 + 电话 + 旗子 + 火车 + 自行车 + 飞机 + 火箭 + 奖杯 + + 吉他 + 喇叭 + 铃铛 + + 耳机 + 文件夹 + 初始化同步: +\n正在导入账号… + 初始化同步: +\n正在导入加密数据 + 初始化同步: +\n正在导入聊天室 + 初始化同步: +\n正在导入已加入的聊天室 + 初始化同步: +\n正在导入已邀请的聊天室 + 初始化同步: +\n正在导入已离开的聊天室 + 初始化同步: +\n正在导入社区 + 初始化同步: +\n正在导入账号数据 + diff --git a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml index 22cf1c0f..0a8f0b45 100644 --- a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml +++ b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml @@ -148,4 +148,21 @@ 資料夾 別針 + 初始化同步: +\n正在匯入帳號…… + 初始化同步: +\n正在匯入 crypto + 初始化同步: +\n正在匯入聊天室 + 初始化同步: +\n正在匯入已加入的聊天室 + 初始化同步: +\n正在匯入已邀請的聊天室 + 初始化同步: +\n正在匯入已離開的聊天室 + 初始化同步: +\n正在匯入社群 + 初始化同步: +\n正在匯入帳號資料 + diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml index 5459cf91..40d35907 100644 --- a/matrix-sdk-android/src/main/res/values/strings.xml +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -31,6 +31,7 @@ anyone. unknown (%s). %1$s turned on end-to-end encryption (%2$s) + %s upgraded this room. %1$s requested a VoIP conference VoIP conference started @@ -238,4 +239,7 @@ Initial Sync:\nImporting Communities Initial Sync:\nImporting Account Data + Sending message… + Clear sending queue + diff --git a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml index 0d2c4cc4..134b699a 100644 --- a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml +++ b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml @@ -1,4 +1,6 @@ + + \ No newline at end of file diff --git a/tools/import_from_riot.sh b/tools/import_from_riot.sh index 194d9c65..cdc42750 100755 --- a/tools/import_from_riot.sh +++ b/tools/import_from_riot.sh @@ -96,6 +96,7 @@ cp ../riot-android/vector/src/main/res/values-nn/strings.xml ./vector/src/ma 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 diff --git a/vector/build.gradle b/vector/build.gradle index 6a7a8e3a..74e6d132 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -1,3 +1,5 @@ +import com.android.build.OutputFile + apply plugin: 'com.android.application' apply plugin: 'com.google.android.gms.oss-licenses-plugin' apply plugin: 'kotlin-android' @@ -13,7 +15,7 @@ androidExtensions { } ext.versionMajor = 0 -ext.versionMinor = 2 +ext.versionMinor = 3 ext.versionPatch = 0 static def getGitTimestamp() { @@ -52,6 +54,11 @@ project.android.buildTypes.all { buildType -> ] } +// map for the version codes +// x86 must have greater values than arm, see https://software.intel.com/en-us/android/articles/google-play-supports-cpu-architecture-filtering-for-multiple-apk +// 64 bits have greater value than 32 bits +ext.abiVersionCodes = ["armeabi-v7a": 1, "arm64-v8a": 2, "x86": 3, "x86_64": 4].withDefault { 0 } + def buildNumber = System.getenv("BUILDKITE_BUILD_NUMBER") as Integer ?: 0 android { @@ -64,9 +71,7 @@ android { targetSdkVersion 28 multiDexEnabled true - // For release, use generateVersionCodeFromVersionName() - // versionCode generateVersionCodeFromTimestamp() - versionCode generateVersionCodeFromVersionName() + // Note: versionCode is depending on the build variant versionName "${versionMajor}.${versionMinor}.${versionPatch}" @@ -83,6 +88,40 @@ android { resValue "string", "build_number", "\"${buildNumber}\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + // Keep abiFilter for the universalApk + ndk { + abiFilters "armeabi-v7a", "x86", 'arm64-v8a', 'x86_64' + } + + // Ref: https://developer.android.com/studio/build/configure-apk-splits.html + splits { + // Configures multiple APKs based on ABI. + abi { + // Enables building multiple APKs per ABI. + enable true + + // By default all ABIs are included, so use reset() and include to specify that we only + // want APKs for armeabi-v7a, x86, arm64-v8a and x86_64. + + // Resets the list of ABIs that Gradle should create APKs for to none. + reset() + + // Specifies a list of ABIs that Gradle should create APKs for. + include "armeabi-v7a", "x86", "arm64-v8a", "x86_64" + + // Generate a universal APK that includes all ABIs, so user who install from CI tool can use this one by default. + universalApk true + } + } + + applicationVariants.all { variant -> + variant.outputs.each { output -> + def baseAbiVersionCode = project.ext.abiVersionCodes.get(output.getFilter(OutputFile.ABI)) + // Known limitation: it does not modify the value in the BuildConfig.java generated file + output.versionCodeOverride = baseAbiVersionCode * 10_000_000 + variant.versionCode + } + } } signingConfigs { @@ -122,6 +161,8 @@ android { gplay { dimension "store" + versionCode = generateVersionCodeFromVersionName() + buildConfigField "boolean", "ALLOW_FCM_USE", "true" buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"G\"" buildConfigField "String", "FLAVOR_DESCRIPTION", "\"GooglePlay\"" @@ -130,6 +171,8 @@ android { fdroid { dimension "store" + versionCode = generateVersionCodeFromTimestamp() + buildConfigField "boolean", "ALLOW_FCM_USE", "false" buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"F\"" buildConfigField "String", "FLAVOR_DESCRIPTION", "\"FDroid\"" @@ -148,7 +191,7 @@ android { dependencies { - def epoxy_version = "3.3.0" + def epoxy_version = "3.7.0" def arrow_version = "0.8.2" def coroutines_version = "1.0.1" def markwon_version = '3.0.0' @@ -193,11 +236,15 @@ dependencies { implementation("com.airbnb.android:epoxy:$epoxy_version") kapt "com.airbnb.android:epoxy-processor:$epoxy_version" + implementation "com.airbnb.android:epoxy-paging:$epoxy_version" implementation 'com.airbnb.android:mvrx:1.0.1' // Work implementation "androidx.work:work-runtime-ktx:2.1.0-rc01" + // Paging + implementation "androidx.paging:paging-runtime-ktx:2.1.0" + // Functional Programming implementation "io.arrow-kt:arrow-core:$arrow_version" @@ -206,7 +253,7 @@ dependencies { // UI implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' - implementation 'com.google.android.material:material:1.1.0-alpha07' + implementation 'com.google.android.material:material:1.1.0-alpha08' implementation 'me.gujun.android:span:1.7' implementation "ru.noties.markwon:core:$markwon_version" implementation "ru.noties.markwon:html:$markwon_version" diff --git a/vector/src/fdroid/java/im/vector/riotx/fdroid/features/settings/troubleshoot/TestAutoStartBoot.kt b/vector/src/fdroid/java/im/vector/riotx/fdroid/features/settings/troubleshoot/TestAutoStartBoot.kt index 076f5ac4..fa4b8aa3 100644 --- a/vector/src/fdroid/java/im/vector/riotx/fdroid/features/settings/troubleshoot/TestAutoStartBoot.kt +++ b/vector/src/fdroid/java/im/vector/riotx/fdroid/features/settings/troubleshoot/TestAutoStartBoot.kt @@ -15,7 +15,6 @@ */ package im.vector.riotx.fdroid.features.settings.troubleshoot -import androidx.appcompat.app.AppCompatActivity import im.vector.riotx.R import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.settings.VectorPreferences @@ -25,12 +24,12 @@ import javax.inject.Inject /** * Test that the application is started on boot */ -class TestAutoStartBoot @Inject constructor(private val context: AppCompatActivity, +class TestAutoStartBoot @Inject constructor(private val vectorPreferences: VectorPreferences, private val stringProvider: StringProvider) : TroubleshootTest(R.string.settings_troubleshoot_test_service_boot_title) { override fun perform() { - if (VectorPreferences.autoStartOnBoot(context)) { + if (vectorPreferences.autoStartOnBoot()) { description = stringProvider.getString(R.string.settings_troubleshoot_test_service_boot_success) status = TestStatus.SUCCESS quickFix = null @@ -38,7 +37,7 @@ class TestAutoStartBoot @Inject constructor(private val context: AppCompatActivi description = stringProvider.getString(R.string.settings_troubleshoot_test_service_boot_failed) quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_service_boot_quickfix) { override fun doFix() { - VectorPreferences.setAutoStartOnBoot(context, true) + vectorPreferences.setAutoStartOnBoot(true) manager?.retry() } } diff --git a/vector/src/fdroid/java/im/vector/riotx/push/fcm/FcmHelper.kt b/vector/src/fdroid/java/im/vector/riotx/push/fcm/FcmHelper.kt index 7ca9957f..5775fc84 100755 --- a/vector/src/fdroid/java/im/vector/riotx/push/fcm/FcmHelper.kt +++ b/vector/src/fdroid/java/im/vector/riotx/push/fcm/FcmHelper.kt @@ -63,9 +63,9 @@ object FcmHelper { AlarmSyncBroadcastReceiver.cancelAlarm(context) } - fun onEnterBackground(context: Context, activeSessionHolder: ActiveSessionHolder) { + fun onEnterBackground(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) { //We need to use alarm in this mode - if (VectorPreferences.areNotificationEnabledForDevice(context) && activeSessionHolder.hasActiveSession()) { + if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) { val currentSession = activeSessionHolder.getActiveSession() AlarmSyncBroadcastReceiver.scheduleAlarm(context, currentSession.myUserId, 4_000L) Timber.i("Alarm scheduled to restart service") diff --git a/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt index 5a4aa36c..5ad06468 100755 --- a/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt +++ b/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt @@ -52,6 +52,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { private lateinit var notifiableEventResolver: NotifiableEventResolver private lateinit var pusherManager: PushersManager private lateinit var activeSessionHolder: ActiveSessionHolder + private lateinit var vectorPreferences: VectorPreferences // UI handler private val mUIHandler by lazy { @@ -64,6 +65,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { notifiableEventResolver = vectorComponent().notifiableEventResolver() pusherManager = vectorComponent().pusherManager() activeSessionHolder = vectorComponent().activeSessionHolder() + vectorPreferences = vectorComponent().vectorPreferences() } /** @@ -72,7 +74,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { * @param message the message */ override fun onMessageReceived(message: RemoteMessage?) { - if (!VectorPreferences.areNotificationEnabledForDevice(applicationContext)) { + if (!vectorPreferences.areNotificationEnabledForDevice()) { Timber.i("Notification are disabled for this device") return } @@ -107,7 +109,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { if (refreshedToken == null) { Timber.w("onNewToken:received null token") } else { - if (VectorPreferences.areNotificationEnabledForDevice(applicationContext) && activeSessionHolder.hasActiveSession()) { + if (vectorPreferences.areNotificationEnabledForDevice() && activeSessionHolder.hasActiveSession()) { pusherManager.registerPusherWithFcmKey(refreshedToken) } } @@ -153,7 +155,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { Timber.w("## Can't sync from push, no current session") } else { if (isEventAlreadyKnown(eventId, roomId)) { - Timber.i("Ignoring push, event already knwown") + Timber.i("Ignoring push, event already known") } else { Timber.v("Requesting background sync") session.requireBackgroundSync() diff --git a/vector/src/gplay/java/im/vector/riotx/push/fcm/FcmHelper.kt b/vector/src/gplay/java/im/vector/riotx/push/fcm/FcmHelper.kt index c19aa561..148a8336 100755 --- a/vector/src/gplay/java/im/vector/riotx/push/fcm/FcmHelper.kt +++ b/vector/src/gplay/java/im/vector/riotx/push/fcm/FcmHelper.kt @@ -27,6 +27,7 @@ import com.google.firebase.iid.FirebaseInstanceId import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.pushers.PushersManager +import im.vector.riotx.features.settings.VectorPreferences import timber.log.Timber /** @@ -105,7 +106,7 @@ object FcmHelper { // No op } - fun onEnterBackground(context: Context, activeSessionHolder: ActiveSessionHolder) { + fun onEnterBackground(context: Context, vectorPreferences: VectorPreferences, activeSessionHolder: ActiveSessionHolder) { // TODO FCM fallback } } diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index e0deced9..e4cdaee2 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -64,6 +64,7 @@ + diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt index f9416fc0..66356ac0 100644 --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt @@ -42,6 +42,7 @@ import im.vector.riotx.core.di.DaggerVectorComponent import im.vector.riotx.core.di.HasVectorInjector import im.vector.riotx.core.di.VectorComponent import im.vector.riotx.core.extensions.configureAndStart +import im.vector.riotx.core.utils.initKnownEmojiHashSet import im.vector.riotx.features.configuration.VectorConfiguration import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks import im.vector.riotx.features.notifications.NotificationDrawerManager @@ -49,12 +50,12 @@ import im.vector.riotx.features.notifications.NotificationUtils import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.rageshake.VectorFileLogger import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler -import im.vector.riotx.features.version.getVersion +import im.vector.riotx.features.settings.VectorPreferences +import im.vector.riotx.features.version.VersionProvider import im.vector.riotx.push.fcm.FcmHelper import timber.log.Timber import java.text.SimpleDateFormat import java.util.* -import im.vector.riotx.core.utils.initKnownEmojiHashSet import javax.inject.Inject class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.Provider, androidx.work.Configuration.Provider { @@ -69,6 +70,8 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener + @Inject lateinit var vectorPreferences: VectorPreferences + @Inject lateinit var versionProvider: VersionProvider lateinit var vectorComponent: VectorComponent private var fontThreadHandler: Handler? = null @@ -122,7 +125,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. fun entersBackground() { Timber.i("App entered background") // call persistInfo notificationDrawerManager.persistInfo() - FcmHelper.onEnterBackground(appContext, activeSessionHolder) + FcmHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder) } }) //This should be done as early as possible @@ -138,7 +141,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. } private fun logInfo() { - val appVersion = getVersion(longFormat = true, useBuildNumber = true) + val appVersion = versionProvider.getVersion(longFormat = true, useBuildNumber = true) val sdkVersion = Matrix.getSdkVersion() val date = SimpleDateFormat("MM-dd HH:mm:ss.SSSZ", Locale.US).format(Date()) diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index a42eec49..fb1c18f9 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -36,6 +36,9 @@ import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.home.HomeDetailFragment import im.vector.riotx.features.home.HomeDrawerFragment import im.vector.riotx.features.home.HomeModule +import im.vector.riotx.features.home.createdirect.CreateDirectRoomActivity +import im.vector.riotx.features.home.createdirect.CreateDirectRoomDirectoryUsersFragment +import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFragment import im.vector.riotx.features.home.group.GroupListFragment import im.vector.riotx.features.home.room.detail.RoomDetailFragment import im.vector.riotx.features.home.room.detail.timeline.action.* @@ -45,6 +48,7 @@ import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.login.LoginActivity import im.vector.riotx.features.media.ImageMediaViewerActivity import im.vector.riotx.features.media.VideoMediaViewerActivity +import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.rageshake.BugReportActivity import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.RageShake @@ -55,10 +59,7 @@ import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment -import im.vector.riotx.features.settings.VectorSettingsActivity -import im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFragment -import im.vector.riotx.features.settings.VectorSettingsNotificationsTroubleshootFragment -import im.vector.riotx.features.settings.VectorSettingsPreferencesFragment +import im.vector.riotx.features.settings.* import im.vector.riotx.features.settings.push.PushGatewaysFragment @Component(dependencies = [VectorComponent::class], modules = [ViewModelModule::class, HomeModule::class]) @@ -73,6 +74,8 @@ interface ScreenComponent { fun rageShake(): RageShake + fun navigator(): Navigator + fun inject(activity: HomeActivity) fun inject(roomDetailFragment: RoomDetailFragment) @@ -147,12 +150,24 @@ interface ScreenComponent { fun inject(vectorSettingsPreferencesFragment: VectorSettingsPreferencesFragment) + fun inject(vectorSettingsAdvancedNotificationPreferenceFragment: VectorSettingsAdvancedNotificationPreferenceFragment) + + fun inject(vectorSettingsSecurityPrivacyFragment: VectorSettingsSecurityPrivacyFragment) + + fun inject(vectorSettingsHelpAboutFragment: VectorSettingsHelpAboutFragment) + fun inject(userAvatarPreference: UserAvatarPreference) fun inject(vectorSettingsNotificationsTroubleshootFragment: VectorSettingsNotificationsTroubleshootFragment) fun inject(pushGatewaysFragment: PushGatewaysFragment) + fun inject(createDirectRoomKnownUsersFragment: CreateDirectRoomKnownUsersFragment) + + fun inject(createDirectRoomDirectoryUsersFragment: CreateDirectRoomDirectoryUsersFragment) + + fun inject(createDirectRoomActivity: CreateDirectRoomActivity) + @Component.Factory interface Factory { fun create(vectorComponent: VectorComponent, 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 3743e886..d1b87f0b 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 @@ -41,6 +41,7 @@ import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler +import im.vector.riotx.features.settings.VectorPreferences import javax.inject.Singleton @Component(modules = [VectorModule::class]) @@ -95,6 +96,8 @@ interface VectorComponent { fun notifiableEventResolver(): NotifiableEventResolver + fun vectorPreferences(): VectorPreferences + @Component.Factory interface Factory { fun create(@BindsInstance context: Context): VectorComponent diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index 534a346a..80410f87 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -30,6 +30,9 @@ import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsVie import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedViewModel import im.vector.riotx.features.crypto.verification.SasVerificationViewModel import im.vector.riotx.features.home.* +import im.vector.riotx.features.home.createdirect.CreateDirectRoomNavigationViewModel +import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel +import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel_AssistedFactory import im.vector.riotx.features.home.group.GroupListViewModel import im.vector.riotx.features.home.group.GroupListViewModel_AssistedFactory import im.vector.riotx.features.home.room.detail.RoomDetailViewModel @@ -116,6 +119,11 @@ interface ViewModelModule { @ViewModelKey(ConfigurationViewModel::class) fun bindConfigurationViewModel(viewModel: ConfigurationViewModel): ViewModel + @Binds + @IntoMap + @ViewModelKey(CreateDirectRoomNavigationViewModel::class) + fun bindCreateDirectRoomNavigationViewModel(viewModel: CreateDirectRoomNavigationViewModel): ViewModel + /** * Below are bindings for the MvRx view models (which extend VectorViewModel). Will be the only usage in the future. */ @@ -168,6 +176,9 @@ interface ViewModelModule { @Binds fun bindCreateRoomViewModelFactory(factory: CreateRoomViewModel_AssistedFactory): CreateRoomViewModel.Factory + @Binds + fun bindCreateDirectRoomViewModelFactory(factory: CreateDirectRoomViewModel_AssistedFactory): CreateDirectRoomViewModel.Factory + @Binds fun bindPushGatewaysViewModelFactory(factory: PushGatewaysViewModel_AssistedFactory): PushGatewaysViewModel.Factory diff --git a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt index 7619d433..d42bce64 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt @@ -30,12 +30,15 @@ class ErrorFormatter @Inject constructor(val stringProvider: StringProvider) { } fun toHumanReadable(throwable: Throwable?): String { - return when (throwable) { - null -> "" + null -> null is Failure.NetworkConnection -> stringProvider.getString(R.string.error_no_network) + is Failure.ServerError -> { + throwable.error.message.takeIf { it.isNotEmpty() } + ?: throwable.error.code.takeIf { it.isNotEmpty() } + } else -> throwable.localizedMessage } - + ?: stringProvider.getString(R.string.unknown_error) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/error/ResourceLimitErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ResourceLimitErrorFormatter.kt new file mode 100644 index 00000000..b57014f1 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/error/ResourceLimitErrorFormatter.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.core.error + +import android.content.Context +import android.text.Html +import androidx.annotation.StringRes +import im.vector.matrix.android.api.failure.MatrixError +import im.vector.riotx.R +import me.gujun.android.span.span + +class ResourceLimitErrorFormatter(private val context: Context) { + + // 'hard' if the logged in user has been locked out, 'soft' if they haven't + sealed class Mode(@StringRes val mauErrorRes: Int, @StringRes val defaultErrorRes: Int, @StringRes val contactRes: Int) { + // User can still send message (will be used in a near future) + object Soft : Mode(R.string.resource_limit_soft_mau, R.string.resource_limit_soft_default, R.string.resource_limit_soft_contact) + + // User cannot send message anymore + object Hard : Mode(R.string.resource_limit_hard_mau, R.string.resource_limit_hard_default, R.string.resource_limit_hard_contact) + } + + fun format(matrixError: MatrixError, + mode: Mode, + separator: CharSequence = " ", + clickable: Boolean = false): CharSequence { + val error = if (MatrixError.LIMIT_TYPE_MAU == matrixError.limitType) { + context.getString(mode.mauErrorRes) + } else { + context.getString(mode.defaultErrorRes) + } + val contact = if (clickable && matrixError.adminUri != null) { + val contactSubString = uriAsLink(matrixError.adminUri!!) + val contactFullString = context.getString(mode.contactRes, contactSubString) + Html.fromHtml(contactFullString) + } else { + val contactSubString = context.getString(R.string.resource_limit_contact_admin) + context.getString(mode.contactRes, contactSubString) + } + return span { + text = error + } + .append(separator) + .append(contact) + } + + /** + * Create a HTML link with a uri + */ + private fun uriAsLink(uri: String): String { + val contactStr = context.getString(R.string.resource_limit_contact_admin) + return "$contactStr" + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/EditText.kt b/vector/src/main/java/im/vector/riotx/core/extensions/EditText.kt index ecc7795a..cf64c9b3 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/EditText.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/EditText.kt @@ -18,22 +18,22 @@ package im.vector.riotx.core.extensions import android.text.Editable import android.text.InputType -import android.text.TextWatcher import android.view.MotionEvent import android.view.View import android.view.inputmethod.EditorInfo import android.widget.EditText +import androidx.annotation.DrawableRes import im.vector.riotx.R +import im.vector.riotx.core.platform.SimpleTextWatcher -fun EditText.setupAsSearch() { - addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(editable: Editable?) { - val clearIcon = if (editable?.isNotEmpty() == true) R.drawable.ic_clear_white else 0 - setCompoundDrawablesWithIntrinsicBounds(0, 0, clearIcon, 0) +fun EditText.setupAsSearch(@DrawableRes searchIconRes: Int = R.drawable.ic_filter, + @DrawableRes clearIconRes: Int = R.drawable.ic_x_green) { + + addTextChangedListener(object : SimpleTextWatcher() { + override fun afterTextChanged(s: Editable) { + val clearIcon = if (s.isNotEmpty()) clearIconRes else 0 + setCompoundDrawablesWithIntrinsicBounds(searchIconRes, 0, clearIcon, 0) } - - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit }) maxLines = 1 diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/LiveData.kt b/vector/src/main/java/im/vector/riotx/core/extensions/LiveData.kt index a278eab0..97215e1e 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/LiveData.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/LiveData.kt @@ -18,6 +18,7 @@ package im.vector.riotx.core.extensions import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import im.vector.riotx.core.utils.FirstThrottler import im.vector.riotx.core.utils.EventObserver @@ -44,3 +45,7 @@ inline fun LiveData>.observeEventFirstThrottle(owner: Lifecycle } }) } + +fun MutableLiveData>.postLiveEvent(content: T) { + this.postValue(LiveEvent(content)) +} diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt b/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt index db171300..58fcd0b5 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt @@ -21,5 +21,5 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent fun TimelineEvent.canReact(): Boolean { // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment - return root.getClearType() == EventType.MESSAGE && sendState.isSent() && !root.isRedacted() + return root.getClearType() == EventType.MESSAGE && root.sendState.isSent() && !root.isRedacted() } diff --git a/vector/src/main/java/im/vector/riotx/core/intent/Filename.kt b/vector/src/main/java/im/vector/riotx/core/intent/Filename.kt index 2b8421f6..3ea25294 100644 --- a/vector/src/main/java/im/vector/riotx/core/intent/Filename.kt +++ b/vector/src/main/java/im/vector/riotx/core/intent/Filename.kt @@ -35,9 +35,9 @@ fun getFilenameFromUri(context: Context?, uri: Uri): String? { } if (result == null) { result = uri.path - val cut = result.lastIndexOf('/') + val cut = result?.lastIndexOf('/') ?: -1 if (cut != -1) { - result = result.substring(cut + 1) + result = result?.substring(cut + 1) } } return result diff --git a/vector/src/main/java/im/vector/riotx/core/mvrx/NavigationViewModel.kt b/vector/src/main/java/im/vector/riotx/core/mvrx/NavigationViewModel.kt index ab3ce7c8..a6bf07e0 100644 --- a/vector/src/main/java/im/vector/riotx/core/mvrx/NavigationViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/core/mvrx/NavigationViewModel.kt @@ -19,6 +19,7 @@ package im.vector.riotx.core.mvrx import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.utils.LiveEvent abstract class NavigationViewModel : ViewModel() { @@ -29,6 +30,6 @@ abstract class NavigationViewModel : ViewModel() { fun goTo(navigation: NavigationClass) { - _navigateTo.postValue(LiveEvent(navigation)) + _navigateTo.postLiveEvent(navigation) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/platform/ConfigurationViewModel.kt b/vector/src/main/java/im/vector/riotx/core/platform/ConfigurationViewModel.kt index bb2db5cb..dae8145f 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/ConfigurationViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/ConfigurationViewModel.kt @@ -19,6 +19,7 @@ package im.vector.riotx.core.platform import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.features.configuration.VectorConfiguration import timber.log.Timber @@ -46,7 +47,7 @@ class ConfigurationViewModel @Inject constructor( if (newHash != currentConfigurationValue) { Timber.v("Configuration: recreate the Activity") currentConfigurationValue = newHash - _activityRestarter.postValue(LiveEvent(Unit)) + _activityRestarter.postLiveEvent(Unit) } } } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt b/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt new file mode 100644 index 00000000..92796bbd --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.core.platform + +import android.annotation.TargetApi +import android.content.Context +import android.content.res.TypedArray +import android.os.Build +import android.util.AttributeSet +import android.view.View +import android.widget.ScrollView + +import im.vector.riotx.R + +private const val DEFAULT_MAX_HEIGHT = 200 + +class MaxHeightScrollView : ScrollView { + + var maxHeight: Int = 0 + set(value) { + field = value + requestLayout() + } + + constructor(context: Context) : super(context) {} + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + if (!isInEditMode) { + init(context, attrs) + } + } + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + if (!isInEditMode) { + init(context, attrs) + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { + if (!isInEditMode) { + init(context, attrs) + } + } + + private fun init(context: Context, attrs: AttributeSet?) { + if (attrs != null) { + val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView) + maxHeight = styledAttrs.getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT) + styledAttrs.recycle() + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST) + super.onMeasure(widthMeasureSpec, newHeightMeasureSpec) + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/platform/NotificationAreaView.kt b/vector/src/main/java/im/vector/riotx/core/platform/NotificationAreaView.kt new file mode 100644 index 00000000..a321fd1a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/platform/NotificationAreaView.kt @@ -0,0 +1,318 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.core.platform + +import android.content.Context +import android.graphics.Color +import android.text.SpannableString +import android.text.TextPaint +import android.text.TextUtils +import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan +import android.util.AttributeSet +import android.view.View +import android.widget.ImageView +import android.widget.RelativeLayout +import android.widget.TextView +import androidx.core.content.ContextCompat +import butterknife.BindView +import butterknife.ButterKnife +import im.vector.matrix.android.api.failure.MatrixError +import im.vector.matrix.android.api.permalinks.PermalinkFactory +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent +import im.vector.riotx.R +import im.vector.riotx.core.error.ResourceLimitErrorFormatter +import im.vector.riotx.features.themes.ThemeUtils +import me.gujun.android.span.span +import me.saket.bettermovementmethod.BetterLinkMovementMethod +import timber.log.Timber + +/** + * The view used to show some information about the room + * It does have a unique render method + */ +class NotificationAreaView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : RelativeLayout(context, attrs, defStyleAttr) { + + @BindView(R.id.room_notification_icon) + lateinit var imageView: ImageView + @BindView(R.id.room_notification_message) + lateinit var messageView: TextView + + var delegate: Delegate? = null + private var state: State = State.Initial + + init { + setupView() + } + + /** + * This methods is responsible for rendering the view according to the newState + * + * @param newState the newState representing the view + */ + fun render(newState: State) { + if (newState == state) { + Timber.d("State unchanged") + return + } + Timber.d("Rendering $newState") + cleanUp() + state = newState + when (newState) { + is State.Default -> renderDefault() + is State.Hidden -> renderHidden() + is State.Tombstone -> renderTombstone(newState) + is State.ResourceLimitExceededError -> renderResourceLimitExceededError(newState) + is State.ConnectionError -> renderConnectionError() + is State.Typing -> renderTyping(newState) + is State.UnreadPreview -> renderUnreadPreview() + is State.ScrollToBottom -> renderScrollToBottom(newState) + is State.UnsentEvents -> renderUnsent(newState) + } + } + + // PRIVATE METHODS ***************************************************************************************************************************************** + + private fun setupView() { + inflate(context, R.layout.view_notification_area, this) + ButterKnife.bind(this) + } + + private fun cleanUp() { + messageView.setOnClickListener(null) + imageView.setOnClickListener(null) + setBackgroundColor(Color.TRANSPARENT) + messageView.text = null + imageView.setImageResource(0) + } + + private fun renderTombstone(state: State.Tombstone) { + visibility = View.VISIBLE + imageView.setImageResource(R.drawable.error) + val message = span { + +resources.getString(R.string.room_tombstone_versioned_description) + +"\n" + span(resources.getString(R.string.room_tombstone_continuation_link)) { + textDecorationLine = "underline" + onClick = { delegate?.onTombstoneEventClicked(state.tombstoneEvent) } + } + } + messageView.movementMethod = BetterLinkMovementMethod.getInstance() + messageView.text = message + } + + private fun renderResourceLimitExceededError(state: State.ResourceLimitExceededError) { + visibility = View.VISIBLE + val resourceLimitErrorFormatter = ResourceLimitErrorFormatter(context) + val formatterMode: ResourceLimitErrorFormatter.Mode + val backgroundColor: Int + if (state.isSoft) { + backgroundColor = R.color.soft_resource_limit_exceeded + formatterMode = ResourceLimitErrorFormatter.Mode.Soft + } else { + backgroundColor = R.color.hard_resource_limit_exceeded + formatterMode = ResourceLimitErrorFormatter.Mode.Hard + } + val message = resourceLimitErrorFormatter.format(state.matrixError, formatterMode, clickable = true) + messageView.setTextColor(Color.WHITE) + messageView.text = message + messageView.movementMethod = LinkMovementMethod.getInstance() + messageView.setLinkTextColor(Color.WHITE) + setBackgroundColor(ContextCompat.getColor(context, backgroundColor)) + } + + private fun renderConnectionError() { + visibility = View.VISIBLE + imageView.setImageResource(R.drawable.error) + messageView.setTextColor(ContextCompat.getColor(context, R.color.vector_fuchsia_color)) + messageView.text = SpannableString(resources.getString(R.string.room_offline_notification)) + } + + private fun renderTyping(state: State.Typing) { + visibility = View.VISIBLE + imageView.setImageResource(R.drawable.vector_typing) + messageView.text = SpannableString(state.message) + messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color)) + } + + private fun renderUnreadPreview() { + visibility = View.VISIBLE + imageView.setImageResource(R.drawable.scrolldown) + messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color)) + imageView.setOnClickListener { delegate?.closeScreen() } + } + + private fun renderScrollToBottom(state: State.ScrollToBottom) { + visibility = View.VISIBLE + if (state.unreadCount > 0) { + imageView.setImageResource(R.drawable.newmessages) + messageView.setTextColor(ContextCompat.getColor(context, R.color.vector_fuchsia_color)) + messageView.text = SpannableString(resources.getQuantityString(R.plurals.room_new_messages_notification, state.unreadCount, state.unreadCount)) + } else { + imageView.setImageResource(R.drawable.scrolldown) + messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color)) + if (!TextUtils.isEmpty(state.message)) { + messageView.text = SpannableString(state.message) + } + } + messageView.setOnClickListener { delegate?.jumpToBottom() } + imageView.setOnClickListener { delegate?.jumpToBottom() } + } + + private fun renderUnsent(state: State.UnsentEvents) { + visibility = View.VISIBLE + imageView.setImageResource(R.drawable.error) + val cancelAll = resources.getString(R.string.room_prompt_cancel) + val resendAll = resources.getString(R.string.room_prompt_resend) + val messageRes = if (state.hasUnknownDeviceEvents) R.string.room_unknown_devices_messages_notification else R.string.room_unsent_messages_notification + val message = context.getString(messageRes, resendAll, cancelAll) + val cancelAllPos = message.indexOf(cancelAll) + val resendAllPos = message.indexOf(resendAll) + val spannableString = SpannableString(message) + // cancelAllPos should always be > 0 but a GA crash reported here + if (cancelAllPos >= 0) { + spannableString.setSpan(CancelAllClickableSpan(), cancelAllPos, cancelAllPos + cancelAll.length, 0) + } + + // resendAllPos should always be > 0 but a GA crash reported here + if (resendAllPos >= 0) { + spannableString.setSpan(ResendAllClickableSpan(), resendAllPos, resendAllPos + resendAll.length, 0) + } + messageView.movementMethod = LinkMovementMethod.getInstance() + messageView.setTextColor(ContextCompat.getColor(context, R.color.vector_fuchsia_color)) + messageView.text = spannableString + } + + private fun renderDefault() { + visibility = View.GONE + } + + private fun renderHidden() { + visibility = View.GONE + } + + /** + * Track the cancel all click. + */ + private inner class CancelAllClickableSpan : ClickableSpan() { + override fun onClick(widget: View) { + delegate?.deleteUnsentEvents() + render(state) + } + + override fun updateDrawState(ds: TextPaint) { + super.updateDrawState(ds) + ds.color = ContextCompat.getColor(context, R.color.vector_fuchsia_color) + ds.bgColor = 0 + ds.isUnderlineText = true + } + } + + /** + * Track the resend all click. + */ + private inner class ResendAllClickableSpan : ClickableSpan() { + override fun onClick(widget: View) { + delegate?.resendUnsentEvents() + render(state) + } + + override fun updateDrawState(ds: TextPaint) { + super.updateDrawState(ds) + ds.color = ContextCompat.getColor(context, R.color.vector_fuchsia_color) + ds.bgColor = 0 + ds.isUnderlineText = true + } + } + + /** + * The state representing the view + * It can take one state at a time + * Priority of state is managed in {@link VectorRoomActivity.refreshNotificationsArea() } + */ + sealed class State { + + // Not yet rendered + object Initial : State() + + // View will be Invisible + object Default : State() + + // View will be Gone + object Hidden : State() + + // Resource limit exceeded error will be displayed (only hard for the moment) + data class ResourceLimitExceededError(val isSoft: Boolean, val matrixError: MatrixError) : State() + + // Server connection is lost + object ConnectionError : State() + + // The room is dead + data class Tombstone(val tombstoneEvent: Event) : State() + + // Somebody is typing + data class Typing(val message: String) : State() + + // Some new messages are unread in preview + object UnreadPreview : State() + + // Some new messages are unread (grey or red) + data class ScrollToBottom(val unreadCount: Int, val message: String? = null) : State() + + // Some event has been unsent + data class UnsentEvents(val hasUndeliverableEvents: Boolean, val hasUnknownDeviceEvents: Boolean) : State() + } + + /** + * An interface to delegate some actions to another object + */ + interface Delegate { + fun onTombstoneEventClicked(tombstoneEvent: Event) + fun resendUnsentEvents() + fun deleteUnsentEvents() + fun closeScreen() + fun jumpToBottom() + } + + companion object { + /** + * Preference key. + */ + private const val SHOW_INFO_AREA_KEY = "SETTINGS_SHOW_INFO_AREA_KEY" + + /** + * Always show the info area. + */ + private const val SHOW_INFO_AREA_VALUE_ALWAYS = "always" + + /** + * Show the info area when it has messages or errors. + */ + private const val SHOW_INFO_AREA_VALUE_MESSAGES_AND_ERRORS = "messages_and_errors" + + /** + * Show the info area only when it has errors. + */ + private const val SHOW_INFO_AREA_VALUE_ONLY_ERRORS = "only_errors" + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt index 546937da..ff301389 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt @@ -18,6 +18,7 @@ package im.vector.riotx.core.platform import android.view.View import android.widget.ProgressBar import android.widget.TextView +import androidx.annotation.CallSuper import androidx.core.view.isGone import androidx.core.view.isVisible import butterknife.BindView @@ -46,6 +47,7 @@ abstract class SimpleFragmentActivity : VectorBaseActivity() { @Inject lateinit var session: Session + @CallSuper override fun injectWith(injector: ScreenComponent) { session = injector.session() } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/SimpleTextWatcher.kt b/vector/src/main/java/im/vector/riotx/core/platform/SimpleTextWatcher.kt index e54a6d29..94680556 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/SimpleTextWatcher.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/SimpleTextWatcher.kt @@ -19,6 +19,7 @@ package im.vector.riotx.core.platform import android.text.Editable import android.text.TextWatcher + /** * TextWatcher with default no op implementation */ 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 92f72de6..6145d5a7 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 @@ -26,6 +26,7 @@ import androidx.annotation.* import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.isVisible +import androidx.fragment.app.FragmentManager import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders @@ -40,6 +41,7 @@ import im.vector.riotx.R import im.vector.riotx.core.di.* import im.vector.riotx.core.utils.toast import im.vector.riotx.features.configuration.VectorConfiguration +import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.rageshake.BugReportActivity import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.RageShake @@ -70,6 +72,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector { private lateinit var configurationViewModel: ConfigurationViewModel protected lateinit var bugReporter: BugReporter private lateinit var rageShake: RageShake + protected lateinit var navigator: Navigator private var unBinder: Unbinder? = null @@ -116,11 +119,14 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector { injectWith(screenComponent) } Timber.v("Injecting dependencies into ${javaClass.simpleName} took $timeForInjection ms") + ThemeUtils.setActivityTheme(this, getOtherThemes()) + super.onCreate(savedInstanceState) viewModelFactory = screenComponent.viewModelFactory() configurationViewModel = ViewModelProviders.of(this, viewModelFactory).get(ConfigurationViewModel::class.java) bugReporter = screenComponent.bugReporter() rageShake = screenComponent.rageShake() + navigator = screenComponent.navigator() configurationViewModel.activityRestarter.observe(this, Observer { if (!it.hasBeenHandled) { // Recreate the Activity because configuration has changed @@ -131,8 +137,6 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector { // Shake detector - ThemeUtils.setActivityTheme(this, getOtherThemes()) - doBeforeSetContentView() if (getLayoutRes() != -1) { @@ -262,6 +266,24 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector { return super.onOptionsItemSelected(item) } + protected fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean { + // if (fm.backStackEntryCount == 0) + // return false + + val reverseOrder = fm.fragments.filter { it is OnBackPressed }.reversed() + for (f in reverseOrder) { + val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager) + if (handledByChildFragments) { + return true + } + val backPressable = f as OnBackPressed + if (backPressable.onBackPressed()) { + return true + } + } + return false + } + /* ========================================================================================== * PROTECTED METHODS * ========================================================================================== */ diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt index ec5e419d..aac19d80 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt @@ -65,7 +65,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed, HasScreen override fun onAttach(context: Context) { screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity) - navigator = vectorBaseActivity.getVectorComponent().navigator() + navigator = screenComponent.navigator() viewModelFactory = screenComponent.viewModelFactory() injectWith(injector()) super.onAttach(context) diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt index 1570a7f8..1c2f1d53 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt @@ -16,20 +16,36 @@ package im.vector.riotx.core.platform -import com.airbnb.mvrx.BaseMvRxViewModel -import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.* import im.vector.matrix.android.api.util.CancelableBag import im.vector.riotx.BuildConfig +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.disposables.Disposable abstract class VectorViewModel(initialState: S) : BaseMvRxViewModel(initialState, false) { - protected val cancelableBag = CancelableBag() - - override fun onCleared() { - super.onCleared() - cancelableBag.cancel() + /** + * This method does the same thing as the execute function, but it doesn't subscribe to the stream + * so you can use this in a switchMap or a flatMap + */ + fun Single.toAsync(stateReducer: S.(Async) -> S): Single> { + setState { stateReducer(Loading()) } + return this.map { Success(it) as Async } + .onErrorReturn { Fail(it) } + .doOnSuccess { setState { stateReducer(it) } } } + /** + * This method does the same thing as the execute function, but it doesn't subscribe to the stream + * so you can use this in a switchMap or a flatMap + */ + fun Observable.toAsync(stateReducer: S.(Async) -> S): Observable> { + setState { stateReducer(Loading()) } + return this.map { Success(it) as Async } + .onErrorReturn { Fail(it) } + .doOnNext { setState { stateReducer(it) } } + } } \ No newline at end of file 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 346f4766..0d2c30a5 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 @@ -16,13 +16,12 @@ package im.vector.riotx.core.resources -import android.content.Context import im.vector.riotx.features.settings.VectorPreferences import javax.inject.Inject -class UserPreferencesProvider @Inject constructor(private val context: Context) { +class UserPreferencesProvider @Inject constructor(private val vectorPreferences: VectorPreferences) { fun shouldShowHiddenEvents(): Boolean { - return VectorPreferences.shouldShowHiddenEvents(context) + return vectorPreferences.shouldShowHiddenEvents() } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/resources/VersionCodeProvider.kt b/vector/src/main/java/im/vector/riotx/core/resources/VersionCodeProvider.kt new file mode 100644 index 00000000..e05db170 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/resources/VersionCodeProvider.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.core.resources + +import android.content.Context +import android.os.Build +import androidx.annotation.NonNull +import javax.inject.Inject + +class VersionCodeProvider @Inject constructor(private val context: Context) { + + /** + * Returns the version code, read from the Manifest. It is not the same than BuildConfig.VERSION_CODE due to versionCodeOverride + */ + @NonNull + fun getVersionCode(): Long { + val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + packageInfo.longVersionCode + } else { + @Suppress("DEPRECATION") + packageInfo.versionCode.toLong() + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/utils/DefaultSubscriber.kt b/vector/src/main/java/im/vector/riotx/core/utils/DefaultSubscriber.kt new file mode 100644 index 00000000..05415991 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/DefaultSubscriber.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.core.utils + +import io.reactivex.Completable +import io.reactivex.Single +import io.reactivex.disposables.Disposable +import io.reactivex.functions.Consumer +import io.reactivex.internal.functions.Functions +import timber.log.Timber + +fun Single.subscribeLogError(): Disposable { + return subscribe(Functions.emptyConsumer(), Consumer { Timber.e(it) }) +} + +fun Completable.subscribeLogError(): Disposable { + return subscribe({}, { Timber.e(it) }) +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt index 32e08597..69ad2cd1 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt @@ -43,6 +43,7 @@ class KeysBackupManageActivity : SimpleFragmentActivity() { @Inject lateinit var keysBackupSettingsViewModelFactory: KeysBackupSettingsViewModel.Factory override fun injectWith(injector: ScreenComponent) { + super.injectWith(injector) injector.inject(this) } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SasVerificationViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SasVerificationViewModel.kt index 87a420d3..41c08e68 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SasVerificationViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SasVerificationViewModel.kt @@ -29,7 +29,7 @@ import javax.inject.Inject class SasVerificationViewModel @Inject constructor() : ViewModel(), - SasVerificationService.SasVerificationListener { + SasVerificationService.SasVerificationListener { companion object { @@ -40,7 +40,7 @@ class SasVerificationViewModel @Inject constructor() : ViewModel(), const val NAVIGATE_CANCELLED = "NAVIGATE_CANCELLED" } - lateinit var sasVerificationService: SasVerificationService + private lateinit var sasVerificationService: SasVerificationService var otherUserId: String? = null var otherDeviceId: String? = null @@ -154,8 +154,8 @@ class SasVerificationViewModel @Inject constructor() : ViewModel(), override fun onCleared() { super.onCleared() - sasVerificationService.removeListener(this) + if (::sasVerificationService.isInitialized) { + sasVerificationService.removeListener(this) + } } - - } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt index 83829e46..2b6c1eb4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt @@ -26,10 +26,10 @@ import com.amulyakhare.textdrawable.TextDrawable import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.target.DrawableImageViewTarget import com.bumptech.glide.request.target.Target -import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.internal.util.firstLetterOfDisplayName import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.glide.GlideApp @@ -41,7 +41,7 @@ import javax.inject.Inject * This helper centralise ways to retrieve avatar into ImageView or even generic Target */ -class AvatarRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder){ +class AvatarRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder) { companion object { private const val THUMBNAIL_SIZE = 250 @@ -92,9 +92,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active return if (text.isEmpty()) { TextDrawable.builder().buildRound("", avatarColor) } else { - val isUserId = MatrixPatterns.isUserId(text) - val firstLetterIndex = if (isUserId) 1 else 0 - val firstLetter = text[firstLetterIndex].toString().toUpperCase() + val firstLetter = text.firstLetterOfDisplayName() TextDrawable.builder() .beginConfig() .bold() diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index 4ec2c0ad..64a86b90 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -65,7 +65,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var homeActivityViewModelFactory: HomeActivityViewModel.Factory @Inject lateinit var homeNavigator: HomeNavigator - @Inject lateinit var navigator: Navigator @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @Inject lateinit var pushManager: PushersManager @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @@ -145,7 +144,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - if (intent?.hasExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) == true) { notificationDrawerManager.clearAllEvents() intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) @@ -194,7 +192,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { bugReporter.openBugReportScreen(this, false) return true } - R.id.menu_home_filter -> { + R.id.menu_home_filter -> { navigator.openRoomsFiltering(this) return true } @@ -214,23 +212,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { } } - private fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean { - // if (fm.backStackEntryCount == 0) - // return false - val reverseOrder = fm.fragments.filter { it is OnBackPressed }.reversed() - for (f in reverseOrder) { - val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager) - if (handledByChildFragments) { - return true - } - val backPressable = f as OnBackPressed - if (backPressable.onBackPressed()) { - return true - } - } - return false - } companion object { diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt index 7f0b610d..917cafe1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt @@ -73,21 +73,21 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho .subscribe { list -> list.let { summaries -> val peopleNotifications = summaries - .filter { it.isDirect } - .map { it.notificationCount } - .takeIf { it.isNotEmpty() } - ?.sumBy { i -> i } - ?: 0 + .filter { it.isDirect } + .map { it.notificationCount } + .takeIf { it.isNotEmpty() } + ?.sumBy { i -> i } + ?: 0 val peopleHasHighlight = summaries .filter { it.isDirect } .any { it.highlightCount > 0 } val roomsNotifications = summaries - .filter { !it.isDirect } - .map { it.notificationCount } - .takeIf { it.isNotEmpty() } - ?.sumBy { i -> i } - ?: 0 + .filter { !it.isDirect } + .map { it.notificationCount } + .takeIf { it.isNotEmpty() } + ?.sumBy { i -> i } + ?: 0 val roomsHasHighlight = summaries .filter { !it.isDirect } .any { it.highlightCount > 0 } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt index ac4cc08d..ad398393 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt @@ -51,8 +51,7 @@ class HomeDrawerFragment : VectorBaseFragment() { val groupListFragment = GroupListFragment.newInstance() replaceChildFragment(groupListFragment, R.id.homeDrawerGroupListContainer) } - - session.observeUser(session.myUserId).observeK(this) { user -> + session.liveUser(session.myUserId).observeK(this) { user -> if (user != null) { avatarRenderer.render(user.avatarUrl, user.userId, user.displayName, homeDrawerHeaderAvatarView) homeDrawerUsernameView.text = user.displayName diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActions.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActions.kt new file mode 100644 index 00000000..50f99a6d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActions.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.createdirect + +import im.vector.matrix.android.api.session.user.model.User + +sealed class CreateDirectRoomActions { + + object CreateRoomAndInviteSelectedUsers : CreateDirectRoomActions() + data class FilterKnownUsers(val value: String) : CreateDirectRoomActions() + data class SearchDirectoryUsers(val value: String) : CreateDirectRoomActions() + object ClearFilterKnownUsers : CreateDirectRoomActions() + data class SelectUser(val user: User) : CreateDirectRoomActions() + data class RemoveSelectedUser(val user: User) : CreateDirectRoomActions() + +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt new file mode 100644 index 00000000..b18892ac --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt @@ -0,0 +1,116 @@ +/* + * + * * Copyright 2019 New Vector Ltd + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package im.vector.riotx.features.home.createdirect + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.ViewModelProviders +import com.airbnb.mvrx.* +import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.extensions.addFragment +import im.vector.riotx.core.extensions.addFragmentToBackstack +import im.vector.riotx.core.extensions.observeEvent +import im.vector.riotx.core.platform.SimpleFragmentActivity +import im.vector.riotx.core.platform.WaitingViewData +import kotlinx.android.synthetic.main.activity.* +import javax.inject.Inject + +class CreateDirectRoomActivity : SimpleFragmentActivity() { + + sealed class Navigation { + object UsersDirectory : Navigation() + object Close : Navigation() + object Previous : Navigation() + } + + private val viewModel: CreateDirectRoomViewModel by viewModel() + lateinit var navigationViewModel: CreateDirectRoomNavigationViewModel + @Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory + @Inject lateinit var errorFormatter: ErrorFormatter + + override fun injectWith(injector: ScreenComponent) { + super.injectWith(injector) + injector.inject(this) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + toolbar.visibility = View.GONE + navigationViewModel = ViewModelProviders.of(this, viewModelFactory).get(CreateDirectRoomNavigationViewModel::class.java) + navigationViewModel.navigateTo.observeEvent(this) { navigation -> + when (navigation) { + is Navigation.UsersDirectory -> addFragmentToBackstack(CreateDirectRoomDirectoryUsersFragment(), R.id.container) + Navigation.Close -> finish() + Navigation.Previous -> onBackPressed() + } + } + if (isFirstCreation()) { + addFragment(CreateDirectRoomKnownUsersFragment(), R.id.container) + } + viewModel.selectSubscribe(this, CreateDirectRoomViewState::createAndInviteState) { + renderCreateAndInviteState(it) + } + } + + private fun renderCreateAndInviteState(state: Async) { + when (state) { + is Loading -> renderCreationLoading() + is Success -> renderCreationSuccess(state()) + is Fail -> renderCreationFailure(state.error) + } + } + + private fun renderCreationLoading() { + updateWaitingView(WaitingViewData(getString(R.string.creating_direct_room))) + } + + private fun renderCreationFailure(error: Throwable) { + hideWaitingView() + if (error is CreateRoomFailure.CreatedWithTimeout) { + finish() + } else + AlertDialog.Builder(this) + .setMessage(errorFormatter.toHumanReadable(error)) + .setPositiveButton(R.string.ok, null) + .show() + } + + private fun renderCreationSuccess(roomId: String?) { + // Navigate to freshly created room + if (roomId != null) { + navigator.openRoom(this, roomId) + } + finish() + } + + + companion object { + fun getIntent(context: Context): Intent { + return Intent(context, CreateDirectRoomActivity::class.java) + } + } + + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt new file mode 100644 index 00000000..3916ff7b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.createdirect + +import android.content.Context +import android.os.Bundle +import android.view.inputmethod.InputMethodManager +import androidx.lifecycle.ViewModelProviders +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.matrix.android.api.session.user.model.User +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.hideKeyboard +import im.vector.riotx.core.extensions.setupAsSearch +import im.vector.riotx.core.platform.VectorBaseFragment +import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.* +import javax.inject.Inject + +class CreateDirectRoomDirectoryUsersFragment : VectorBaseFragment(), DirectoryUsersController.Callback { + + override fun getLayoutResId() = R.layout.fragment_create_direct_room_directory_users + + private val viewModel: CreateDirectRoomViewModel by activityViewModel() + + @Inject lateinit var directRoomController: DirectoryUsersController + private lateinit var navigationViewModel: CreateDirectRoomNavigationViewModel + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + navigationViewModel = ViewModelProviders.of(requireActivity(), viewModelFactory).get(CreateDirectRoomNavigationViewModel::class.java) + setupRecyclerView() + setupSearchByMatrixIdView() + setupCloseView() + } + + private fun setupRecyclerView() { + recyclerView.setHasFixedSize(true) + directRoomController.callback = this + recyclerView.setController(directRoomController) + } + + private fun setupSearchByMatrixIdView() { + createDirectRoomSearchById.setupAsSearch(searchIconRes = 0) + createDirectRoomSearchById + .textChanges() + .subscribe { + viewModel.handle(CreateDirectRoomActions.SearchDirectoryUsers(it.toString())) + } + .disposeOnDestroy() + createDirectRoomSearchById.requestFocus() + val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + imm?.showSoftInput(createDirectRoomSearchById, InputMethodManager.SHOW_IMPLICIT) + + } + + private fun setupCloseView() { + createDirectRoomClose.setOnClickListener { + navigationViewModel.goTo(CreateDirectRoomActivity.Navigation.Previous) + } + } + + override fun invalidate() = withState(viewModel) { + directRoomController.setData(it) + } + + override fun onItemClick(user: User) { + view?.hideKeyboard() + viewModel.handle(CreateDirectRoomActions.SelectUser(user)) + navigationViewModel.goTo(CreateDirectRoomActivity.Navigation.Previous) + } + + override fun retryDirectoryUsersRequest() { + val currentSearch = createDirectRoomSearchById.text.toString() + viewModel.handle(CreateDirectRoomActions.SearchDirectoryUsers(currentSearch)) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomKnownUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomKnownUsersFragment.kt new file mode 100644 index 00000000..77473366 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomKnownUsersFragment.kt @@ -0,0 +1,177 @@ +/* + * + * * Copyright 2019 New Vector Ltd + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package im.vector.riotx.features.home.createdirect + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.widget.ScrollView +import androidx.core.view.size +import androidx.lifecycle.ViewModelProviders +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState +import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipGroup +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.matrix.android.api.session.user.model.User +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.hideKeyboard +import im.vector.riotx.core.extensions.observeEvent +import im.vector.riotx.core.extensions.setupAsSearch +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.utils.DimensionUtils +import im.vector.riotx.features.home.AvatarRenderer +import kotlinx.android.synthetic.main.fragment_create_direct_room.* +import javax.inject.Inject + +class CreateDirectRoomKnownUsersFragment : VectorBaseFragment(), KnownUsersController.Callback { + + override fun getLayoutResId() = R.layout.fragment_create_direct_room + + override fun getMenuRes() = R.menu.vector_create_direct_room + + private val viewModel: CreateDirectRoomViewModel by activityViewModel() + + @Inject lateinit var directRoomController: KnownUsersController + @Inject lateinit var avatarRenderer: AvatarRenderer + private lateinit var navigationViewModel: CreateDirectRoomNavigationViewModel + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + navigationViewModel = ViewModelProviders.of(requireActivity(), viewModelFactory).get(CreateDirectRoomNavigationViewModel::class.java) + vectorBaseActivity.setSupportActionBar(createDirectRoomToolbar) + setupRecyclerView() + setupFilterView() + setupAddByMatrixIdView() + setupCloseView() + viewModel.selectUserEvent.observeEvent(this) { + updateChipsView(it) + } + viewModel.selectSubscribe(this, CreateDirectRoomViewState::selectedUsers) { + renderSelectedUsers(it) + } + } + + override fun onPrepareOptionsMenu(menu: Menu) { + withState(viewModel) { + val createMenuItem = menu.findItem(R.id.action_create_direct_room) + val showMenuItem = it.selectedUsers.isNotEmpty() + createMenuItem.setVisible(showMenuItem) + } + super.onPrepareOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_create_direct_room -> { + viewModel.handle(CreateDirectRoomActions.CreateRoomAndInviteSelectedUsers) + true + } + else -> + super.onOptionsItemSelected(item) + } + } + + private fun setupAddByMatrixIdView() { + addByMatrixId.setOnClickListener { + navigationViewModel.goTo(CreateDirectRoomActivity.Navigation.UsersDirectory) + } + } + + private fun setupRecyclerView() { + recyclerView.setHasFixedSize(true) + // Don't activate animation as we might have way to much item animation when filtering + recyclerView.itemAnimator = null + directRoomController.callback = this + recyclerView.setController(directRoomController) + } + + private fun setupFilterView() { + createDirectRoomFilter + .textChanges() + .startWith(createDirectRoomFilter.text) + .subscribe { text -> + val filterValue = text.trim() + val action = if (filterValue.isBlank()) { + CreateDirectRoomActions.ClearFilterKnownUsers + } else { + CreateDirectRoomActions.FilterKnownUsers(filterValue.toString()) + } + viewModel.handle(action) + } + .disposeOnDestroy() + + createDirectRoomFilter.setupAsSearch() + createDirectRoomFilter.requestFocus() + } + + private fun setupCloseView() { + createDirectRoomClose.setOnClickListener { + requireActivity().finish() + } + } + + override fun invalidate() = withState(viewModel) { + directRoomController.setData(it) + } + + private fun updateChipsView(data: SelectUserAction) { + if (data.isAdded) { + addChipToGroup(data.user, chipGroup) + } else { + if (chipGroup.size > data.index) { + chipGroup.removeViewAt(data.index) + } + } + } + + private fun renderSelectedUsers(selectedUsers: Set) { + vectorBaseActivity.invalidateOptionsMenu() + if (selectedUsers.isNotEmpty() && chipGroup.size == 0) { + selectedUsers.forEach { addChipToGroup(it, chipGroup) } + } + } + + private fun addChipToGroup(user: User, chipGroup: ChipGroup) { + val chip = Chip(requireContext()) + chip.setChipBackgroundColorResource(android.R.color.transparent) + chip.chipStrokeWidth = DimensionUtils.dpToPx(1, requireContext()).toFloat() + chip.text = if (user.displayName.isNullOrBlank()) user.userId else user.displayName + chip.isClickable = true + chip.isCheckable = false + chip.isCloseIconVisible = true + chipGroup.addView(chip) + chip.setOnCloseIconClickListener { + viewModel.handle(CreateDirectRoomActions.RemoveSelectedUser(user)) + } + chipGroupScrollView.post { + chipGroupScrollView.fullScroll(ScrollView.FOCUS_DOWN) + } + } + + override fun onItemClick(user: User) { + view?.hideKeyboard() + viewModel.handle(CreateDirectRoomActions.SelectUser(user)) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomLetterHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomLetterHeaderItem.kt new file mode 100644 index 00000000..fcb3b10c --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomLetterHeaderItem.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.createdirect + +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.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel + +@EpoxyModelClass(layout = R.layout.item_create_direct_room_letter_header) +abstract class CreateDirectRoomLetterHeaderItem : VectorEpoxyModel() { + + @EpoxyAttribute var letter: String = "" + + override fun bind(holder: Holder) { + holder.letterView.text = letter + } + + class Holder : VectorEpoxyHolder() { + val letterView by bind(R.id.createDirectRoomLetterView) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomNavigationViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomNavigationViewModel.kt new file mode 100644 index 00000000..442dc23d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomNavigationViewModel.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.createdirect + +import im.vector.riotx.core.mvrx.NavigationViewModel +import javax.inject.Inject + +class CreateDirectRoomNavigationViewModel @Inject constructor(): NavigationViewModel() \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt new file mode 100644 index 00000000..c6d7f85b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt @@ -0,0 +1,77 @@ +/* + * + * * Copyright 2019 New Vector Ltd + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package im.vector.riotx.features.home.createdirect + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.core.content.ContextCompat +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.amulyakhare.textdrawable.TextDrawable +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.features.home.AvatarRenderer + +@EpoxyModelClass(layout = R.layout.item_create_direct_room_user) +abstract class CreateDirectRoomUserItem : VectorEpoxyModel() { + + @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer + @EpoxyAttribute var name: String? = null + @EpoxyAttribute var userId: String = "" + @EpoxyAttribute var avatarUrl: String? = null + @EpoxyAttribute var clickListener: View.OnClickListener? = null + @EpoxyAttribute var selected: Boolean = false + + + override fun bind(holder: Holder) { + holder.view.setOnClickListener(clickListener) + // If name is empty, use userId as name and force it being centered + if (name.isNullOrEmpty()) { + holder.userIdView.visibility = View.GONE + holder.nameView.text = userId + } else { + holder.userIdView.visibility = View.VISIBLE + holder.nameView.text = name + holder.userIdView.text = userId + } + renderSelection(holder, selected) + } + + private fun renderSelection(holder: Holder, isSelected: Boolean) { + if (isSelected) { + holder.avatarCheckedImageView.visibility = View.VISIBLE + val backgroundColor = ContextCompat.getColor(holder.view.context, R.color.riotx_accent) + val backgroundDrawable = TextDrawable.builder().buildRound("", backgroundColor) + holder.avatarImageView.setImageDrawable(backgroundDrawable) + } else { + holder.avatarCheckedImageView.visibility = View.GONE + avatarRenderer.render(avatarUrl, userId, name, holder.avatarImageView) + } + } + + class Holder : VectorEpoxyHolder() { + val userIdView by bind(R.id.createDirectRoomUserID) + val nameView by bind(R.id.createDirectRoomUserName) + val avatarImageView by bind(R.id.createDirectRoomUserAvatar) + val avatarCheckedImageView by bind(R.id.createDirectRoomUserAvatarChecked) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt new file mode 100644 index 00000000..a0d47dc0 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt @@ -0,0 +1,171 @@ +/* + * + * * Copyright 2019 New Vector Ltd + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package im.vector.riotx.features.home.createdirect + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import arrow.core.Option +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.jakewharton.rxrelay2.BehaviorRelay +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams +import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.internal.util.firstLetterOfDisplayName +import im.vector.matrix.rx.rx +import im.vector.riotx.core.extensions.postLiveEvent +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.utils.LiveEvent +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import java.util.concurrent.TimeUnit + +private typealias KnowUsersFilter = String +private typealias DirectoryUsersSearch = String + +data class SelectUserAction( + val user: User, + val isAdded: Boolean, + val index: Int +) + +class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted + initialState: CreateDirectRoomViewState, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: CreateDirectRoomViewState): CreateDirectRoomViewModel + } + + private val knownUsersFilter = BehaviorRelay.createDefault>(Option.empty()) + private val directoryUsersSearch = BehaviorRelay.create() + + private val _selectUserEvent = MutableLiveData>() + val selectUserEvent: LiveData> + get() = _selectUserEvent + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: CreateDirectRoomViewState): CreateDirectRoomViewModel? { + val activity: CreateDirectRoomActivity = (viewModelContext as ActivityViewModelContext).activity() + return activity.createDirectRoomViewModelFactory.create(state) + } + } + + init { + observeKnownUsers() + observeDirectoryUsers() + } + + fun handle(action: CreateDirectRoomActions) { + when (action) { + is CreateDirectRoomActions.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers() + is CreateDirectRoomActions.FilterKnownUsers -> knownUsersFilter.accept(Option.just(action.value)) + is CreateDirectRoomActions.ClearFilterKnownUsers -> knownUsersFilter.accept(Option.empty()) + is CreateDirectRoomActions.SearchDirectoryUsers -> directoryUsersSearch.accept(action.value) + is CreateDirectRoomActions.SelectUser -> handleSelectUser(action) + is CreateDirectRoomActions.RemoveSelectedUser -> handleRemoveSelectedUser(action) + } + } + + private fun createRoomAndInviteSelectedUsers() = withState { currentState -> + val isDirect = currentState.selectedUsers.size == 1 + val roomParams = CreateRoomParams().apply { + invitedUserIds = ArrayList(currentState.selectedUsers.map { it.userId }) + if (isDirect) { + setDirectMessage() + } + } + session.rx() + .createRoom(roomParams) + .execute { + copy(createAndInviteState = it) + } + } + + private fun handleRemoveSelectedUser(action: CreateDirectRoomActions.RemoveSelectedUser) = withState { state -> + val index = state.selectedUsers.indexOfFirst { it.userId == action.user.userId } + val selectedUsers = state.selectedUsers.minus(action.user) + setState { copy(selectedUsers = selectedUsers) } + _selectUserEvent.postLiveEvent(SelectUserAction(action.user, false, index)) + } + + private fun handleSelectUser(action: CreateDirectRoomActions.SelectUser) = withState { state -> + //Reset the filter asap + directoryUsersSearch.accept("") + val isAddOperation: Boolean + val selectedUsers: Set + val indexOfUser = state.selectedUsers.indexOfFirst { it.userId == action.user.userId } + val changeIndex: Int + if (indexOfUser == -1) { + changeIndex = state.selectedUsers.size + selectedUsers = state.selectedUsers.plus(action.user) + isAddOperation = true + } else { + changeIndex = indexOfUser + selectedUsers = state.selectedUsers.minus(action.user) + isAddOperation = false + } + setState { copy(selectedUsers = selectedUsers) } + _selectUserEvent.postLiveEvent(SelectUserAction(action.user, isAddOperation, changeIndex)) + } + + private fun observeDirectoryUsers() { + directoryUsersSearch + .debounce(300, TimeUnit.MILLISECONDS) + .switchMapSingle { search -> + val stream = if (search.isBlank()) { + Single.just(emptyList()) + } else { + session.rx() + .searchUsersDirectory(search, 50, emptySet()) + .map { users -> + users.sortedBy { it.displayName.firstLetterOfDisplayName() } + } + } + stream.toAsync { + copy(directoryUsers = it, directorySearchTerm = search) + } + } + .subscribe() + .disposeOnClear() + } + + private fun observeKnownUsers() { + knownUsersFilter + .throttleLast(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .switchMap { + session.rx().livePagedUsers(it.orNull()) + } + .execute { async -> + copy( + knownUsers = async, + filterKnownUsersValue = knownUsersFilter.value ?: Option.empty() + ) + } + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewState.kt new file mode 100644 index 00000000..e1c9ad46 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewState.kt @@ -0,0 +1,42 @@ +/* + * + * * Copyright 2019 New Vector Ltd + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package im.vector.riotx.features.home.createdirect + +import androidx.paging.PagedList +import arrow.core.Option +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.user.model.User + +data class CreateDirectRoomViewState( + val knownUsers: Async> = Uninitialized, + val directoryUsers: Async> = Uninitialized, + val selectedUsers: Set = emptySet(), + val createAndInviteState: Async = Uninitialized, + val directorySearchTerm: String = "", + val filterKnownUsersValue: Option = Option.empty() +) : MvRxState { + + enum class DisplayMode { + KNOWN_USERS, + DIRECTORY_USERS + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/DirectoryUsersController.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/DirectoryUsersController.kt new file mode 100644 index 00000000..c174ac6b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/DirectoryUsersController.kt @@ -0,0 +1,127 @@ +/* + * + * * Copyright 2019 New Vector Ltd + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package im.vector.riotx.features.home.createdirect + +import com.airbnb.epoxy.EpoxyController +import com.airbnb.mvrx.* +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.internal.util.firstLetterOfDisplayName +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.errorWithRetryItem +import im.vector.riotx.core.epoxy.loadingItem +import im.vector.riotx.core.epoxy.noResultItem +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.home.AvatarRenderer +import javax.inject.Inject + +class DirectoryUsersController @Inject constructor(private val session: Session, + private val avatarRenderer: AvatarRenderer, + private val stringProvider: StringProvider, + private val errorFormatter: ErrorFormatter) : EpoxyController() { + + private var state: CreateDirectRoomViewState? = null + + var callback: Callback? = null + + init { + requestModelBuild() + } + + fun setData(state: CreateDirectRoomViewState) { + this.state = state + requestModelBuild() + } + + + override fun buildModels() { + val currentState = state ?: return + val hasSearch = currentState.directorySearchTerm.isNotBlank() + val asyncUsers = currentState.directoryUsers + when (asyncUsers) { + is Uninitialized -> renderEmptyState(false) + is Loading -> renderLoading() + is Success -> renderSuccess(asyncUsers(), currentState.selectedUsers.map { it.userId }, hasSearch) + is Fail -> renderFailure(asyncUsers.error) + } + } + + private fun renderLoading() { + loadingItem { + id("loading") + } + } + + private fun renderFailure(failure: Throwable) { + errorWithRetryItem { + id("error") + text(errorFormatter.toHumanReadable(failure)) + listener { callback?.retryDirectoryUsersRequest() } + } + } + + private fun renderSuccess(users: List, + selectedUsers: List, + hasSearch: Boolean) { + if (users.isEmpty()) { + renderEmptyState(hasSearch) + } else { + renderUsers(users, selectedUsers) + } + } + + private fun renderUsers(users: List, selectedUsers: List) { + for (user in users) { + if (user.userId == session.myUserId) { + continue + } + val isSelected = selectedUsers.contains(user.userId) + createDirectRoomUserItem { + id(user.userId) + selected(isSelected) + userId(user.userId) + name(user.displayName) + avatarUrl(user.avatarUrl) + avatarRenderer(avatarRenderer) + clickListener { _ -> + callback?.onItemClick(user) + } + } + } + } + + private fun renderEmptyState(hasSearch: Boolean) { + val noResultRes = if (hasSearch) { + R.string.no_result_placeholder + } else { + R.string.direct_room_start_search + } + noResultItem { + id("noResult") + text(stringProvider.getString(noResultRes)) + } + } + + interface Callback { + fun onItemClick(user: User) + fun retryDirectoryUsersRequest() + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt new file mode 100644 index 00000000..fbb1cfcc --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.createdirect + +import com.airbnb.epoxy.EpoxyModel +import com.airbnb.epoxy.paging.PagedListEpoxyController +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Incomplete +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.internal.util.createUIHandler +import im.vector.matrix.android.internal.util.firstLetterOfDisplayName +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.EmptyItem_ +import im.vector.riotx.core.epoxy.errorWithRetryItem +import im.vector.riotx.core.epoxy.loadingItem +import im.vector.riotx.core.epoxy.noResultItem +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.home.AvatarRenderer +import javax.inject.Inject + +class KnownUsersController @Inject constructor(private val session: Session, + private val avatarRenderer: AvatarRenderer, + private val stringProvider: StringProvider) : PagedListEpoxyController( + modelBuildingHandler = createUIHandler() +) { + + private var selectedUsers: List = emptyList() + private var users: Async> = Uninitialized + private var isFiltering: Boolean = false + + var callback: Callback? = null + + init { + requestModelBuild() + } + + fun setData(state: CreateDirectRoomViewState) { + this.isFiltering = !state.filterKnownUsersValue.isEmpty() + val newSelection = state.selectedUsers.map { it.userId } + this.users = state.knownUsers + if (newSelection != selectedUsers) { + this.selectedUsers = newSelection + requestForcedModelBuild() + } + submitList(state.knownUsers()) + } + + override fun buildItemModel(currentPosition: Int, item: User?): EpoxyModel<*> { + return if (item == null) { + EmptyItem_().id(currentPosition) + } else { + val isSelected = selectedUsers.contains(item.userId) + CreateDirectRoomUserItem_() + .id(item.userId) + .selected(isSelected) + .userId(item.userId) + .name(item.displayName) + .avatarUrl(item.avatarUrl) + .avatarRenderer(avatarRenderer) + .clickListener { _ -> + callback?.onItemClick(item) + } + } + } + + override fun addModels(models: List>) { + if (users is Incomplete) { + renderLoading() + } else if (models.isEmpty()) { + renderEmptyState() + } else { + var lastFirstLetter: String? = null + for (model in models) { + if (model is CreateDirectRoomUserItem) { + if (model.userId == session.myUserId) continue + val currentFirstLetter = model.name.firstLetterOfDisplayName() + val showLetter = !isFiltering && currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter + lastFirstLetter = currentFirstLetter + + CreateDirectRoomLetterHeaderItem_() + .id(currentFirstLetter) + .letter(currentFirstLetter) + .addIf(showLetter, this) + + model.addTo(this) + } else { + continue + } + } + } + } + + private fun renderLoading() { + loadingItem { + id("loading") + } + } + + private fun renderEmptyState() { + noResultItem { + id("noResult") + text(stringProvider.getString(R.string.direct_room_no_known_users)) + } + } + + interface Callback { + fun onItemClick(user: User) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt index 513379bd..7aff4a32 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt @@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.rx.rx import im.vector.riotx.R +import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.utils.LiveEvent @@ -67,7 +68,7 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro private fun observeSelectionState() { selectSubscribe(GroupListViewState::selectedGroup) { if (it != null) { - _openGroupLiveData.postValue(LiveEvent(it)) + _openGroupLiveData.postLiveEvent(it) val optionGroup = Option.fromNullable(it) selectedGroupHolder.post(optionGroup) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt index ace0802e..e60bc422 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.home.room.detail import com.jaiselrahman.filepicker.model.MediaFile +import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.timeline.Timeline @@ -27,19 +28,24 @@ sealed class RoomDetailActions { data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions() data class SendMedia(val mediaFiles: List) : RoomDetailActions() data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions() - data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions() + data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions() data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions() data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions() data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions() data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions() data class NavigateToEvent(val eventId: String, val position: Int?) : RoomDetailActions() data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailActions() + data class HandleTombstoneEvent(val event: Event): RoomDetailActions() object AcceptInvite : RoomDetailActions() object RejectInvite : RoomDetailActions() data class EnterEditMode(val eventId: String) : RoomDetailActions() data class EnterQuoteMode(val eventId: String) : RoomDetailActions() data class EnterReplyMode(val eventId: String) : RoomDetailActions() + data class ResendMessage(val eventId: String) : RoomDetailActions() + data class RemoveFailedEcho(val eventId: String) : RoomDetailActions() + object ClearSendQueue : RoomDetailActions() + object ResendAll : RoomDetailActions() } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt index 6ad9a61f..e1731c27 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt @@ -19,11 +19,17 @@ package im.vector.riotx.features.home.room.detail import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import com.airbnb.mvrx.activityViewModel import androidx.appcompat.widget.Toolbar +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProviders import im.vector.riotx.R import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity +import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable { @@ -33,6 +39,7 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + waitingView = waiting_view if (isFirstCreation()) { val roomDetailArgs: RoomDetailArgs = intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS) ?: return diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index b7491ae6..70793bec 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -22,20 +22,23 @@ import android.content.Context import android.content.Intent import android.graphics.drawable.ColorDrawable import android.net.Uri +import android.os.Build import android.os.Bundle import android.os.Parcelable import android.text.Editable import android.text.Spannable import android.text.TextUtils -import android.view.HapticFeedbackConstants -import android.view.LayoutInflater -import android.view.View +import android.view.* import android.view.inputmethod.InputMethodManager import android.widget.TextView import android.widget.Toast import androidx.annotation.DrawableRes import androidx.appcompat.app.AlertDialog +import androidx.core.app.ActivityOptionsCompat import androidx.core.content.ContextCompat +import androidx.core.util.Pair +import androidx.core.view.ViewCompat +import androidx.core.view.forEach import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager @@ -43,8 +46,7 @@ import androidx.recyclerview.widget.RecyclerView import butterknife.BindView import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyVisibilityTracker -import com.airbnb.mvrx.args -import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.* import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.loader.ImageLoader import com.google.android.material.snackbar.Snackbar @@ -56,13 +58,14 @@ import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.CharPolicy import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.Session -import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary +import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent +import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent @@ -74,6 +77,7 @@ import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.glide.GlideApp +import im.vector.riotx.core.platform.NotificationAreaView import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.utils.* import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter @@ -106,6 +110,7 @@ import im.vector.riotx.features.themes.ThemeUtils import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_room_detail.* import kotlinx.android.synthetic.main.merge_composer_layout.view.* +import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import org.commonmark.parser.Parser import timber.log.Timber import java.io.File @@ -180,10 +185,13 @@ class RoomDetailFragment : private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback @Inject lateinit var eventHtmlRenderer: EventHtmlRenderer + @Inject lateinit var vectorPreferences: VectorPreferences override fun getLayoutResId() = R.layout.fragment_room_detail + override fun getMenuRes() = R.menu.menu_timeline + private lateinit var actionViewModel: ActionsHandler @BindView(R.id.composerLayout) @@ -201,6 +209,7 @@ class RoomDetailFragment : setupComposer() setupAttachmentButton() setupInviteView() + setupNotificationView() roomDetailViewModel.subscribe { renderState(it) } textComposerViewModel.subscribe { renderTextComposerState(it) } roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) } @@ -218,6 +227,10 @@ class RoomDetailFragment : scrollOnHighlightedEventCallback.scheduleScrollTo(it) } + roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) { + renderTombstoneEventHandling(it) + } + roomDetailViewModel.downloadedFileEvent.observeEvent(this) { downloadFileState -> if (downloadFileState.throwable != null) { requireActivity().toast(errorFormatter.toHumanReadable(downloadFileState.throwable)) @@ -235,6 +248,60 @@ class RoomDetailFragment : is SendMode.REPLY -> enterSpecialMode(mode.timelineEvent, R.drawable.ic_reply, false) } } + + roomDetailViewModel.selectSubscribe(RoomDetailViewState::syncState) { syncState -> + syncProgressBar.visibility = when (syncState) { + is SyncState.RUNNING -> if (syncState.afterPause) View.VISIBLE else View.GONE + else -> View.GONE + } + syncProgressBarWrap.visibility = syncProgressBar.visibility + } + } + + private fun setupNotificationView() { + notificationAreaView.delegate = object : NotificationAreaView.Delegate { + + override fun onTombstoneEventClicked(tombstoneEvent: Event) { + roomDetailViewModel.process(RoomDetailActions.HandleTombstoneEvent(tombstoneEvent)) + } + + override fun resendUnsentEvents() { + vectorBaseActivity.notImplemented() + } + + override fun deleteUnsentEvents() { + vectorBaseActivity.notImplemented() + } + + override fun closeScreen() { + vectorBaseActivity.notImplemented() + } + + override fun jumpToBottom() { + vectorBaseActivity.notImplemented() + } + } + } + + override fun onPrepareOptionsMenu(menu: Menu) { + menu.forEach { + it.isVisible = roomDetailViewModel.isMenuItemVisible(it.itemId) + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.clear_message_queue) { + //This a temporary option during dev as it is not super stable + //Cancel all pending actions in room queue and post a dummy + //Then mark all sending events as undelivered + roomDetailViewModel.process(RoomDetailActions.ClearSendQueue) + return true + } + if (item.itemId == R.id.resend_all) { + roomDetailViewModel.process(RoomDetailActions.ResendAll) + return true + } + return super.onOptionsItemSelected(item) } private fun exitSpecialMode() { @@ -242,7 +309,9 @@ class RoomDetailFragment : composerLayout.collapse() } - private fun enterSpecialMode(event: TimelineEvent, @DrawableRes iconRes: Int, useText: Boolean) { + private fun enterSpecialMode(event: TimelineEvent, + @DrawableRes iconRes: Int, + useText: Boolean) { commandAutocompletePolicy.enabled = false //switch to expanded bar composerLayout.composerRelatedMessageTitle.apply { @@ -325,12 +394,12 @@ class RoomDetailFragment : recyclerView.addOnScrollListener( EndlessRecyclerViewScrollListener(layoutManager, RoomDetailViewModel.PAGINATION_COUNT) { direction -> - roomDetailViewModel.process(RoomDetailActions.LoadMore(direction)) + roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction)) }) recyclerView.setController(timelineEventController) timelineEventController.callback = this - if (VectorPreferences.swipeToReplyIsEnabled(requireContext())) { + if (vectorPreferences.swipeToReplyIsEnabled()) { val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), R.drawable.ic_reply, object : RoomMessageTouchHelperCallback.QuickReplayHandler { @@ -423,7 +492,7 @@ class RoomDetailFragment : composerLayout.sendButton.setOnClickListener { val textMessage = composerLayout.composerEditText.text.toString() if (textMessage.isNotBlank()) { - roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage, VectorPreferences.isMarkdownEnabled(requireContext()))) + roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage, vectorPreferences.isMarkdownEnabled())) } } composerLayout.composerRelatedMessageCloseButton.setOnClickListener { @@ -448,7 +517,7 @@ class RoomDetailFragment : items.add(DialogListItem.SendFile) // Send voice - if (VectorPreferences.isSendVoiceFeatureEnabled(this)) { + if (vectorPreferences.isSendVoiceFeatureEnabled()) { items.add(DialogListItem.SendVoice.INSTANCE) } @@ -457,7 +526,7 @@ class RoomDetailFragment : //items.add(DialogListItem.SendSticker) // Camera - //if (VectorPreferences.useNativeCamera(this)) { + //if (vectorPreferences.useNativeCamera()) { items.add(DialogListItem.TakePhoto) items.add(DialogListItem.TakeVideo) //} else { @@ -517,7 +586,6 @@ class RoomDetailFragment : if (summary?.membership == Membership.JOIN) { timelineEventController.setTimeline(state.timeline, state.eventId) inviteView.visibility = View.GONE - val uid = session.myUserId val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) avatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView) @@ -531,14 +599,26 @@ class RoomDetailFragment : } else if (state.asyncInviter.complete) { vectorBaseActivity.finish() } - composerLayout.setRoomEncrypted(state.isEncrypted) + if (state.tombstoneEvent == null) { + composerLayout.visibility = View.VISIBLE + composerLayout.setRoomEncrypted(state.isEncrypted) + notificationAreaView.render(NotificationAreaView.State.Hidden) + } else { + composerLayout.visibility = View.GONE + notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent)) + } } private fun renderRoomSummary(state: RoomDetailViewState) { state.asyncRoomSummary()?.let { - roomToolbarTitleView.text = it.displayName - avatarRenderer.render(it, roomToolbarAvatarImageView) - roomToolbarSubtitleView.setTextOrHide(it.topic) + if (it.membership.isLeft()) { + Timber.w("The room has been left") + activity?.finish() + } else { + roomToolbarTitleView.text = it.displayName + avatarRenderer.render(it, roomToolbarAvatarImageView) + roomToolbarSubtitleView.setTextOrHide(it.topic) + } } } @@ -546,10 +626,34 @@ class RoomDetailFragment : autocompleteUserPresenter.render(state.asyncUsers) } + private fun renderTombstoneEventHandling(async: Async) { + when (async) { + is Loading -> { + // TODO Better handling progress + vectorBaseActivity.showWaitingView() + vectorBaseActivity.waiting_view_status_text.visibility = View.VISIBLE + vectorBaseActivity.waiting_view_status_text.text = getString(R.string.joining_room) + } + is Success -> { + navigator.openRoom(vectorBaseActivity, async()) + vectorBaseActivity.finish() + } + is Fail -> { + vectorBaseActivity.hideWaitingView() + vectorBaseActivity.toast(errorFormatter.toHumanReadable(async.error)) + } + } + } + + private fun renderSendMessageResult(sendMessageResult: SendMessageResult) { when (sendMessageResult) { - is SendMessageResult.MessageSent, + is SendMessageResult.MessageSent -> { + // Clear composer + composerLayout.composerEditText.text = null + } is SendMessageResult.SlashCommandHandled -> { + sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } // Clear composer composerLayout.composerEditText.text = null } @@ -579,7 +683,7 @@ class RoomDetailFragment : .show() } - // TimelineEventController.Callback ************************************************************ +// TimelineEventController.Callback ************************************************************ override fun onUrlClicked(url: String): Boolean { return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor { @@ -603,8 +707,10 @@ class RoomDetailFragment : } override fun onUrlLongClicked(url: String): Boolean { - // Copy the url to the clipboard - copyToClipboard(requireContext(), url, true, R.string.link_copied_to_clipboard) + if (url != getString(R.string.edited_suffix)) { + // Copy the url to the clipboard + copyToClipboard(requireContext(), url, true, R.string.link_copied_to_clipboard) + } return true } @@ -618,8 +724,24 @@ class RoomDetailFragment : override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) { // TODO Use navigator - val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData) - startActivity(intent) + + val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData, ViewCompat.getTransitionName(view)) + val pairs = ArrayList>() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + requireActivity().window.decorView.findViewById(android.R.id.statusBarBackground)?.let { + pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) + } + requireActivity().window.decorView.findViewById(android.R.id.navigationBarBackground)?.let { + pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) + } + } + pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: "")) + pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: "")) + pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: "")) + + val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation( + requireActivity(), *pairs.toTypedArray()).toBundle() + startActivity(intent, bundle) } override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) { @@ -694,119 +816,135 @@ class RoomDetailFragment : .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") } - override fun onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?) { + override fun onEditedDecorationClicked(informationData: MessageInformationData) { ViewEditHistoryBottomSheet.newInstance(roomDetailArgs.roomId, informationData) .show(requireActivity().supportFragmentManager, "DISPLAY_EDITS") } + + override fun onRoomCreateLinkClicked(url: String) { + permalinkHandler.launch(requireContext(), url, object : NavigateToRoomInterceptor { + override fun navToRoom(roomId: String, eventId: String?): Boolean { + requireActivity().finish() + return false + } + }) + } + // AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { textComposerViewModel.process(TextComposerActions.QueryUsers(query)) } - private fun handleActions(actionData: ActionsHandler.ActionData) { - when (actionData.actionId) { - MessageMenuViewModel.ACTION_ADD_REACTION -> { - val eventId = actionData.data?.toString() ?: return - startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), eventId), REACTION_SELECT_REQUEST_CODE) + private fun handleActions(action: SimpleAction) { + when (action) { + is SimpleAction.AddReaction -> { + startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE) } - MessageMenuViewModel.ACTION_VIEW_REACTIONS -> { - val messageInformationData = actionData.data as? MessageInformationData - ?: return - ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, messageInformationData) + is SimpleAction.ViewReactions -> { + ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData) .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") } - MessageMenuViewModel.ACTION_COPY -> { + is SimpleAction.Copy -> { //I need info about the current selected message :/ - copyToClipboard(requireContext(), actionData.data?.toString() ?: "", false) + copyToClipboard(requireContext(), action.content, false) val msg = requireContext().getString(R.string.copied_to_clipboard) showSnackWithMessage(msg, Snackbar.LENGTH_SHORT) } - MessageMenuViewModel.ACTION_DELETE -> { - val eventId = actionData.data?.toString() ?: return - roomDetailViewModel.process(RoomDetailActions.RedactAction(eventId, context?.getString(R.string.event_redacted_by_user_reason))) + is SimpleAction.Delete -> { + roomDetailViewModel.process(RoomDetailActions.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason))) } - MessageMenuViewModel.ACTION_SHARE -> { + is SimpleAction.Share -> { //TODO current data communication is too limited //Need to now the media type - actionData.data?.toString()?.let { - //TODO bad, just POC - BigImageViewer.imageLoader().loadImage( - actionData.hashCode(), - Uri.parse(it), - object : ImageLoader.Callback { - override fun onFinish() {} - - override fun onSuccess(image: File?) { - if (image != null) - shareMedia(requireContext(), image, "image/*") - } - - override fun onFail(error: Exception?) {} - - override fun onCacheHit(imageType: Int, image: File?) {} - - override fun onCacheMiss(imageType: Int, image: File?) {} - - override fun onProgress(progress: Int) {} - - override fun onStart() {} + //TODO bad, just POC + BigImageViewer.imageLoader().loadImage( + action.hashCode(), + Uri.parse(action.imageUrl), + object : ImageLoader.Callback { + override fun onFinish() {} + override fun onSuccess(image: File?) { + if (image != null) + shareMedia(requireContext(), image, "image/*") } - ) - } + override fun onFail(error: Exception?) {} + + override fun onCacheHit(imageType: Int, image: File?) {} + + override fun onCacheMiss(imageType: Int, image: File?) {} + + override fun onProgress(progress: Int) {} + + override fun onStart() {} + + } + ) } - MessageMenuViewModel.VIEW_SOURCE, - MessageMenuViewModel.VIEW_DECRYPTED_SOURCE -> { + is SimpleAction.ViewEditHistory -> { + onEditedDecorationClicked(action.messageInformationData) + } + is SimpleAction.ViewSource -> { val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) view.findViewById(R.id.event_content_text_view)?.let { - it.text = actionData.data?.toString() ?: "" + it.text = action.content } AlertDialog.Builder(requireActivity()) .setView(view) - .setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() } + .setPositiveButton(R.string.ok, null) .show() } - MessageMenuViewModel.ACTION_QUICK_REACT -> { - //eventId,ClickedOn,Add - (actionData.data as? Triple)?.let { (eventId, clickedOn, add) -> - roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(eventId, clickedOn, add)) + is SimpleAction.ViewDecryptedSource -> { + val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) + view.findViewById(R.id.event_content_text_view)?.let { + it.text = action.content } + + AlertDialog.Builder(requireActivity()) + .setView(view) + .setPositiveButton(R.string.ok, null) + .show() } - MessageMenuViewModel.ACTION_EDIT -> { - val eventId = actionData.data.toString() - roomDetailViewModel.process(RoomDetailActions.EnterEditMode(eventId)) + is SimpleAction.QuickReact -> { + //eventId,ClickedOn,Add + roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add)) } - MessageMenuViewModel.ACTION_QUOTE -> { - val eventId = actionData.data.toString() - roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(eventId)) + is SimpleAction.Edit -> { + roomDetailViewModel.process(RoomDetailActions.EnterEditMode(action.eventId)) } - MessageMenuViewModel.ACTION_REPLY -> { - val eventId = actionData.data.toString() - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) + is SimpleAction.Quote -> { + roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(action.eventId)) } - MessageMenuViewModel.ACTION_COPY_PERMALINK -> { - val eventId = actionData.data.toString() - val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, eventId) + is SimpleAction.Reply -> { + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(action.eventId)) + } + is SimpleAction.CopyPermalink -> { + val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId) copyToClipboard(requireContext(), permalink, false) showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT) } - else -> { - Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show() + is SimpleAction.Resend -> { + roomDetailViewModel.process(RoomDetailActions.ResendMessage(action.eventId)) + } + is SimpleAction.Remove -> { + roomDetailViewModel.process(RoomDetailActions.RemoveFailedEcho(action.eventId)) + } + else -> { + Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show() } } } - //utils +//utils /** * Insert an user displayname in the message editor. * * @param text the text to insert. */ - //TODO legacy, refactor +//TODO legacy, refactor private fun insertUserDisplayNameInTextEditor(text: String?) { //TODO move logic outside of fragment if (null != text) { @@ -835,7 +973,7 @@ class RoomDetailFragment : // vibrate = true } -// if (vibrate && VectorPreferences.vibrateWhenMentioning(context)) { +// if (vibrate && vectorPreferences.vibrateWhenMentioning()) { // val v= context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator // if (v?.hasVibrator() == true) { // v.vibrate(100) @@ -857,7 +995,7 @@ class RoomDetailFragment : snack.show() } - // VectorInviteView.Callback +// VectorInviteView.Callback override fun onAcceptInvite() { notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) 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 d38561fa..ec372a4d 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 @@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail import android.net.Uri import android.text.TextUtils +import androidx.annotation.IdRes import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.airbnb.mvrx.FragmentViewModelContext @@ -28,25 +29,34 @@ import com.jakewharton.rxrelay2.BehaviorRelay 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.MatrixPatterns import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.content.ContentAttachmentData +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.isImageMessage +import im.vector.matrix.android.api.session.events.model.isTextMessage import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.getFileUrl +import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.rx.rx +import im.vector.riotx.R +import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.intent.getFilenameFromUri import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.resources.UserPreferencesProvider import im.vector.riotx.core.utils.LiveEvent +import im.vector.riotx.core.utils.subscribeLogError import im.vector.riotx.features.command.CommandParser import im.vector.riotx.features.command.ParsedCommand import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents +import im.vector.riotx.features.settings.VectorPreferences import io.reactivex.rxkotlin.subscribeBy import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer @@ -57,6 +67,7 @@ import java.util.concurrent.TimeUnit class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState, userPreferencesProvider: UserPreferencesProvider, + private val vectorPreferences: VectorPreferences, private val session: Session ) : VectorViewModel(initialState) { @@ -92,10 +103,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } init { + observeSyncState() observeRoomSummary() observeEventDisplayedActions() - observeInvitationState() - cancelableBag += room.loadRoomMembersIfNeeded() + observeSummaryState() + room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() timeline.start() setState { copy(timeline = this@RoomDetailViewModel.timeline) } } @@ -105,7 +117,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is RoomDetailActions.SendMessage -> handleSendMessage(action) is RoomDetailActions.SendMedia -> handleSendMedia(action) is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action) - is RoomDetailActions.LoadMore -> handleLoadMore(action) + is RoomDetailActions.LoadMoreTimelineEvents -> handleLoadMore(action) is RoomDetailActions.SendReaction -> handleSendReaction(action) is RoomDetailActions.AcceptInvite -> handleAcceptInvite() is RoomDetailActions.RejectInvite -> handleRejectInvite() @@ -117,10 +129,41 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is RoomDetailActions.EnterReplyMode -> handleReplyAction(action) is RoomDetailActions.DownloadFile -> handleDownloadFile(action) is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action) + is RoomDetailActions.HandleTombstoneEvent -> handleTombstoneEvent(action) + is RoomDetailActions.ResendMessage -> handleResendEvent(action) + is RoomDetailActions.RemoveFailedEcho -> handleRemove(action) + is RoomDetailActions.ClearSendQueue -> handleClearSendQueue() + is RoomDetailActions.ResendAll -> handleResendAll() else -> Timber.e("Unhandled Action: $action") } } + private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) { + val tombstoneContent = action.event.getClearContent().toModel() + ?: return + + val roomId = tombstoneContent.replacementRoom ?: "" + val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN + if (isRoomJoined) { + setState { copy(tombstoneEventHandling = Success(roomId)) } + } else { + val viaServer = MatrixPatterns.extractServerNameFromId(action.event.senderId).let { + if (it.isNullOrBlank()) { + emptyList() + } else { + listOf(it) + } + } + session.rx() + .joinRoom(roomId, viaServer) + .map { roomId } + .execute { + copy(tombstoneEventHandling = it) + } + } + + } + private fun enterEditMode(event: TimelineEvent) { setState { copy( @@ -141,7 +184,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro val nonBlockingPopAlert: LiveData>>> get() = _nonBlockingPopAlert - private val _sendMessageResultLiveData = MutableLiveData>() val sendMessageResultLiveData: LiveData> get() = _sendMessageResultLiveData @@ -155,6 +197,20 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro get() = _downloadedFileEvent + fun isMenuItemVisible(@IdRes itemId: Int): Boolean { + if (itemId == R.id.clear_message_queue) { + //For now always disable, woker cancellation is not working properly + return false//timeline.pendingEventCount() > 0 + } + if (itemId == R.id.resend_all) { + return timeline.failedToDeliverEventCount() > 0 + } + if (itemId == R.id.clear_all) { + return timeline.failedToDeliverEventCount() > 0 + } + return false + } + // PRIVATE METHODS ***************************************************************************** private fun handleSendMessage(action: RoomDetailActions.SendMessage) { @@ -167,62 +223,63 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is ParsedCommand.ErrorNotACommand -> { // Send the text message to the room room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) } is ParsedCommand.ErrorSyntax -> { - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command))) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command)) } is ParsedCommand.ErrorEmptySlashCommand -> { - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown("/"))) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandUnknown("/")) } is ParsedCommand.ErrorUnknownSlashCommand -> { - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand))) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand)) } is ParsedCommand.Invite -> { handleInviteSlashCommand(slashCommandResult) } is ParsedCommand.SetUserPowerLevel -> { // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) } is ParsedCommand.ClearScalarToken -> { // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) } is ParsedCommand.SetMarkdown -> { - // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + vectorPreferences.setMarkdownEnabled(slashCommandResult.enable) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled( + if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled)) } is ParsedCommand.UnbanUser -> { // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) } is ParsedCommand.BanUser -> { // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) } is ParsedCommand.KickUser -> { // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) } is ParsedCommand.JoinRoom -> { // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) } is ParsedCommand.PartRoom -> { // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) } is ParsedCommand.SendEmote -> { room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE) - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) } is ParsedCommand.ChangeTopic -> { handleChangeTopicSlashCommand(slashCommandResult) } is ParsedCommand.ChangeDisplayName -> { // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) } } } @@ -254,7 +311,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro sendMode = SendMode.REGULAR ) } - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) } is SendMode.QUOTE -> { val messageContent: MessageContent? = @@ -279,7 +336,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro sendMode = SendMode.REGULAR ) } - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) } is SendMode.REPLY -> { state.sendMode.timelineEvent.let { @@ -289,14 +346,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro sendMode = SendMode.REGULAR ) } - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) } } } } - // Handle slash command - } private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { @@ -318,29 +373,29 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) { - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) room.updateTopic(changeTopic.topic, object : MatrixCallback { override fun onSuccess(data: Unit) { - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultOk)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultOk) } override fun onFailure(failure: Throwable) { - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultError(failure))) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultError(failure)) } }) } private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) { - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) room.invite(invite.userId, object : MatrixCallback { override fun onSuccess(data: Unit) { - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultOk)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultOk) } override fun onFailure(failure: Throwable) { - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultError(failure))) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultError(failure)) } }) } @@ -388,7 +443,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) { - if (action.event.sendState.isSent()) { //ignore pending/local events + if (action.event.root.sendState.isSent()) { //ignore pending/local events displayedEventsObservable.accept(action) } //We need to update this with the related m.replace also (to move read receipt) @@ -399,7 +454,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - private fun handleLoadMore(action: RoomDetailActions.LoadMore) { + private fun handleLoadMore(action: RoomDetailActions.LoadMoreTimelineEvents) { timeline.paginate(action.direction, PAGINATION_COUNT) } @@ -408,7 +463,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun handleAcceptInvite() { - room.join(object : MatrixCallback {}) + room.join(callback = object : MatrixCallback {}) } private fun handleEditAction(action: RoomDetailActions.EnterEditMode) { @@ -452,19 +507,19 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(), object : MatrixCallback { override fun onSuccess(data: File) { - _downloadedFileEvent.postValue(LiveEvent(DownloadFileState( + _downloadedFileEvent.postLiveEvent(DownloadFileState( action.messageFileContent.getMimeType(), data, null - ))) + )) } override fun onFailure(failure: Throwable) { - _downloadedFileEvent.postValue(LiveEvent(DownloadFileState( + _downloadedFileEvent.postLiveEvent(DownloadFileState( action.messageFileContent.getMimeType(), null, failure - ))) + )) } }) @@ -493,7 +548,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - _navigateToEvent.postValue(LiveEvent(targetEventId)) + _navigateToEvent.postLiveEvent(targetEventId) } else { // change timeline timeline.dispose() @@ -518,10 +573,50 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - _navigateToEvent.postValue(LiveEvent(targetEventId)) + _navigateToEvent.postLiveEvent(targetEventId) } } + private fun handleResendEvent(action: RoomDetailActions.ResendMessage) { + val targetEventId = action.eventId + room.getTimeLineEvent(targetEventId)?.let { + //State must be UNDELIVERED or Failed + if (!it.root.sendState.hasFailed()) { + Timber.e("Cannot resend message, it is not failed, Cancel first") + return + } + if (it.root.isTextMessage()) { + room.resendTextMessage(it) + } else if (it.root.isImageMessage()) { + room.resendMediaMessage(it) + } else { + //TODO + } + } + + } + + private fun handleRemove(action: RoomDetailActions.RemoveFailedEcho) { + val targetEventId = action.eventId + room.getTimeLineEvent(targetEventId)?.let { + //State must be UNDELIVERED or Failed + if (!it.root.sendState.hasFailed()) { + Timber.e("Cannot resend message, it is not failed, Cancel first") + return + } + room.deleteFailedEcho(it) + } + } + + private fun handleClearSendQueue() { + room.clearSendingQueue() + } + + private fun handleResendAll() { + room.resendAllFailedMessages() + } + + private fun observeEventDisplayedActions() { // We are buffering scroll events for one second // and keep the most recent one to set the read receipt on. @@ -537,6 +632,17 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro .disposeOnClear() } + private fun observeSyncState() { + session.rx() + .liveSyncState() + .subscribe { syncState -> + setState { + copy(syncState = syncState) + } + } + .disposeOnClear() + } + private fun observeRoomSummary() { room.rx().liveRoomSummary() .execute { async -> @@ -547,7 +653,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - private fun observeInvitationState() { + private fun observeSummaryState() { asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary -> if (summary.membership == Membership.INVITE) { summary.latestEvent?.root?.senderId?.let { senderId -> @@ -556,6 +662,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro setState { copy(asyncInviter = Success(it)) } } } + room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE)?.also { + setState { copy(tombstoneEvent = it) } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index 63171491..d8358efe 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -19,9 +19,11 @@ package im.vector.riotx.features.home.room.detail import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.RoomSummary 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.sync.SyncState import im.vector.matrix.android.api.session.user.model.User /** @@ -46,7 +48,10 @@ data class RoomDetailViewState( val asyncInviter: Async = Uninitialized, val asyncRoomSummary: Async = Uninitialized, val sendMode: SendMode = SendMode.REGULAR, - val isEncrypted: Boolean = false + val isEncrypted: Boolean = false, + val tombstoneEvent: Event? = null, + val tombstoneEventHandling: Async = Uninitialized, + val syncState: SyncState = SyncState.IDLE ) : MvRxState { constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomMessageTouchHelperCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomMessageTouchHelperCallback.kt index cb283511..d30bad2f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomMessageTouchHelperCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomMessageTouchHelperCallback.kt @@ -130,8 +130,7 @@ class RoomMessageTouchHelperCallback(private val context: Context, private fun drawReplyButton(canvas: Canvas, itemView: View) { - - Timber.v("drawReplyButton") + //Timber.v("drawReplyButton") val translationX = Math.abs(itemView.translationX) val newTime = System.currentTimeMillis() val dt = Math.min(17, newTime - lastReplyButtonAnimationTime) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/SendMessageResult.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/SendMessageResult.kt index ae7a4706..c64b1fbd 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/SendMessageResult.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/SendMessageResult.kt @@ -16,13 +16,14 @@ package im.vector.riotx.features.home.room.detail +import androidx.annotation.StringRes import im.vector.riotx.features.command.Command sealed class SendMessageResult { object MessageSent : SendMessageResult() class SlashCommandError(val command: Command) : SendMessageResult() class SlashCommandUnknown(val command: String) : SendMessageResult() - object SlashCommandHandled : SendMessageResult() + data class SlashCommandHandled(@StringRes val messageRes: Int? = null) : SendMessageResult() object SlashCommandResultOk : SendMessageResult() class SlashCommandResultError(val throwable: Throwable) : SendMessageResult() // TODO Remove 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 0b78f815..e8956309 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 @@ -24,7 +24,6 @@ import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyModel -import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent @@ -54,12 +53,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback, UrlClickCallback { fun onEventVisible(event: TimelineEvent) + fun onRoomCreateLinkClicked(url: String) fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) - fun onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?) + fun onEditedDecorationClicked(informationData: MessageInformationData) } interface ReactionPillCallback { @@ -293,8 +293,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim // 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 initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) ?: true val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState } if (isCollapsed) { collapsedEventIds.addAll(mergedEventIds) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ActionsHandler.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ActionsHandler.kt index 6ad47bfe..ddc8a543 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ActionsHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ActionsHandler.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.home.room.detail.timeline.action import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.utils.LiveEvent import javax.inject.Inject @@ -25,15 +26,10 @@ import javax.inject.Inject */ class ActionsHandler @Inject constructor() : ViewModel() { - data class ActionData( - val actionId: String, - val data: Any? - ) + val actionCommandEvent = MutableLiveData>() - val actionCommandEvent = MutableLiveData>() - - fun fireAction(actionId: String, data: Any? = null) { - actionCommandEvent.value = LiveEvent(ActionData(actionId,data)) + fun fireAction(action: SimpleAction) { + actionCommandEvent.postLiveEvent(action) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt index f8f5fe3e..a80a3454 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt @@ -89,7 +89,7 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() { } menuActionFragment.interactionListener = object : MessageMenuFragment.InteractionListener { override fun didSelectMenuAction(simpleAction: SimpleAction) { - actionHandlerModel.fireAction(simpleAction.uid, simpleAction.data) + actionHandlerModel.fireAction(simpleAction) dismiss() } } @@ -105,7 +105,7 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() { quickReactionFragment.interactionListener = object : QuickReactionFragment.InteractionListener { override fun didQuickReactWith(clickedOn: String, add: Boolean, eventId: String) { - actionHandlerModel.fireAction(MessageMenuViewModel.ACTION_QUICK_REACT, Triple(eventId, clickedOn, add)) + actionHandlerModel.fireAction(SimpleAction.QuickReact(eventId, clickedOn, add)) dismiss() } } @@ -138,6 +138,19 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() { } quickReactBottomDivider.isVisible = it.canReact() bottom_sheet_quick_reaction_container.isVisible = it.canReact() + if (it.informationData.sendState.isSending()) { + messageStatusInfo.isVisible = true + messageStatusProgress.isVisible = true + messageStatusText.text = getString(R.string.event_status_sending_message) + messageStatusText.setCompoundDrawables(null, null, null, null) + } else if (it.informationData.sendState.hasFailed()) { + messageStatusInfo.isVisible = true + messageStatusProgress.isVisible = false + messageStatusText.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_warning_small, 0, 0, 0) + messageStatusText.text = getString(R.string.unable_to_send_message) + } else { + messageStatusInfo.isVisible = false + } return@withState } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt index 5b0dbdfe..ae8803f5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt @@ -15,17 +15,21 @@ */ package im.vector.riotx.features.home.room.detail.timeline.action +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes import com.airbnb.mvrx.* import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.isTextMessage import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageImageContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited import im.vector.matrix.rx.RxRoom import im.vector.riotx.R import im.vector.riotx.core.extensions.canReact @@ -35,7 +39,26 @@ import im.vector.riotx.core.utils.isSingleEmoji import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData -data class SimpleAction(val uid: String, val titleRes: Int, val iconResId: Int?, val data: Any? = null) +sealed class SimpleAction(@StringRes val titleRes: Int, @DrawableRes val iconResId: Int) { + data class AddReaction(val eventId: String) : SimpleAction(R.string.message_add_reaction, R.drawable.ic_add_reaction) + data class Copy(val content: String) : SimpleAction(R.string.copy, R.drawable.ic_copy) + data class Edit(val eventId: String) : SimpleAction(R.string.edit, R.drawable.ic_edit) + data class Quote(val eventId: String) : SimpleAction(R.string.quote, R.drawable.ic_quote) + data class Reply(val eventId: String) : SimpleAction(R.string.reply, R.drawable.ic_reply) + data class Share(val imageUrl: String?) : SimpleAction(R.string.share, R.drawable.ic_share) + data class Resend(val eventId: String) : SimpleAction(R.string.global_retry, R.drawable.ic_refresh_cw) + data class Remove(val eventId: String) : SimpleAction(R.string.remove, R.drawable.ic_trash) + data class Delete(val eventId: String) : SimpleAction(R.string.delete, R.drawable.ic_delete) + data class Cancel(val eventId: String) : SimpleAction(R.string.cancel, R.drawable.ic_close_round) + data class ViewSource(val content: String) : SimpleAction(R.string.view_source, R.drawable.ic_view_source) + data class ViewDecryptedSource(val content: String) : SimpleAction(R.string.view_decrypted_source, R.drawable.ic_view_source) + data class CopyPermalink(val eventId: String) : SimpleAction(R.string.permalink, R.drawable.ic_permalink) + data class Flag(val eventId: String) : SimpleAction(R.string.report_content, R.drawable.ic_flag) + data class QuickReact(val eventId: String, val clickedOn: String, val add: Boolean) : SimpleAction(0, 0) + data class ViewReactions(val messageInformationData: MessageInformationData) : SimpleAction(R.string.message_view_reaction, R.drawable.ic_view_reactions) + data class ViewEditHistory(val messageInformationData: MessageInformationData) : + SimpleAction(R.string.message_view_edit_history, R.drawable.ic_view_edit_history) +} data class MessageMenuState( val roomId: String, @@ -67,22 +90,6 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M private val informationData: MessageInformationData = initialState.informationData companion object : MvRxViewModelFactory { - - const val ACTION_ADD_REACTION = "add_reaction" - const val ACTION_COPY = "copy" - const val ACTION_EDIT = "edit" - const val ACTION_QUOTE = "quote" - const val ACTION_REPLY = "reply" - const val ACTION_SHARE = "share" - const val ACTION_RESEND = "resend" - const val ACTION_DELETE = "delete" - const val VIEW_SOURCE = "VIEW_SOURCE" - const val VIEW_DECRYPTED_SOURCE = "VIEW_DECRYPTED_SOURCE" - const val ACTION_COPY_PERMALINK = "ACTION_COPY_PERMALINK" - const val ACTION_FLAG = "ACTION_FLAG" - const val ACTION_QUICK_REACT = "ACTION_QUICK_REACT" - const val ACTION_VIEW_REACTIONS = "ACTION_VIEW_REACTIONS" - override fun create(viewModelContext: ViewModelContext, state: MessageMenuState): MessageMenuViewModel? { val fragment: MessageMenuFragment = (viewModelContext as FragmentViewModelContext).fragment() return fragment.messageMenuViewModelFactory.create(state) @@ -96,80 +103,74 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M private fun observeEvent() { RxRoom(room) .liveTimelineEvent(eventId) - ?.map { + .map { actionsForEvent(it) } - ?.execute { + .execute { copy(actions = it) } } private fun actionsForEvent(event: TimelineEvent): List { - val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel() ?: event.root.getClearContent().toModel() val type = messageContent?.type - val actions = if (!event.sendState.isSent()) { - //Resend and Delete - listOf( -// SimpleAction(ACTION_RESEND, R.string.resend, R.drawable.ic_send, event.root.eventId), -// //TODO delete icon -// SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, event.root.eventId) - ) - } else { - arrayListOf().apply { - - if (event.sendState == SendState.SENDING) { - //TODO add cancel? - return@apply + return arrayListOf().apply { + if (event.root.sendState.hasFailed()) { + if (canRetry(event)) { + add(SimpleAction.Resend(eventId)) } - //TODO is downloading attachement? - + add(SimpleAction.Remove(eventId)) + } else if (event.root.sendState.isSending()) { + //TODO is uploading attachment? + if (canCancel(event)) { + add(SimpleAction.Cancel(eventId)) + } + } else { if (!event.root.isRedacted()) { - if (canReply(event, messageContent)) { - this.add(SimpleAction(ACTION_REPLY, R.string.reply, R.drawable.ic_reply, eventId)) + add(SimpleAction.Reply(eventId)) } if (canEdit(event, session.myUserId)) { - this.add(SimpleAction(ACTION_EDIT, R.string.edit, R.drawable.ic_edit, eventId)) + add(SimpleAction.Edit(eventId)) } if (canRedact(event, session.myUserId)) { - this.add(SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, eventId)) + add(SimpleAction.Delete(eventId)) } if (canCopy(type)) { //TODO copy images? html? see ClipBoard - this.add(SimpleAction(ACTION_COPY, R.string.copy, R.drawable.ic_copy, messageContent!!.body)) + add(SimpleAction.Copy(messageContent!!.body)) } if (event.canReact()) { - this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_add_reaction, eventId)) + add(SimpleAction.AddReaction(eventId)) } if (canQuote(event, messageContent)) { - this.add(SimpleAction(ACTION_QUOTE, R.string.quote, R.drawable.ic_quote, eventId)) + add(SimpleAction.Quote(eventId)) } if (canViewReactions(event)) { - this.add(SimpleAction(ACTION_VIEW_REACTIONS, R.string.message_view_reaction, R.drawable.ic_view_reactions, informationData)) + add(SimpleAction.ViewReactions(informationData)) + } + + if (event.hasBeenEdited()) { + add(SimpleAction.ViewEditHistory(informationData)) } if (canShare(type)) { if (messageContent is MessageImageContent) { - this.add( - SimpleAction(ACTION_SHARE, - R.string.share, R.drawable.ic_share, - session.contentUrlResolver().resolveFullSize(messageContent.url)) - ) + add(SimpleAction.Share(session.contentUrlResolver().resolveFullSize(messageContent.url))) } //TODO } - if (event.sendState == SendState.SENT) { + if (event.root.sendState == SendState.SENT) { //TODO Can be redacted @@ -177,23 +178,25 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M } } - this.add(SimpleAction(VIEW_SOURCE, R.string.view_source, R.drawable.ic_view_source, event.root.toContentStringWithIndent())) + add(SimpleAction.ViewSource(event.root.toContentStringWithIndent())) if (event.isEncrypted()) { val decryptedContent = event.root.toClearContentStringWithIndent() ?: stringProvider.getString(R.string.encryption_information_decryption_error) - this.add(SimpleAction(VIEW_DECRYPTED_SOURCE, R.string.view_decrypted_source, R.drawable.ic_view_source, decryptedContent)) + add(SimpleAction.ViewDecryptedSource(decryptedContent)) } - this.add(SimpleAction(ACTION_COPY_PERMALINK, R.string.permalink, R.drawable.ic_permalink, event.root.eventId)) + add(SimpleAction.CopyPermalink(eventId)) if (session.myUserId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) { //not sent by me - this.add(SimpleAction(ACTION_FLAG, R.string.report_content, R.drawable.ic_flag, event.root.eventId)) + add(SimpleAction.Flag(eventId)) } } } - return actions } + private fun canCancel(event: TimelineEvent): Boolean { + return false + } private fun canReply(event: TimelineEvent, messageContent: MessageContent?): Boolean { //Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment @@ -232,6 +235,11 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M return event.root.senderId == myUserId } + private fun canRetry(event: TimelineEvent): Boolean { + return event.root.sendState.hasFailed() && event.root.isTextMessage() + } + + private fun canViewReactions(event: TimelineEvent): Boolean { //Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment if (event.root.getClearType() != EventType.MESSAGE) return false @@ -258,9 +266,7 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M MessageType.MSGTYPE_NOTICE, MessageType.MSGTYPE_EMOTE, MessageType.FORMAT_MATRIX_HTML, - MessageType.MSGTYPE_LOCATION -> { - true - } + MessageType.MSGTYPE_LOCATION -> true else -> false } } @@ -270,9 +276,7 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M return when (type) { MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_AUDIO, - MessageType.MSGTYPE_VIDEO -> { - true - } + MessageType.MSGTYPE_VIDEO -> true else -> false } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt index ea7036b7..4a3f50c4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt @@ -43,7 +43,7 @@ class EncryptionItemFactory @Inject constructor(private val stringProvider: Stri val informationData = MessageInformationData( eventId = event.root.eventId ?: "?", senderId = event.root.senderId ?: "", - sendState = event.sendState, + sendState = event.root.sendState, avatarUrl = event.senderAvatar(), memberName = event.senderName(), showInformation = false diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 3a0d2d1d..f81ef9d3 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -28,7 +28,6 @@ import im.vector.matrix.android.api.permalinks.MatrixLinkify import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent @@ -41,11 +40,13 @@ import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.linkify.VectorLinkify import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.resources.UserPreferencesProvider import im.vector.riotx.core.utils.DebouncedClickListener 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.ContentUploadStateTrackerBinder import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider +import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar import im.vector.riotx.features.home.room.detail.timeline.item.* import im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory import im.vector.riotx.features.html.EventHtmlRenderer @@ -63,7 +64,8 @@ class MessageItemFactory @Inject constructor( private val emojiCompatFontProvider: EmojiCompatFontProvider, private val imageContentRenderer: ImageContentRenderer, private val messageInformationDataFactory: MessageInformationDataFactory, - private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder) { + private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, + private val userPreferencesProvider: UserPreferencesProvider) { fun create(event: TimelineEvent, @@ -89,20 +91,37 @@ class MessageItemFactory @Inject constructor( || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE ) { // ignore replace event, the targeted id is already edited - return BlankItem_() + if (userPreferencesProvider.shouldShowHiddenEvents()) { + //These are just for debug to display hidden event, they should be filtered out in normal mode + val informationData = MessageInformationData( + eventId = event.root.eventId ?: "?", + senderId = event.root.senderId ?: "", + sendState = event.root.sendState, + time = "", + avatarUrl = event.senderAvatar(), + memberName = "", + showInformation = false + ) + return NoticeItem_() + .avatarRenderer(avatarRenderer) + .informationData(informationData) + .noticeText("{ \"type\": ${event.root.getClearType()} }") + .highlighted(highlight) + .baseCallback(callback) + } else { + return BlankItem_() + } } // val all = event.root.toContent() // val ev = all.toModel() return when (messageContent) { is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, - event.annotations?.editSummary, highlight, callback) - is MessageTextContent -> buildTextMessageItem(event.sendState, + is MessageTextContent -> buildTextMessageItem(event.root.sendState, messageContent, informationData, - event.annotations?.editSummary, highlight, callback ) @@ -276,7 +295,6 @@ class MessageItemFactory @Inject constructor( private fun buildTextMessageItem(sendState: SendState, messageContent: MessageTextContent, informationData: MessageInformationData, - editSummary: EditAggregatedSummary?, highlight: Boolean, callback: TimelineEventController.Callback?): MessageTextItem? { @@ -289,7 +307,7 @@ class MessageItemFactory @Inject constructor( return MessageTextItem_() .apply { if (informationData.hasBeenEdited) { - val spannable = annotateWithEdited(linkifiedBody, callback, informationData, editSummary) + val spannable = annotateWithEdited(linkifiedBody, callback, informationData) message(spannable) } else { message(linkifiedBody) @@ -316,14 +334,13 @@ class MessageItemFactory @Inject constructor( private fun annotateWithEdited(linkifiedBody: CharSequence, callback: TimelineEventController.Callback?, - informationData: MessageInformationData, - editSummary: EditAggregatedSummary?): SpannableStringBuilder { + informationData: MessageInformationData): SpannableStringBuilder { val spannable = SpannableStringBuilder() spannable.append(linkifiedBody) val editedSuffix = stringProvider.getString(R.string.edited_suffix) spannable.append(" ").append(editedSuffix) val color = colorProvider.getColorFromAttribute(R.attr.vctr_list_header_secondary_text_color) - val editStart = spannable.indexOf(editedSuffix) + val editStart = spannable.lastIndexOf(editedSuffix) val editEnd = editStart + editedSuffix.length spannable.setSpan( ForegroundColorSpan(color), @@ -334,7 +351,7 @@ class MessageItemFactory @Inject constructor( spannable.setSpan(RelativeSizeSpan(.9f), editStart, editEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) spannable.setSpan(object : ClickableSpan() { override fun onClick(widget: View?) { - callback?.onEditedDecorationClicked(informationData, editSummary) + callback?.onEditedDecorationClicked(informationData) } override fun updateDrawState(ds: TextPaint?) { @@ -386,7 +403,6 @@ class MessageItemFactory @Inject constructor( private fun buildEmoteMessageItem(messageContent: MessageEmoteContent, informationData: MessageInformationData, - editSummary: EditAggregatedSummary?, highlight: Boolean, callback: TimelineEventController.Callback?): MessageTextItem? { @@ -397,7 +413,7 @@ class MessageItemFactory @Inject constructor( return MessageTextItem_() .apply { if (informationData.hasBeenEdited) { - val spannable = annotateWithEdited(message, callback, informationData, editSummary) + val spannable = annotateWithEdited(message, callback, informationData) message(spannable) } else { message(message) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index c23fdfbd..52771ad6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -37,7 +37,7 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv val informationData = MessageInformationData( eventId = event.root.eventId ?: "?", senderId = event.root.senderId ?: "", - sendState = event.sendState, + sendState = event.root.sendState, avatarUrl = event.senderAvatar(), memberName = event.senderName(), showInformation = false 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 new file mode 100644 index 00000000..ed503eaf --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.detail.timeline.factory + +import im.vector.matrix.android.api.permalinks.PermalinkFactory +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.resources.StringProvider +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) { + + fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): RoomCreateItem? { + val createRoomContent = event.root.getClearContent().toModel() + ?: return null + val predecessorId = createRoomContent.predecessor?.roomId ?: return null + val roomLink = PermalinkFactory.createPermalink(predecessorId) ?: return null + val text = span { + +stringProvider.getString(R.string.room_tombstone_continuation_description) + +"\n" + span(stringProvider.getString(R.string.room_tombstone_predecessor_link)) { + textDecorationLine = "underline" + onClick = { callback?.onRoomCreateLinkClicked(roomLink) } + } + } + return RoomCreateItem_() + .text(text) + } + + +} \ No newline at end of file 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 4a927b19..43197d8b 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 @@ -33,6 +33,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me private val encryptedItemFactory: EncryptedItemFactory, private val noticeItemFactory: NoticeItemFactory, private val defaultItemFactory: DefaultItemFactory, + private val roomCreateItemFactory: RoomCreateItemFactory, private val avatarRenderer: AvatarRenderer) { fun create(event: TimelineEvent, @@ -45,6 +46,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me when (event.root.getClearType()) { EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback) // State and call + EventType.STATE_ROOM_TOMBSTONE, EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_MEMBER, @@ -52,7 +54,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER -> noticeItemFactory.create(event, highlight, callback) - + // State room create + EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback) // Crypto EventType.ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback) EventType.ENCRYPTED -> { @@ -66,15 +69,14 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me // Unhandled event types (yet) EventType.STATE_ROOM_THIRD_PARTY_INVITE, - EventType.STICKER, - EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event, highlight) + EventType.STICKER -> defaultItemFactory.create(event, highlight) else -> { //These are just for debug to display hidden event, they should be filtered out in normal mode val informationData = MessageInformationData( eventId = event.root.eventId ?: "?", senderId = event.root.senderId ?: "", - sendState = event.sendState, + sendState = event.root.sendState, time = "", avatarUrl = event.senderAvatar(), memberName = "", 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 369af64a..2b1a2633 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 @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.* import im.vector.matrix.android.api.session.room.model.call.CallInviteContent +import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.R import im.vector.riotx.core.resources.StringProvider @@ -37,6 +38,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderName()) EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) @@ -56,6 +58,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER -> formatCallEvent(event, senderName) + EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName) else -> { Timber.v("Type $type not handled by this formatter") null @@ -72,6 +75,10 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin } } + private fun formatRoomTombstoneEvent(event: Event, senderName: String?): CharSequence? { + return stringProvider.getString(R.string.notice_room_update, senderName) + } + private fun formatRoomTopicEvent(event: Event, senderName: String?): CharSequence? { val content = event.getClearContent().toModel() ?: return null return if (content.topic.isNullOrEmpty()) { 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 667de65a..fa0a71bd 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 @@ -39,7 +39,8 @@ object TimelineDisplayableEvents { EventType.ENCRYPTION, EventType.STATE_ROOM_THIRD_PARTY_INVITE, EventType.STICKER, - EventType.STATE_ROOM_CREATE + EventType.STATE_ROOM_CREATE, + EventType.STATE_ROOM_TOMBSTONE ) val DEBUG_DISPLAYABLE_TYPES = DISPLAYABLE_TYPES + listOf( diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index 88697db4..670cf471 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -29,6 +29,7 @@ import androidx.core.view.children import androidx.core.view.isGone import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute +import im.vector.matrix.android.api.session.room.send.SendState import im.vector.riotx.R import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.DebouncedClickListener @@ -161,9 +162,11 @@ abstract class AbsMessageItem : BaseEventItem() { return true } - protected fun renderSendState(root: View, textView: TextView?) { + protected open fun renderSendState(root: View, textView: TextView?, failureIndicator: ImageView? = null) { root.isClickable = informationData.sendState.isSent() - textView?.setTextColor(colorProvider.getMessageTextColor(informationData.sendState)) + val state = if (informationData.hasPendingEdits) SendState.UNSENT else informationData.sendState + textView?.setTextColor(colorProvider.getMessageTextColor(state)) + failureIndicator?.isVisible = informationData.sendState.hasFailed() } abstract class Holder(@IdRes stubId: Int) : BaseHolder(stubId) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt index d551e44c..6f713b17 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt @@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail.timeline.item import android.view.View import android.view.ViewGroup import android.widget.ImageView +import androidx.core.view.ViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R @@ -42,13 +43,16 @@ abstract class MessageImageVideoItem : AbsMessageItem(R.id.messageMediaPlayView) val mediaContentView by bind(R.id.messageContentMedia) + val failedToSendIndicator by bind(R.id.messageFailToSendIndicator) } companion object { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt index 5f49fdc3..31b92c0e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -30,8 +30,9 @@ data class MessageInformationData( val memberName: CharSequence? = null, val showInformation: Boolean = true, /*List of reactions (emoji,count,isSelected)*/ - var orderedReactionList: List? = null, - var hasBeenEdited: Boolean = false + val orderedReactionList: List? = null, + val hasBeenEdited: Boolean = false, + val hasPendingEdits: Boolean = false ) : Parcelable diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/RoomCreateItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/RoomCreateItem.kt new file mode 100644 index 00000000..ff0e6a99 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/RoomCreateItem.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.detail.timeline.item + +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.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel +import me.saket.bettermovementmethod.BetterLinkMovementMethod + +@EpoxyModelClass(layout = R.layout.item_timeline_event_create) +abstract class RoomCreateItem : VectorEpoxyModel() { + + @EpoxyAttribute lateinit var text: CharSequence + + override fun bind(holder: Holder) { + holder.description.movementMethod = BetterLinkMovementMethod.getInstance() + holder.description.text = text + } + + class Holder : VectorEpoxyHolder() { + val description by bind(R.id.roomCreateItemDescription) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt index d17c8de6..fe15c5d2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail.timeline.util import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.isSingleEmoji @@ -59,12 +60,10 @@ class MessageInformationDataFactory @Inject constructor(private val timelineDate ?: "")) } - val hasBeenEdited = event.annotations?.editSummary != null - return MessageInformationData( eventId = eventId, senderId = event.root.senderId ?: "", - sendState = event.sendState, + sendState = event.root.sendState, time = time, avatarUrl = avatarUrl, memberName = formattedMemberName, @@ -74,7 +73,8 @@ class MessageInformationDataFactory @Inject constructor(private val timelineDate ?.map { ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty()) }, - hasBeenEdited = hasBeenEdited + hasBeenEdited = event.hasBeenEdited(), + hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false ) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt index b61e69ac..4e8fe284 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt @@ -149,7 +149,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O } override fun createDirectChat() { - vectorBaseActivity.notImplemented("creating direct chat") + navigator.openCreateDirectRoom(requireActivity()) } private fun setupRecyclerView() { @@ -253,7 +253,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O return super.onBackPressed() } -// RoomSummaryController.Callback ************************************************************** + // RoomSummaryController.Callback ************************************************************** override fun onRoomSelected(room: RoomSummary) { roomListViewModel.accept(RoomListActions.SelectRoom(room)) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt index a1ae4fdf..0fed679e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt @@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.tag.RoomTag +import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.features.home.HomeRoomListObservableStore @@ -83,7 +84,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room // PRIVATE METHODS ***************************************************************************** private fun handleSelectRoom(action: RoomListActions.SelectRoom) { - _openRoomLiveData.postValue(LiveEvent(action.roomSummary.roomId)) + _openRoomLiveData.postLiveEvent(action.roomSummary.roomId) } private fun handleToggleCategory(action: RoomListActions.ToggleCategory) = setState { @@ -134,7 +135,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room ) } - session.getRoom(roomId)?.join(object : MatrixCallback { + session.getRoom(roomId)?.join(emptyList(), object : MatrixCallback { override fun onSuccess(data: Unit) { // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // Instead, we wait for the room to be joined @@ -142,7 +143,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room override fun onFailure(failure: Throwable) { // Notify the user - _invitationAnswerErrorLiveData.postValue(LiveEvent(failure)) + _invitationAnswerErrorLiveData.postLiveEvent(failure) setState { copy( @@ -178,7 +179,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room override fun onFailure(failure: Throwable) { // Notify the user - _invitationAnswerErrorLiveData.postValue(LiveEvent(failure)) + _invitationAnswerErrorLiveData.postLiveEvent(failure) setState { copy( diff --git a/vector/src/main/java/im/vector/riotx/features/homeserver/ServerUrlsRepository.kt b/vector/src/main/java/im/vector/riotx/features/homeserver/ServerUrlsRepository.kt index 37ac4400..18587086 100644 --- a/vector/src/main/java/im/vector/riotx/features/homeserver/ServerUrlsRepository.kt +++ b/vector/src/main/java/im/vector/riotx/features/homeserver/ServerUrlsRepository.kt @@ -69,39 +69,19 @@ object ServerUrlsRepository { val prefs = PreferenceManager.getDefaultSharedPreferences(context) return prefs.getString(HOME_SERVER_URL_PREF, - prefs.getString(DEFAULT_REFERRER_HOME_SERVER_URL_PREF, - getDefaultHomeServerUrl(context))) + prefs.getString(DEFAULT_REFERRER_HOME_SERVER_URL_PREF, + getDefaultHomeServerUrl(context))) } - /** - * Return last used identity server url, or the default one from referrer or the default one from resources - */ - fun getLastIdentityServerUrl(context: Context): String { - val prefs = PreferenceManager.getDefaultSharedPreferences(context) - - return prefs.getString(IDENTITY_SERVER_URL_PREF, - prefs.getString(DEFAULT_REFERRER_IDENTITY_SERVER_URL_PREF, - getDefaultIdentityServerUrl(context))) - } - /** * Return true if url is the default home server url form resources */ fun isDefaultHomeServerUrl(context: Context, url: String) = url == getDefaultHomeServerUrl(context) - /** - * Return true if url is the default identity server url form resources - */ - fun isDefaultIdentityServerUrl(context: Context, url: String) = url == getDefaultIdentityServerUrl(context) - /** * Return default home server url from resources */ fun getDefaultHomeServerUrl(context: Context): String = context.getString(R.string.default_hs_server_url) - /** - * Return default identity server url from resources - */ - fun getDefaultIdentityServerUrl(context: Context): String = context.getString(R.string.default_identity_server_url) } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 53519fc4..41eed536 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -37,6 +37,7 @@ import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.utils.openUrlInExternalBrowser import im.vector.riotx.features.disclaimer.showDisclaimerDialog import im.vector.riotx.features.home.HomeActivity +import im.vector.riotx.features.homeserver.ServerUrlsRepository import im.vector.riotx.features.notifications.PushRuleTriggerListener import io.reactivex.Observable import io.reactivex.functions.Function3 @@ -44,9 +45,6 @@ import io.reactivex.rxkotlin.subscribeBy import kotlinx.android.synthetic.main.activity_login.* import javax.inject.Inject -private const val DEFAULT_HOME_SERVER_URI = "https://matrix.org" -private const val DEFAULT_IDENTITY_SERVER_URI = "https://vector.im" -private const val DEFAULT_ANTIVIRUS_SERVER_URI = "https://matrix.org" class LoginActivity : VectorBaseActivity() { @@ -66,7 +64,7 @@ class LoginActivity : VectorBaseActivity() { setupNotice() setupAuthButton() setupPasswordReveal() - homeServerField.setText(DEFAULT_HOME_SERVER_URI) + homeServerField.setText(ServerUrlsRepository.getDefaultHomeServerUrl(this)) } private fun setupNotice() { @@ -118,8 +116,6 @@ class LoginActivity : VectorBaseActivity() { val homeServerUri = homeServerField.text?.trim().toString() HomeServerConnectionConfig.Builder() .withHomeServerUri(homeServerUri) - .withIdentityServerUri(DEFAULT_IDENTITY_SERVER_URI) - .withAntiVirusServerUri(DEFAULT_ANTIVIRUS_SERVER_URI) .build() } } diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt index 4d68d869..7a7c880c 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt @@ -16,16 +16,22 @@ package im.vector.riotx.features.media +import android.graphics.drawable.Drawable import android.net.Uri import android.os.Parcelable import android.widget.ImageView import androidx.exifinterface.media.ExifInterface +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target import com.github.piasy.biv.view.BigImageView import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.glide.GlideApp +import im.vector.riotx.core.glide.GlideRequest import im.vector.riotx.core.utils.DimensionUtils.dpToPx import kotlinx.android.parcel.Parcelize import timber.log.Timber @@ -62,7 +68,43 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: imageView.layoutParams.height = height imageView.layoutParams.width = width - val glideRequest = if (data.elementToDecrypt != null) { + createGlideRequest(data, mode, imageView, width, height) + .dontAnimate() + .transform(RoundedCorners(dpToPx(8, imageView.context))) + .thumbnail(0.3f) + .into(imageView) + + } + + fun renderFitTarget(data: Data, mode: Mode, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) { + val (width, height) = processSize(data, mode) + + createGlideRequest(data, mode, imageView, width, height) + .listener(object : RequestListener { + override fun onLoadFailed(e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean): Boolean { + callback?.invoke(false) + return false + } + + override fun onResourceReady(resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean): Boolean { + callback?.invoke(true) + return false + } + + }) + .fitCenter() + .into(imageView) + } + + private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, width: Int, height: Int): GlideRequest { + return if (data.elementToDecrypt != null) { // Encrypted image GlideApp .with(imageView) @@ -81,12 +123,6 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: .with(imageView) .load(resolvedUrl) } - - glideRequest - .dontAnimate() - .transform(RoundedCorners(dpToPx(8, imageView.context))) - .thumbnail(0.3f) - .into(imageView) } fun render(data: Data, imageView: BigImageView) { diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt index bd3f4480..a44672a5 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt @@ -18,15 +18,29 @@ package im.vector.riotx.features.media import android.content.Context import android.content.Intent +import android.graphics.drawable.Drawable +import android.os.Build import android.os.Bundle +import android.view.View +import android.view.ViewTreeObserver +import androidx.annotation.RequiresApi import androidx.appcompat.widget.Toolbar +import androidx.core.transition.addListener +import androidx.core.view.ViewCompat +import androidx.core.view.isInvisible import androidx.core.view.isVisible +import androidx.transition.Transition +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target import com.github.piasy.biv.indicator.progresspie.ProgressPieIndicator import com.github.piasy.biv.view.GlideImageViewFactory import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.platform.VectorBaseActivity import kotlinx.android.synthetic.main.activity_image_media_viewer.* +import timber.log.Timber import javax.inject.Inject @@ -34,6 +48,8 @@ class ImageMediaViewerActivity : VectorBaseActivity() { @Inject lateinit var imageContentRenderer: ImageContentRenderer + lateinit var mediaData: ImageContentRenderer.Data + override fun injectWith(injector: ScreenComponent) { injector.inject(this) } @@ -41,11 +57,31 @@ class ImageMediaViewerActivity : VectorBaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(im.vector.riotx.R.layout.activity_image_media_viewer) - val mediaData = intent.getParcelableExtra(EXTRA_MEDIA_DATA) + mediaData = intent.getParcelableExtra(EXTRA_MEDIA_DATA) + intent.extras.getString(EXTRA_SHARED_TRANSITION_NAME)?.let { + ViewCompat.setTransitionName(imageTransitionView, it) + } if (mediaData.url.isNullOrEmpty()) { finish() + return + } + + configureToolbar(imageMediaViewerToolbar, mediaData) + + if (isFirstCreation() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && addTransitionListener()) { + // Encrypted image + imageTransitionView.isVisible = true + imageMediaViewerImageView.isVisible = false + encryptedImageView.isVisible = false + //Postpone transaction a bit until thumbnail is loaded + supportPostponeEnterTransition() + imageContentRenderer.renderFitTarget(mediaData, ImageContentRenderer.Mode.THUMBNAIL, imageTransitionView) { + //Proceed with transaction + scheduleStartPostponedTransition(imageTransitionView) + } + } else { - configureToolbar(imageMediaViewerToolbar, mediaData) + imageTransitionView.isVisible = false if (mediaData.elementToDecrypt != null) { // Encrypted image @@ -78,13 +114,101 @@ class ImageMediaViewerActivity : VectorBaseActivity() { } } + override fun onBackPressed() { + //show again for exit animation + imageTransitionView.isVisible = true + super.onBackPressed() + } + + private fun scheduleStartPostponedTransition(sharedElement: View) { + sharedElement.viewTreeObserver.addOnPreDrawListener( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + sharedElement.viewTreeObserver.removeOnPreDrawListener(this) + supportStartPostponedEnterTransition() + return true + } + }) + } + + /** + * Try and add a [Transition.TransitionListener] to the entering shared element + * [Transition]. We do this so that we can load the full-size image after the transition + * has completed. + * + * @return true if we were successful in adding a listener to the enter transition + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + private fun addTransitionListener(): Boolean { + val transition = window.sharedElementEnterTransition + + if (transition != null) { + // There is an entering shared element transition so add a listener to it + transition.addListener( + onEnd = { + if (mediaData.elementToDecrypt != null) { + // Encrypted image + GlideApp + .with(this) + .load(mediaData) + .dontAnimate() + .listener(object : RequestListener { + override fun onLoadFailed(e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean): Boolean { + //TODO ? + Timber.e("TRANSITION onLoadFailed") + imageMediaViewerImageView.isVisible = false + encryptedImageView.isVisible = true + return false + } + + override fun onResourceReady(resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean): Boolean { + Timber.e("TRANSITION onResourceReady") + imageTransitionView.isInvisible = true + imageMediaViewerImageView.isVisible = false + encryptedImageView.isVisible = true + return false + } + + }) + .into(encryptedImageView) + } else { + imageTransitionView.isInvisible = true + // Clear image + imageMediaViewerImageView.isVisible = true + encryptedImageView.isVisible = false + + imageMediaViewerImageView.setImageViewFactory(GlideImageViewFactory()) + imageMediaViewerImageView.setProgressIndicator(ProgressPieIndicator()) + imageContentRenderer.render(mediaData, imageMediaViewerImageView) + } + }, + onCancel = { + //Something to do? + } + ) + return true + } + + // If we reach here then we have not added a listener + return false + } + companion object { private const val EXTRA_MEDIA_DATA = "EXTRA_MEDIA_DATA" + private const val EXTRA_SHARED_TRANSITION_NAME = "EXTRA_SHARED_TRANSITION_NAME" - fun newIntent(context: Context, mediaData: ImageContentRenderer.Data): Intent { + fun newIntent(context: Context, mediaData: ImageContentRenderer.Data, shareTransitionName: String?): Intent { return Intent(context, ImageMediaViewerActivity::class.java).apply { putExtra(EXTRA_MEDIA_DATA, mediaData) + putExtra(EXTRA_SHARED_TRANSITION_NAME, shareTransitionName) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 2e34a7d1..1428a0ac 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -25,6 +25,7 @@ import im.vector.riotx.core.utils.toast import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity import im.vector.riotx.features.debug.DebugMenuActivity +import im.vector.riotx.features.home.createdirect.CreateDirectRoomActivity import im.vector.riotx.features.home.room.detail.RoomDetailActivity import im.vector.riotx.features.home.room.detail.RoomDetailArgs import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity @@ -68,6 +69,11 @@ class DefaultNavigator @Inject constructor() : Navigator { context.startActivity(intent) } + override fun openCreateDirectRoom(context: Context) { + val intent = CreateDirectRoomActivity.getIntent(context) + context.startActivity(intent) + } + override fun openRoomsFiltering(context: Context) { val intent = FilteredRoomsActivity.newIntent(context) context.startActivity(intent) diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index bf888ffe..c2da7643 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -29,6 +29,8 @@ interface Navigator { fun openCreateRoom(context: Context, initialName: String = "") + fun openCreateDirectRoom(context: Context) + fun openRoomDirectory(context: Context, initialFilter: String = "") fun openRoomsFiltering(context: Context) diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt index ac6068b0..638cf7d1 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt @@ -75,7 +75,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { private fun handleJoinRoom(roomId: String) { activeSessionHolder.getSafeActiveSession()?.let { session -> session.getRoom(roomId) - ?.join(object : MatrixCallback {}) + ?.join(emptyList(), object : MatrixCallback {}) } } diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt index 45317da5..6695c109 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt @@ -44,6 +44,7 @@ import javax.inject.Singleton */ @Singleton class NotificationDrawerManager @Inject constructor(private val context: Context, + private val vectorPreferences: VectorPreferences, private val activeSessionHolder: ActiveSessionHolder, private val iconLoader: IconLoader, private val bitmapLoader: BitmapLoader, @@ -73,7 +74,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context Events might be grouped and there might not be one notification per event! */ fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) { - if (!VectorPreferences.areNotificationEnabledForDevice(context)) { + if (!vectorPreferences.areNotificationEnabledForDevice()) { Timber.i("Notification are disabled for this device") return } @@ -326,7 +327,13 @@ class NotificationDrawerManager @Inject constructor(private val context: Context globalLastMessageTimestamp = lastMessageTimestamp } - NotificationUtils.buildMessagesListNotification(context, style, roomEventGroupInfo, largeBitmap, lastMessageTimestamp, myUserDisplayName) + NotificationUtils.buildMessagesListNotification(context, + vectorPreferences, + style, + roomEventGroupInfo, + largeBitmap, + lastMessageTimestamp, + myUserDisplayName) ?.let { //is there an id for this room? notifications.add(it) @@ -344,7 +351,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context for (event in simpleEvents) { //We build a simple event if (firstTime || !event.hasBeenDisplayed) { - NotificationUtils.buildSimpleEventNotification(context, event, null, session.myUserId)?.let { + NotificationUtils.buildSimpleEventNotification(context, vectorPreferences, event, null, session.myUserId)?.let { notifications.add(it) NotificationUtils.showNotificationMessage(context, event.eventId, ROOM_EVENT_NOTIFICATION_ID, it) event.hasBeenDisplayed = true //we can consider it as displayed @@ -383,6 +390,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context NotificationUtils.buildSummaryListNotification( context, + vectorPreferences, summaryInboxStyle, sumTitle, noisy = hasNewEvent && summaryIsNoisy, diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt index 80249912..4d7a3edf 100755 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt @@ -367,6 +367,7 @@ object NotificationUtils { * Build a notification for a Room */ fun buildMessagesListNotification(context: Context, + vectorPreferences: VectorPreferences, messageStyle: NotificationCompat.MessagingStyle, roomInfo: RoomEventGroupInfo, largeIcon: Bitmap?, @@ -420,7 +421,7 @@ object NotificationUtils { priority = NotificationCompat.PRIORITY_DEFAULT if (roomInfo.shouldBing) { //Compat - VectorPreferences.getNotificationRingTone(context)?.let { + vectorPreferences.getNotificationRingTone()?.let { setSound(it) } setLights(accentColor, 500, 500) @@ -476,7 +477,11 @@ object NotificationUtils { } - fun buildSimpleEventNotification(context: Context, simpleNotifiableEvent: NotifiableEvent, largeIcon: Bitmap?, matrixId: String): Notification? { + fun buildSimpleEventNotification(context: Context, + vectorPreferences: VectorPreferences, + simpleNotifiableEvent: NotifiableEvent, + largeIcon: Bitmap?, + matrixId: String): Notification? { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) // Build the pending intent for when the notification is clicked val smallIcon = R.drawable.ic_status_bar @@ -534,7 +539,7 @@ object NotificationUtils { if (simpleNotifiableEvent.noisy) { //Compat priority = NotificationCompat.PRIORITY_DEFAULT - VectorPreferences.getNotificationRingTone(context)?.let { + vectorPreferences.getNotificationRingTone()?.let { setSound(it) } setLights(accentColor, 500, 500) @@ -606,6 +611,7 @@ object NotificationUtils { * Build the summary notification */ fun buildSummaryListNotification(context: Context, + vectorPreferences: VectorPreferences, style: NotificationCompat.InboxStyle, compatSummary: String, noisy: Boolean, @@ -630,7 +636,7 @@ object NotificationUtils { if (noisy) { //Compat priority = NotificationCompat.PRIORITY_DEFAULT - VectorPreferences.getNotificationRingTone(context)?.let { + vectorPreferences.getNotificationRingTone()?.let { setSound(it) } setLights(accentColor, 500, 500) diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt b/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt index 760128de..d404b64c 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt @@ -56,6 +56,10 @@ class PushRuleTriggerListener @Inject constructor( } } + override fun onRoomLeft(roomId: String) { + notificationDrawerManager.clearMessageEventOfRoom(roomId) + } + override fun batchFinish() { notificationDrawerManager.refreshNotificationDrawer() } 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 d34d2be4..d21ad021 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 @@ -35,7 +35,7 @@ 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.themes.ThemeUtils -import im.vector.riotx.features.version.getVersion +import im.vector.riotx.features.version.VersionProvider import okhttp3.* import org.json.JSONException import org.json.JSONObject @@ -51,7 +51,8 @@ import javax.inject.Singleton * BugReporter creates and sends the bug reports. */ @Singleton -class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSessionHolder) { +class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, + private val versionProvider: VersionProvider) { var inMultiWindowMode = false companion object { @@ -225,7 +226,7 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes .addFormDataPart("user_agent", Matrix.getInstance(context).getUserAgent()) .addFormDataPart("user_id", userId) .addFormDataPart("device_id", deviceId) - .addFormDataPart("version", getVersion(longFormat = true, useBuildNumber = false)) + .addFormDataPart("version", versionProvider.getVersion(longFormat = true, useBuildNumber = false)) .addFormDataPart("branch_name", context.getString(R.string.git_branch_name)) .addFormDataPart("matrix_sdk_version", Matrix.getSdkVersion()) .addFormDataPart("olm_version", olmVersion) diff --git a/vector/src/main/java/im/vector/riotx/features/rageshake/VectorUncaughtExceptionHandler.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/VectorUncaughtExceptionHandler.kt index 86e4d9ee..89e386e4 100644 --- a/vector/src/main/java/im/vector/riotx/features/rageshake/VectorUncaughtExceptionHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/rageshake/VectorUncaughtExceptionHandler.kt @@ -21,8 +21,8 @@ import android.os.Build import androidx.core.content.edit import androidx.preference.PreferenceManager import im.vector.matrix.android.api.Matrix -import im.vector.riotx.BuildConfig -import im.vector.riotx.features.version.getVersion +import im.vector.riotx.core.resources.VersionCodeProvider +import im.vector.riotx.features.version.VersionProvider import timber.log.Timber import java.io.PrintWriter import java.io.StringWriter @@ -30,16 +30,15 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class VectorUncaughtExceptionHandler @Inject constructor(private val bugReporter: BugReporter) : Thread.UncaughtExceptionHandler { +class VectorUncaughtExceptionHandler @Inject constructor(private val bugReporter: BugReporter, + private val versionProvider: VersionProvider, + private val versionCodeProvider: VersionCodeProvider) : Thread.UncaughtExceptionHandler { // key to save the crash status companion object { private const val PREFS_CRASH_KEY = "PREFS_CRASH_KEY" } - private val vectorVersion = getVersion(longFormat = true, useBuildNumber = true) - private val matrixSdkVersion = Matrix.getSdkVersion() - private var previousHandler: Thread.UncaughtExceptionHandler? = null private lateinit var context: Context @@ -68,9 +67,9 @@ class VectorUncaughtExceptionHandler @Inject constructor(private val bugReporter val b = StringBuilder() val appName = "RiotX" // TODO Matrix.getApplicationName() - b.append(appName + " Build : " + BuildConfig.VERSION_CODE + "\n") - b.append("$appName Version : $vectorVersion\n") - b.append("SDK Version : $matrixSdkVersion\n") + b.append(appName + " Build : " + versionCodeProvider.getVersionCode() + "\n") + b.append("$appName Version : ${versionProvider.getVersion(longFormat = true, useBuildNumber = true)}\n") + b.append("SDK Version : ${Matrix.getSdkVersion()}\n") b.append("Phone : " + Build.MODEL.trim() + " (" + Build.VERSION.INCREMENTAL + " " + Build.VERSION.RELEASE + " " + Build.VERSION.CODENAME + ")\n") b.append("Memory statuses \n") @@ -94,14 +93,6 @@ class VectorUncaughtExceptionHandler @Inject constructor(private val bugReporter b.append("Thread: ") b.append(thread.name) - /* - val a = VectorApp.getCurrentActivity() - if (a != null) { - b.append(", Activity:") - b.append(a.localClassName) - } - */ - b.append(", Exception: ") val sw = StringWriter() diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt index c47e8bbd..bf36fb26 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt @@ -31,6 +31,7 @@ import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRooms import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.rx.rx +import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.utils.LiveEvent import timber.log.Timber @@ -199,7 +200,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: ) } - session.joinRoom(publicRoom.roomId, object : MatrixCallback { + session.joinRoom(publicRoom.roomId, emptyList(), object : MatrixCallback { override fun onSuccess(data: Unit) { // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // Instead, we wait for the room to be joined @@ -207,7 +208,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: override fun onFailure(failure: Throwable) { // Notify the user - _joinRoomErrorLiveData.postValue(LiveEvent(failure)) + _joinRoomErrorLiveData.postLiveEvent(failure) setState { copy( diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt index bf5fa743..964f30d3 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt @@ -90,7 +90,7 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: R ) } - session.joinRoom(state.roomId, object : MatrixCallback { + session.joinRoom(state.roomId, emptyList(), object : MatrixCallback { override fun onSuccess(data: Unit) { // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // Instead, we wait for the room to be joined 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 b607b5a6..8ba6c7ed 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 @@ -31,187 +31,192 @@ import im.vector.riotx.features.themes.ThemeUtils import timber.log.Timber import java.io.File import java.util.* +import javax.inject.Inject -object VectorPreferences { +class VectorPreferences @Inject constructor(private val context: Context) { - const val SETTINGS_MESSAGES_SENT_BY_BOT_PREFERENCE_KEY = "SETTINGS_MESSAGES_SENT_BY_BOT_PREFERENCE_KEY_2" - const val SETTINGS_CHANGE_PASSWORD_PREFERENCE_KEY = "SETTINGS_CHANGE_PASSWORD_PREFERENCE_KEY" - const val SETTINGS_VERSION_PREFERENCE_KEY = "SETTINGS_VERSION_PREFERENCE_KEY" - const val SETTINGS_SDK_VERSION_PREFERENCE_KEY = "SETTINGS_SDK_VERSION_PREFERENCE_KEY" - const val SETTINGS_OLM_VERSION_PREFERENCE_KEY = "SETTINGS_OLM_VERSION_PREFERENCE_KEY" - const val SETTINGS_LOGGED_IN_PREFERENCE_KEY = "SETTINGS_LOGGED_IN_PREFERENCE_KEY" - const val SETTINGS_HOME_SERVER_PREFERENCE_KEY = "SETTINGS_HOME_SERVER_PREFERENCE_KEY" - const val SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY = "SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY" - const val SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY = "SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY" - const val SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY = "SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY" + companion object { + const val SETTINGS_MESSAGES_SENT_BY_BOT_PREFERENCE_KEY = "SETTINGS_MESSAGES_SENT_BY_BOT_PREFERENCE_KEY_2" + const val SETTINGS_CHANGE_PASSWORD_PREFERENCE_KEY = "SETTINGS_CHANGE_PASSWORD_PREFERENCE_KEY" + const val SETTINGS_VERSION_PREFERENCE_KEY = "SETTINGS_VERSION_PREFERENCE_KEY" + const val SETTINGS_SDK_VERSION_PREFERENCE_KEY = "SETTINGS_SDK_VERSION_PREFERENCE_KEY" + const val SETTINGS_OLM_VERSION_PREFERENCE_KEY = "SETTINGS_OLM_VERSION_PREFERENCE_KEY" + const val SETTINGS_LOGGED_IN_PREFERENCE_KEY = "SETTINGS_LOGGED_IN_PREFERENCE_KEY" + const val SETTINGS_HOME_SERVER_PREFERENCE_KEY = "SETTINGS_HOME_SERVER_PREFERENCE_KEY" + const val SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY = "SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY" + const val SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY = "SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY" + const val SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY = "SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY" - const val SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY" - const val SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY" - const val SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY = "SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY" - const val SETTINGS_OTHER_THIRD_PARTY_NOTICES_PREFERENCE_KEY = "SETTINGS_OTHER_THIRD_PARTY_NOTICES_PREFERENCE_KEY" - const val SETTINGS_COPYRIGHT_PREFERENCE_KEY = "SETTINGS_COPYRIGHT_PREFERENCE_KEY" - const val SETTINGS_CLEAR_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_CACHE_PREFERENCE_KEY" - const val SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY" - const val SETTINGS_USER_SETTINGS_PREFERENCE_KEY = "SETTINGS_USER_SETTINGS_PREFERENCE_KEY" - const val SETTINGS_CONTACT_PREFERENCE_KEYS = "SETTINGS_CONTACT_PREFERENCE_KEYS" - const val SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY = "SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY" - const val SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY = "SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY" - const val SETTINGS_IGNORED_USERS_PREFERENCE_KEY = "SETTINGS_IGNORED_USERS_PREFERENCE_KEY" - const val SETTINGS_IGNORE_USERS_DIVIDER_PREFERENCE_KEY = "SETTINGS_IGNORE_USERS_DIVIDER_PREFERENCE_KEY" - const val SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY = "SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY" - const val SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY = "SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY" - const val SETTINGS_LABS_PREFERENCE_KEY = "SETTINGS_LABS_PREFERENCE_KEY" - const val SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY" - const val SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY" - const val SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY" - const val SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY" - const val SETTINGS_DEVICES_LIST_PREFERENCE_KEY = "SETTINGS_DEVICES_LIST_PREFERENCE_KEY" - const val SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY = "SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY" - const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY" - const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY" - const val SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY" - const val SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY" - const val SETTINGS_ENCRYPTION_EXPORT_E2E_ROOM_KEYS_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_EXPORT_E2E_ROOM_KEYS_PREFERENCE_KEY" - const val SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY" - const val SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY" - const val SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY" + const val SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY" + const val SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY" + const val SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY = "SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY" + const val SETTINGS_OTHER_THIRD_PARTY_NOTICES_PREFERENCE_KEY = "SETTINGS_OTHER_THIRD_PARTY_NOTICES_PREFERENCE_KEY" + const val SETTINGS_COPYRIGHT_PREFERENCE_KEY = "SETTINGS_COPYRIGHT_PREFERENCE_KEY" + const val SETTINGS_CLEAR_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_CACHE_PREFERENCE_KEY" + const val SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY" + const val SETTINGS_USER_SETTINGS_PREFERENCE_KEY = "SETTINGS_USER_SETTINGS_PREFERENCE_KEY" + const val SETTINGS_CONTACT_PREFERENCE_KEYS = "SETTINGS_CONTACT_PREFERENCE_KEYS" + const val SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY = "SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY" + const val SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY = "SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY" + const val SETTINGS_IGNORED_USERS_PREFERENCE_KEY = "SETTINGS_IGNORED_USERS_PREFERENCE_KEY" + const val SETTINGS_IGNORE_USERS_DIVIDER_PREFERENCE_KEY = "SETTINGS_IGNORE_USERS_DIVIDER_PREFERENCE_KEY" + const val SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY = "SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY" + const val SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY = "SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY" + const val SETTINGS_LABS_PREFERENCE_KEY = "SETTINGS_LABS_PREFERENCE_KEY" + const val SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY" + const val SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY" + const val SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY" + const val SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY" + const val SETTINGS_DEVICES_LIST_PREFERENCE_KEY = "SETTINGS_DEVICES_LIST_PREFERENCE_KEY" + const val SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY = "SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY" + const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY" + const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY" + const val SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY" + const val SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY" + const val SETTINGS_ENCRYPTION_EXPORT_E2E_ROOM_KEYS_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_EXPORT_E2E_ROOM_KEYS_PREFERENCE_KEY" + const val SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY" + const val SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY" + const val SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY" - const val SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY = "SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY" + const val SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY = "SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY" - // user - const val SETTINGS_DISPLAY_NAME_PREFERENCE_KEY = "SETTINGS_DISPLAY_NAME_PREFERENCE_KEY" - const val SETTINGS_PROFILE_PICTURE_PREFERENCE_KEY = "SETTINGS_PROFILE_PICTURE_PREFERENCE_KEY" + // user + const val SETTINGS_DISPLAY_NAME_PREFERENCE_KEY = "SETTINGS_DISPLAY_NAME_PREFERENCE_KEY" + const val SETTINGS_PROFILE_PICTURE_PREFERENCE_KEY = "SETTINGS_PROFILE_PICTURE_PREFERENCE_KEY" - // contacts - const val SETTINGS_CONTACTS_PHONEBOOK_COUNTRY_PREFERENCE_KEY = "SETTINGS_CONTACTS_PHONEBOOK_COUNTRY_PREFERENCE_KEY" + // contacts + const val SETTINGS_CONTACTS_PHONEBOOK_COUNTRY_PREFERENCE_KEY = "SETTINGS_CONTACTS_PHONEBOOK_COUNTRY_PREFERENCE_KEY" - // interface - const val SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY = "SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY" - const val SETTINGS_INTERFACE_TEXT_SIZE_KEY = "SETTINGS_INTERFACE_TEXT_SIZE_KEY" - const val SETTINGS_SHOW_URL_PREVIEW_KEY = "SETTINGS_SHOW_URL_PREVIEW_KEY" - private const val SETTINGS_SEND_TYPING_NOTIF_KEY = "SETTINGS_SEND_TYPING_NOTIF_KEY" - private const val SETTINGS_ENABLE_MARKDOWN_KEY = "SETTINGS_ENABLE_MARKDOWN_KEY" - 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_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" - private const val SETTINGS_SEND_MESSAGE_WITH_ENTER = "SETTINGS_SEND_MESSAGE_WITH_ENTER" + // interface + const val SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY = "SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY" + const val SETTINGS_INTERFACE_TEXT_SIZE_KEY = "SETTINGS_INTERFACE_TEXT_SIZE_KEY" + const val SETTINGS_SHOW_URL_PREVIEW_KEY = "SETTINGS_SHOW_URL_PREVIEW_KEY" + private const val SETTINGS_SEND_TYPING_NOTIF_KEY = "SETTINGS_SEND_TYPING_NOTIF_KEY" + private const val SETTINGS_ENABLE_MARKDOWN_KEY = "SETTINGS_ENABLE_MARKDOWN_KEY" + 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_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" + private const val SETTINGS_SEND_MESSAGE_WITH_ENTER = "SETTINGS_SEND_MESSAGE_WITH_ENTER" - // home - private const val SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY = "SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY" - private const val SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY = "SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY" + // home + private const val SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY = "SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY" + private const val SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY = "SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY" - // flair - const val SETTINGS_GROUPS_FLAIR_KEY = "SETTINGS_GROUPS_FLAIR_KEY" + // flair + const val SETTINGS_GROUPS_FLAIR_KEY = "SETTINGS_GROUPS_FLAIR_KEY" - // notifications - const val SETTINGS_NOTIFICATIONS_KEY = "SETTINGS_NOTIFICATIONS_KEY" - const val SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY = "SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY" - const val SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY = "SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY" - // public static final String SETTINGS_TURN_SCREEN_ON_PREFERENCE_KEY = "SETTINGS_TURN_SCREEN_ON_PREFERENCE_KEY"; - const val SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY = "SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY" - const val SETTINGS_SYSTEM_NOISY_NOTIFICATION_PREFERENCE_KEY = "SETTINGS_SYSTEM_NOISY_NOTIFICATION_PREFERENCE_KEY" - const val SETTINGS_SYSTEM_SILENT_NOTIFICATION_PREFERENCE_KEY = "SETTINGS_SYSTEM_SILENT_NOTIFICATION_PREFERENCE_KEY" - const val SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY" - const val SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY" - const val SETTINGS_CONTAINING_MY_DISPLAY_NAME_PREFERENCE_KEY = "SETTINGS_CONTAINING_MY_DISPLAY_NAME_PREFERENCE_KEY_2" - const val SETTINGS_CONTAINING_MY_USER_NAME_PREFERENCE_KEY = "SETTINGS_CONTAINING_MY_USER_NAME_PREFERENCE_KEY_2" - const val SETTINGS_MESSAGES_IN_ONE_TO_ONE_PREFERENCE_KEY = "SETTINGS_MESSAGES_IN_ONE_TO_ONE_PREFERENCE_KEY_2" - const val SETTINGS_MESSAGES_IN_GROUP_CHAT_PREFERENCE_KEY = "SETTINGS_MESSAGES_IN_GROUP_CHAT_PREFERENCE_KEY_2" - const val SETTINGS_INVITED_TO_ROOM_PREFERENCE_KEY = "SETTINGS_INVITED_TO_ROOM_PREFERENCE_KEY_2" - const val SETTINGS_CALL_INVITATIONS_PREFERENCE_KEY = "SETTINGS_CALL_INVITATIONS_PREFERENCE_KEY_2" + // notifications + const val SETTINGS_NOTIFICATIONS_KEY = "SETTINGS_NOTIFICATIONS_KEY" + const val SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY = "SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY" + const val SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY = "SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY" + // public static final String SETTINGS_TURN_SCREEN_ON_PREFERENCE_KEY = "SETTINGS_TURN_SCREEN_ON_PREFERENCE_KEY"; + const val SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY = "SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY" + const val SETTINGS_SYSTEM_NOISY_NOTIFICATION_PREFERENCE_KEY = "SETTINGS_SYSTEM_NOISY_NOTIFICATION_PREFERENCE_KEY" + const val SETTINGS_SYSTEM_SILENT_NOTIFICATION_PREFERENCE_KEY = "SETTINGS_SYSTEM_SILENT_NOTIFICATION_PREFERENCE_KEY" + const val SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY" + const val SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY" + const val SETTINGS_CONTAINING_MY_DISPLAY_NAME_PREFERENCE_KEY = "SETTINGS_CONTAINING_MY_DISPLAY_NAME_PREFERENCE_KEY_2" + const val SETTINGS_CONTAINING_MY_USER_NAME_PREFERENCE_KEY = "SETTINGS_CONTAINING_MY_USER_NAME_PREFERENCE_KEY_2" + const val SETTINGS_MESSAGES_IN_ONE_TO_ONE_PREFERENCE_KEY = "SETTINGS_MESSAGES_IN_ONE_TO_ONE_PREFERENCE_KEY_2" + const val SETTINGS_MESSAGES_IN_GROUP_CHAT_PREFERENCE_KEY = "SETTINGS_MESSAGES_IN_GROUP_CHAT_PREFERENCE_KEY_2" + const val SETTINGS_INVITED_TO_ROOM_PREFERENCE_KEY = "SETTINGS_INVITED_TO_ROOM_PREFERENCE_KEY_2" + const val SETTINGS_CALL_INVITATIONS_PREFERENCE_KEY = "SETTINGS_CALL_INVITATIONS_PREFERENCE_KEY_2" - // media - private const val SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY = "SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY" - private const val SETTINGS_DEFAULT_MEDIA_SOURCE_KEY = "SETTINGS_DEFAULT_MEDIA_SOURCE_KEY" - private const val SETTINGS_PREVIEW_MEDIA_BEFORE_SENDING_KEY = "SETTINGS_PREVIEW_MEDIA_BEFORE_SENDING_KEY" - private const val SETTINGS_PLAY_SHUTTER_SOUND_KEY = "SETTINGS_PLAY_SHUTTER_SOUND_KEY" + // media + private const val SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY = "SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY" + private const val SETTINGS_DEFAULT_MEDIA_SOURCE_KEY = "SETTINGS_DEFAULT_MEDIA_SOURCE_KEY" + private const val SETTINGS_PREVIEW_MEDIA_BEFORE_SENDING_KEY = "SETTINGS_PREVIEW_MEDIA_BEFORE_SENDING_KEY" + private const val SETTINGS_PLAY_SHUTTER_SOUND_KEY = "SETTINGS_PLAY_SHUTTER_SOUND_KEY" - // background sync - const val SETTINGS_START_ON_BOOT_PREFERENCE_KEY = "SETTINGS_START_ON_BOOT_PREFERENCE_KEY" - const val SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY = "SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY" - const val SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY = "SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY" - const val SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY = "SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY" + // background sync + const val SETTINGS_START_ON_BOOT_PREFERENCE_KEY = "SETTINGS_START_ON_BOOT_PREFERENCE_KEY" + const val SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY = "SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY" + const val SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY = "SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY" + const val SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY = "SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY" - // Calls - const val SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY = "SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY" - const val SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY = "SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY" + // Calls + const val SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY = "SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY" + const val SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY = "SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY" - // labs - const val SETTINGS_LAZY_LOADING_PREFERENCE_KEY = "SETTINGS_LAZY_LOADING_PREFERENCE_KEY" - const val SETTINGS_USER_REFUSED_LAZY_LOADING_PREFERENCE_KEY = "SETTINGS_USER_REFUSED_LAZY_LOADING_PREFERENCE_KEY" - const val SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY = "SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY" - private const val SETTINGS_USE_JITSI_CONF_PREFERENCE_KEY = "SETTINGS_USE_JITSI_CONF_PREFERENCE_KEY" - private const val SETTINGS_USE_NATIVE_CAMERA_PREFERENCE_KEY = "SETTINGS_USE_NATIVE_CAMERA_PREFERENCE_KEY" - private const val SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY = "SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY" + // labs + const val SETTINGS_LAZY_LOADING_PREFERENCE_KEY = "SETTINGS_LAZY_LOADING_PREFERENCE_KEY" + const val SETTINGS_USER_REFUSED_LAZY_LOADING_PREFERENCE_KEY = "SETTINGS_USER_REFUSED_LAZY_LOADING_PREFERENCE_KEY" + const val SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY = "SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY" + private const val SETTINGS_USE_JITSI_CONF_PREFERENCE_KEY = "SETTINGS_USE_JITSI_CONF_PREFERENCE_KEY" + private const val SETTINGS_USE_NATIVE_CAMERA_PREFERENCE_KEY = "SETTINGS_USE_NATIVE_CAMERA_PREFERENCE_KEY" + private const val SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY = "SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY" - private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY" - private const val SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY" + private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY" + private const val SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY" - // analytics - const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY" - const val SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY" + // analytics + const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY" + const val SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY" - // other - const val SETTINGS_MEDIA_SAVING_PERIOD_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_KEY" - private const val SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY" - private const val DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY = "DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY" - private const val DID_MIGRATE_TO_NOTIFICATION_REWORK = "DID_MIGRATE_TO_NOTIFICATION_REWORK" - private const val DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY = "DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY" - const val SETTINGS_DEACTIVATE_ACCOUNT_KEY = "SETTINGS_DEACTIVATE_ACCOUNT_KEY" - private const val SETTINGS_DISPLAY_ALL_EVENTS_KEY = "SETTINGS_DISPLAY_ALL_EVENTS_KEY" + // other + const val SETTINGS_MEDIA_SAVING_PERIOD_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_KEY" + private const val SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY" + private const val DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY = "DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY" + private const val DID_MIGRATE_TO_NOTIFICATION_REWORK = "DID_MIGRATE_TO_NOTIFICATION_REWORK" + private const val DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY = "DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY" + const val SETTINGS_DEACTIVATE_ACCOUNT_KEY = "SETTINGS_DEACTIVATE_ACCOUNT_KEY" + private const val SETTINGS_DISPLAY_ALL_EVENTS_KEY = "SETTINGS_DISPLAY_ALL_EVENTS_KEY" - private const val MEDIA_SAVING_3_DAYS = 0 - private const val MEDIA_SAVING_1_WEEK = 1 - private const val MEDIA_SAVING_1_MONTH = 2 - private const val MEDIA_SAVING_FOREVER = 3 + private const val MEDIA_SAVING_3_DAYS = 0 + private const val MEDIA_SAVING_1_WEEK = 1 + private const val MEDIA_SAVING_1_MONTH = 2 + private const val MEDIA_SAVING_FOREVER = 3 - // some preferences keys must be kept after a logout - private val mKeysToKeepAfterLogout = Arrays.asList( - SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY, - SETTINGS_DEFAULT_MEDIA_SOURCE_KEY, - SETTINGS_PLAY_SHUTTER_SOUND_KEY, + // some preferences keys must be kept after a logout + private val mKeysToKeepAfterLogout = Arrays.asList( + SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY, + SETTINGS_DEFAULT_MEDIA_SOURCE_KEY, + SETTINGS_PLAY_SHUTTER_SOUND_KEY, - SETTINGS_SEND_TYPING_NOTIF_KEY, - SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY, - SETTINGS_12_24_TIMESTAMPS_KEY, - SETTINGS_SHOW_READ_RECEIPTS_KEY, - SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY, - SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY, - SETTINGS_MEDIA_SAVING_PERIOD_KEY, - SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY, - SETTINGS_PREVIEW_MEDIA_BEFORE_SENDING_KEY, - SETTINGS_SEND_MESSAGE_WITH_ENTER, + SETTINGS_SEND_TYPING_NOTIF_KEY, + SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY, + SETTINGS_12_24_TIMESTAMPS_KEY, + SETTINGS_SHOW_READ_RECEIPTS_KEY, + SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY, + SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY, + SETTINGS_MEDIA_SAVING_PERIOD_KEY, + SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY, + SETTINGS_PREVIEW_MEDIA_BEFORE_SENDING_KEY, + SETTINGS_SEND_MESSAGE_WITH_ENTER, - SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY, - SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY, - // Do not keep SETTINGS_LAZY_LOADING_PREFERENCE_KEY because the user may log in on a server which does not support lazy loading - SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY, - SETTINGS_START_ON_BOOT_PREFERENCE_KEY, - SETTINGS_INTERFACE_TEXT_SIZE_KEY, - SETTINGS_USE_JITSI_CONF_PREFERENCE_KEY, - SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY, - SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY, + SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY, + SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY, + // Do not keep SETTINGS_LAZY_LOADING_PREFERENCE_KEY because the user may log in on a server which does not support lazy loading + SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY, + SETTINGS_START_ON_BOOT_PREFERENCE_KEY, + SETTINGS_INTERFACE_TEXT_SIZE_KEY, + SETTINGS_USE_JITSI_CONF_PREFERENCE_KEY, + SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY, + SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY, - SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY, - SETTINGS_CONTACTS_PHONEBOOK_COUNTRY_PREFERENCE_KEY, - SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY, - SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY, - SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY, - SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY, - SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY, + SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY, + SETTINGS_CONTACTS_PHONEBOOK_COUNTRY_PREFERENCE_KEY, + SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY, + SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY, + SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY, + SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY, + SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY, - SETTINGS_USE_RAGE_SHAKE_KEY - ) + SETTINGS_USE_RAGE_SHAKE_KEY + ) + } + + private val defaultPrefs = PreferenceManager.getDefaultSharedPreferences(context) /** * Clear the preferences. * * @param context the context */ - fun clearPreferences(context: Context) { + fun clearPreferences() { val keysToKeep = HashSet(mKeysToKeepAfterLogout) // home server urls @@ -221,37 +226,35 @@ object VectorPreferences { // theme keysToKeep.add(ThemeUtils.APPLICATION_THEME_KEY) - val preferences = PreferenceManager.getDefaultSharedPreferences(context) - preferences.edit { - // get all the existing keys - val keys = preferences.all.keys - // remove the one to keep + // get all the existing keys + val keys = defaultPrefs.all.keys - keys.removeAll(keysToKeep) + // remove the one to keep + keys.removeAll(keysToKeep) + defaultPrefs.edit { for (key in keys) { remove(key) } } } - fun areNotificationEnabledForDevice(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY, true) + fun areNotificationEnabledForDevice(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY, true) } - fun setNotificationEnabledForDevice(context: Context, enabled: Boolean?) { - PreferenceManager.getDefaultSharedPreferences(context) - .edit { - putBoolean(SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY, enabled!!) - } + fun setNotificationEnabledForDevice(enabled: Boolean?) { + defaultPrefs.edit { + putBoolean(SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY, enabled!!) + } } - fun shouldShowHiddenEvents(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY, false) + fun shouldShowHiddenEvents(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY, false) } - fun swipeToReplyIsEnabled(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY, true) + fun swipeToReplyIsEnabled(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY, true) } /** @@ -260,8 +263,8 @@ object VectorPreferences { * @param context the context * @return true if it was already requested */ - fun didAskUserToIgnoreBatteryOptimizations(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY, false) + fun didAskUserToIgnoreBatteryOptimizations(): Boolean { + return defaultPrefs.getBoolean(DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY, false) } /** @@ -269,22 +272,20 @@ object VectorPreferences { * * @param context the context */ - fun setDidAskUserToIgnoreBatteryOptimizations(context: Context) { - PreferenceManager.getDefaultSharedPreferences(context) - .edit { - putBoolean(DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY, true) - } + fun setDidAskUserToIgnoreBatteryOptimizations() { + defaultPrefs.edit { + putBoolean(DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY, true) + } } - fun didMigrateToNotificationRework(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(DID_MIGRATE_TO_NOTIFICATION_REWORK, false) + fun didMigrateToNotificationRework(): Boolean { + return defaultPrefs.getBoolean(DID_MIGRATE_TO_NOTIFICATION_REWORK, false) } - fun setDidMigrateToNotificationRework(context: Context) { - PreferenceManager.getDefaultSharedPreferences(context) - .edit { - putBoolean(DID_MIGRATE_TO_NOTIFICATION_REWORK, true) - } + fun setDidMigrateToNotificationRework() { + defaultPrefs.edit { + putBoolean(DID_MIGRATE_TO_NOTIFICATION_REWORK, true) + } } /** @@ -293,8 +294,8 @@ object VectorPreferences { * @param context the context * @return true if the time must be displayed in 12h format */ - fun displayTimeIn12hFormat(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_12_24_TIMESTAMPS_KEY, false) + fun displayTimeIn12hFormat(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_12_24_TIMESTAMPS_KEY, false) } /** @@ -303,8 +304,8 @@ object VectorPreferences { * @param context the context * @return true if the join and leave membership events should be shown in the messages list */ - fun showJoinLeaveMessages(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY, true) + fun showJoinLeaveMessages(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY, true) } /** @@ -313,8 +314,8 @@ object VectorPreferences { * @param context the context * @return true true if the avatar and display name events should be shown in the messages list. */ - fun showAvatarDisplayNameChangeMessages(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY, true) + fun showAvatarDisplayNameChangeMessages(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY, true) } /** @@ -323,8 +324,8 @@ object VectorPreferences { * @param context the context * @return true to use the native camera app to record video or take photo. */ - fun useNativeCamera(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_NATIVE_CAMERA_PREFERENCE_KEY, false) + fun useNativeCamera(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_USE_NATIVE_CAMERA_PREFERENCE_KEY, false) } /** @@ -333,8 +334,8 @@ object VectorPreferences { * @param context the context * @return true if the send voice feature is enabled. */ - fun isSendVoiceFeatureEnabled(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY, false) + fun isSendVoiceFeatureEnabled(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY, false) } /** @@ -343,8 +344,8 @@ object VectorPreferences { * @param context the context * @return the selected compression level */ - fun getSelectedDefaultMediaCompressionLevel(context: Context): Int { - return Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(context).getString(SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY, "0")!!) + fun getSelectedDefaultMediaCompressionLevel(): Int { + return Integer.parseInt(defaultPrefs.getString(SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY, "0")!!) } /** @@ -353,8 +354,8 @@ object VectorPreferences { * @param context the context * @return the selected media source */ - fun getSelectedDefaultMediaSource(context: Context): Int { - return Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(context).getString(SETTINGS_DEFAULT_MEDIA_SOURCE_KEY, "0")!!) + fun getSelectedDefaultMediaSource(): Int { + return Integer.parseInt(defaultPrefs.getString(SETTINGS_DEFAULT_MEDIA_SOURCE_KEY, "0")!!) } /** @@ -363,8 +364,8 @@ object VectorPreferences { * @param context the context * @return true if shutter sound should play */ - fun useShutterSound(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_PLAY_SHUTTER_SOUND_KEY, true) + fun useShutterSound(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_PLAY_SHUTTER_SOUND_KEY, true) } /** @@ -373,9 +374,8 @@ object VectorPreferences { * @param context the context * @param uri the new notification ringtone, or null for no RingTone */ - fun setNotificationRingTone(context: Context, uri: Uri?) { - PreferenceManager.getDefaultSharedPreferences(context).edit { - + fun setNotificationRingTone(uri: Uri?) { + defaultPrefs.edit { var value = "" if (null != uri) { @@ -399,8 +399,8 @@ object VectorPreferences { * @param context the context * @return the selected ring tone or null for no RingTone */ - fun getNotificationRingTone(context: Context): Uri? { - val url = PreferenceManager.getDefaultSharedPreferences(context).getString(SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY, null) + fun getNotificationRingTone(): Uri? { + val url = defaultPrefs.getString(SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY, null) // the user selects "None" if (TextUtils.equals(url, "")) { @@ -433,8 +433,8 @@ object VectorPreferences { * @param context the context * @return the filename or null if "None" is selected */ - fun getNotificationRingToneName(context: Context): String? { - val toneUri = getNotificationRingTone(context) ?: return null + fun getNotificationRingToneName(): String? { + val toneUri = getNotificationRingTone() ?: return null var name: String? = null @@ -467,11 +467,10 @@ object VectorPreferences { * @param context the context * @param newValue true to enable lazy loading, false to disable it */ - fun setUseLazyLoading(context: Context, newValue: Boolean) { - PreferenceManager.getDefaultSharedPreferences(context) - .edit { - putBoolean(SETTINGS_LAZY_LOADING_PREFERENCE_KEY, newValue) - } + fun setUseLazyLoading(newValue: Boolean) { + defaultPrefs.edit { + putBoolean(SETTINGS_LAZY_LOADING_PREFERENCE_KEY, newValue) + } } /** @@ -480,8 +479,8 @@ object VectorPreferences { * @param context the context * @return true if the lazy loading of room members is enabled */ - fun useLazyLoading(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_LAZY_LOADING_PREFERENCE_KEY, false) + fun useLazyLoading(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_LAZY_LOADING_PREFERENCE_KEY, false) } /** @@ -489,11 +488,10 @@ object VectorPreferences { * * @param context the context */ - fun setUserRefuseLazyLoading(context: Context) { - PreferenceManager.getDefaultSharedPreferences(context) - .edit { - putBoolean(SETTINGS_USER_REFUSED_LAZY_LOADING_PREFERENCE_KEY, true) - } + fun setUserRefuseLazyLoading() { + defaultPrefs.edit { + putBoolean(SETTINGS_USER_REFUSED_LAZY_LOADING_PREFERENCE_KEY, true) + } } /** @@ -502,8 +500,8 @@ object VectorPreferences { * @param context the context * @return true if the user has explicitly refuse the lazy loading of room members */ - fun hasUserRefusedLazyLoading(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USER_REFUSED_LAZY_LOADING_PREFERENCE_KEY, false) + fun hasUserRefusedLazyLoading(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_USER_REFUSED_LAZY_LOADING_PREFERENCE_KEY, false) } /** @@ -512,8 +510,8 @@ object VectorPreferences { * @param context the context * @return true if the data save mode is enabled */ - fun useDataSaveMode(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY, false) + fun useDataSaveMode(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY, false) } /** @@ -522,8 +520,8 @@ object VectorPreferences { * @param context the context * @return true if the conference call must be done with jitsi. */ - fun useJitsiConfCall(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_JITSI_CONF_PREFERENCE_KEY, true) + fun useJitsiConfCall(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_USE_JITSI_CONF_PREFERENCE_KEY, true) } /** @@ -532,8 +530,8 @@ object VectorPreferences { * @param context the context * @return true if the application must be started on boot */ - fun autoStartOnBoot(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_START_ON_BOOT_PREFERENCE_KEY, true) + fun autoStartOnBoot(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_START_ON_BOOT_PREFERENCE_KEY, true) } /** @@ -542,11 +540,10 @@ object VectorPreferences { * @param context the context * @param value true to start the application on boot */ - fun setAutoStartOnBoot(context: Context, value: Boolean) { - PreferenceManager.getDefaultSharedPreferences(context) - .edit { - putBoolean(SETTINGS_START_ON_BOOT_PREFERENCE_KEY, value) - } + fun setAutoStartOnBoot(value: Boolean) { + defaultPrefs.edit { + putBoolean(SETTINGS_START_ON_BOOT_PREFERENCE_KEY, value) + } } /** @@ -555,8 +552,8 @@ object VectorPreferences { * @param context the context * @return the selected period */ - fun getSelectedMediasSavingPeriod(context: Context): Int { - return PreferenceManager.getDefaultSharedPreferences(context).getInt(SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY, MEDIA_SAVING_1_WEEK) + fun getSelectedMediasSavingPeriod(): Int { + return defaultPrefs.getInt(SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY, MEDIA_SAVING_1_WEEK) } /** @@ -565,11 +562,10 @@ object VectorPreferences { * @param context the context * @param index the selected period index */ - fun setSelectedMediasSavingPeriod(context: Context, index: Int) { - PreferenceManager.getDefaultSharedPreferences(context) - .edit { - putInt(SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY, index) - } + fun setSelectedMediasSavingPeriod(index: Int) { + defaultPrefs.edit { + putInt(SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY, index) + } } /** @@ -578,8 +574,8 @@ object VectorPreferences { * @param context the context * @return the min last access time (in seconds) */ - fun getMinMediasLastAccessTime(context: Context): Long { - val selection = getSelectedMediasSavingPeriod(context) + fun getMinMediasLastAccessTime(): Long { + val selection = getSelectedMediasSavingPeriod() when (selection) { MEDIA_SAVING_3_DAYS -> return System.currentTimeMillis() / 1000 - 3 * 24 * 60 * 60 @@ -597,8 +593,8 @@ object VectorPreferences { * @param context the context * @return the selected period */ - fun getSelectedMediasSavingPeriodString(context: Context): String { - val selection = getSelectedMediasSavingPeriod(context) + fun getSelectedMediasSavingPeriodString(): String { + val selection = getSelectedMediasSavingPeriod() when (selection) { MEDIA_SAVING_3_DAYS -> return context.getString(R.string.media_saving_period_3_days) @@ -612,7 +608,7 @@ object VectorPreferences { /** * Fix some migration issues */ - fun fixMigrationIssues(context: Context) { + fun fixMigrationIssues() { // Nothing to do for the moment } @@ -622,8 +618,8 @@ object VectorPreferences { * @param context the context * @return true if the markdown is enabled */ - fun isMarkdownEnabled(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_ENABLE_MARKDOWN_KEY, true) + fun isMarkdownEnabled(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_ENABLE_MARKDOWN_KEY, true) } /** @@ -632,11 +628,10 @@ object VectorPreferences { * @param context the context * @param isEnabled true to enable the markdown */ - fun setMarkdownEnabled(context: Context, isEnabled: Boolean) { - PreferenceManager.getDefaultSharedPreferences(context) - .edit { - putBoolean(SETTINGS_ENABLE_MARKDOWN_KEY, isEnabled) - } + fun setMarkdownEnabled(isEnabled: Boolean) { + defaultPrefs.edit { + putBoolean(SETTINGS_ENABLE_MARKDOWN_KEY, isEnabled) + } } /** @@ -645,8 +640,8 @@ object VectorPreferences { * @param context the context * @return true if the read receipts should be shown */ - fun showReadReceipts(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_SHOW_READ_RECEIPTS_KEY, true) + fun showReadReceipts(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_SHOW_READ_RECEIPTS_KEY, true) } /** @@ -655,8 +650,8 @@ object VectorPreferences { * @param context the context * @return true if the message timestamps must be always shown */ - fun alwaysShowTimeStamps(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY, false) + fun alwaysShowTimeStamps(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY, false) } /** @@ -665,8 +660,8 @@ object VectorPreferences { * @param context the context * @return true to send the typing notifs */ - fun sendTypingNotifs(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_SEND_TYPING_NOTIF_KEY, true) + fun sendTypingNotifs(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_SEND_TYPING_NOTIF_KEY, true) } /** @@ -675,8 +670,8 @@ object VectorPreferences { * @param context the context * @return true to move the missed notifications to the left side */ - fun pinMissedNotifications(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY, true) + fun pinMissedNotifications(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY, true) } /** @@ -685,8 +680,8 @@ object VectorPreferences { * @param context the context * @return true to move the unread room to the left side */ - fun pinUnreadMessages(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY, true) + fun pinUnreadMessages(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY, true) } /** @@ -695,8 +690,8 @@ object VectorPreferences { * @param context the context * @return true */ - fun vibrateWhenMentioning(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_VIBRATE_ON_MENTION_KEY, false) + fun vibrateWhenMentioning(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_VIBRATE_ON_MENTION_KEY, false) } /** @@ -705,8 +700,8 @@ object VectorPreferences { * @param context the context * @return true if a dialog has been displayed to ask to use the analytics tracking */ - fun didAskToUseAnalytics(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY, false) + fun didAskToUseAnalytics(): Boolean { + return defaultPrefs.getBoolean(DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY, false) } /** @@ -714,11 +709,10 @@ object VectorPreferences { * * @param context the context */ - fun setDidAskToUseAnalytics(context: Context) { - PreferenceManager.getDefaultSharedPreferences(context) - .edit { - putBoolean(DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY, true) - } + fun setDidAskToUseAnalytics() { + defaultPrefs.edit { + putBoolean(DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY, true) + } } /** @@ -727,8 +721,8 @@ object VectorPreferences { * @param context the context * @return true if the analytics tracking is authorized */ - fun useAnalytics(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_ANALYTICS_KEY, false) + fun useAnalytics(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_USE_ANALYTICS_KEY, false) } /** @@ -737,11 +731,10 @@ object VectorPreferences { * @param context the context * @param useAnalytics true to enable the analytics tracking */ - fun setUseAnalytics(context: Context, useAnalytics: Boolean) { - PreferenceManager.getDefaultSharedPreferences(context) - .edit { - putBoolean(SETTINGS_USE_ANALYTICS_KEY, useAnalytics) - } + fun setUseAnalytics(useAnalytics: Boolean) { + defaultPrefs.edit { + putBoolean(SETTINGS_USE_ANALYTICS_KEY, useAnalytics) + } } /** @@ -750,8 +743,8 @@ object VectorPreferences { * @param context the context * @return true to preview media */ - fun previewMediaWhenSending(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_PREVIEW_MEDIA_BEFORE_SENDING_KEY, false) + fun previewMediaWhenSending(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_PREVIEW_MEDIA_BEFORE_SENDING_KEY, false) } /** @@ -760,8 +753,8 @@ object VectorPreferences { * @param context the context * @return true to send message with enter */ - fun sendMessageWithEnter(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_SEND_MESSAGE_WITH_ENTER, false) + fun sendMessageWithEnter(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_SEND_MESSAGE_WITH_ENTER, false) } /** @@ -770,8 +763,8 @@ object VectorPreferences { * @param context the context * @return true if the rage shake is used */ - fun useRageshake(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, true) + fun useRageshake(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, true) } /** @@ -780,11 +773,10 @@ object VectorPreferences { * @param context the context * @param isEnabled true to enable the rage shake */ - fun setUseRageshake(context: Context, isEnabled: Boolean) { - PreferenceManager.getDefaultSharedPreferences(context) - .edit { - putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, isEnabled) - } + fun setUseRageshake(isEnabled: Boolean) { + defaultPrefs.edit { + putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, isEnabled) + } } /** @@ -793,7 +785,7 @@ object VectorPreferences { * @param context the context * @return true to display all the events even the redacted ones. */ - fun displayAllEvents(context: Context): Boolean { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_DISPLAY_ALL_EVENTS_KEY, false) + fun displayAllEvents(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_DISPLAY_ALL_EVENTS_KEY, false) } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt index 5db218c3..fcf7efa4 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt @@ -17,7 +17,6 @@ package im.vector.riotx.features.settings import android.content.Context import android.content.Intent -import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat @@ -75,32 +74,30 @@ class VectorSettingsActivity : VectorBaseActivity(), } } - override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat?, pref: Preference?): Boolean { - var oFragment: Fragment? = null - - if (VectorPreferences.SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY == pref?.key) { - oFragment = VectorSettingsNotificationsTroubleshootFragment.newInstance(session.myUserId) - } else if (VectorPreferences.SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY == pref?.key) { - oFragment = VectorSettingsAdvancedNotificationPreferenceFragment.newInstance(session.myUserId) - } else { - try { - pref?.fragment?.let { - oFragment = supportFragmentManager.fragmentFactory - .instantiate(classLoader, it) + override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference): Boolean { + val oFragment = when { + VectorPreferences.SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY == pref.key -> + VectorSettingsNotificationsTroubleshootFragment.newInstance(session.myUserId) + VectorPreferences.SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY == pref.key -> + VectorSettingsAdvancedNotificationPreferenceFragment.newInstance(session.myUserId) + else -> + try { + pref.fragment?.let { + supportFragmentManager.fragmentFactory.instantiate(classLoader, it) + } + } catch (e: Throwable) { + showSnackbar(getString(R.string.not_implemented)) + Timber.e(e) + null } - } catch (e: Throwable) { - showSnackbar(getString(R.string.not_implemented)) - Timber.e(e) - } } if (oFragment != null) { - oFragment!!.setTargetFragment(caller, 0) + oFragment.setTargetFragment(caller, 0) // Replace the existing Fragment with the new Fragment supportFragmentManager.beginTransaction() - .setCustomAnimations(R.anim.right_in, R.anim.fade_out, - R.anim.fade_in, R.anim.right_out) - .replace(R.id.vector_settings_page, oFragment!!, pref?.title.toString()) + .setCustomAnimations(R.anim.right_in, R.anim.fade_out, R.anim.fade_in, R.anim.right_out) + .replace(R.id.vector_settings_page, oFragment, pref.title.toString()) .addToBackStack(null) .commit() return true diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt index 5694ecf8..cdd489a9 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt @@ -24,11 +24,13 @@ import androidx.core.content.edit import androidx.preference.Preference import androidx.preference.PreferenceManager import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.withArgs import im.vector.riotx.core.preference.BingRule import im.vector.riotx.core.preference.BingRulePreference import im.vector.riotx.features.notifications.NotificationUtils import im.vector.riotx.features.notifications.supportNotificationChannels +import javax.inject.Inject class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseFragment() { @@ -45,6 +47,13 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseF override val preferenceXmlRes = R.xml.vector_settings_notification_advanced_preferences + @Inject lateinit var vectorPreferences: VectorPreferences + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun bindPref() { val callNotificationsSystemOptions = findPreference(VectorPreferences.SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY) if (supportNotificationChannels()) { @@ -83,13 +92,13 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseF if (supportNotificationChannels()) { ringtonePreference.isVisible = false } else { - ringtonePreference.summary = VectorPreferences.getNotificationRingToneName(requireContext()) + ringtonePreference.summary = vectorPreferences.getNotificationRingToneName() ringtonePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER) intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION) - if (null != VectorPreferences.getNotificationRingTone(requireContext())) { - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, VectorPreferences.getNotificationRingTone(requireContext())) + if (null != vectorPreferences.getNotificationRingTone()) { + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, vectorPreferences.getNotificationRingTone()) } startActivityForResult(intent, REQUEST_NOTIFICATION_RINGTONE) @@ -152,13 +161,12 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseF if (resultCode == Activity.RESULT_OK) { when (requestCode) { REQUEST_NOTIFICATION_RINGTONE -> { - VectorPreferences.setNotificationRingTone(requireContext(), - data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) as Uri?) + vectorPreferences.setNotificationRingTone(data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) as Uri?) // test if the selected ring tone can be played - val notificationRingToneName = VectorPreferences.getNotificationRingToneName(requireContext()) + val notificationRingToneName = vectorPreferences.getNotificationRingToneName() if (null != notificationRingToneName) { - VectorPreferences.setNotificationRingTone(requireContext(), VectorPreferences.getNotificationRingTone(requireContext())) + vectorPreferences.setNotificationRingTone(vectorPreferences.getNotificationRingTone()) findPreference(VectorPreferences.SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY).summary = notificationRingToneName } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsHelpAboutFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsHelpAboutFragment.kt index d27ae075..5408eb4e 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsHelpAboutFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsHelpAboutFragment.kt @@ -23,15 +23,23 @@ import androidx.preference.Preference import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import im.vector.matrix.android.api.Matrix import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.utils.copyToClipboard import im.vector.riotx.core.utils.displayInWebView -import im.vector.riotx.features.version.getVersion +import im.vector.riotx.features.version.VersionProvider +import javax.inject.Inject class VectorSettingsHelpAboutFragment : VectorSettingsBaseFragment() { override var titleRes = R.string.preference_root_help_about override val preferenceXmlRes = R.xml.vector_settings_help_about + @Inject lateinit var versionProvider: VersionProvider + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + override fun bindPref() { // preference to start the App info screen, to facilitate App permissions access findPreference(APP_INFO_LINK_PREFERENCE_KEY) @@ -54,7 +62,7 @@ class VectorSettingsHelpAboutFragment : VectorSettingsBaseFragment() { // application version (findPreference(VectorPreferences.SETTINGS_VERSION_PREFERENCE_KEY)).let { - it.summary = getVersion(longFormat = false, useBuildNumber = true) + it.summary = versionProvider.getVersion(longFormat = false, useBuildNumber = true) it.setOnPreferenceClickListener { pref -> copyToClipboard(requireContext(), pref.summary) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationFragment.kt index f7e3fbfd..6536c170 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationFragment.kt @@ -36,6 +36,7 @@ class VectorSettingsNotificationPreferenceFragment : VectorSettingsBaseFragment( @Inject lateinit var pushManager: PushersManager @Inject lateinit var activeSessionHolder: ActiveSessionHolder + @Inject lateinit var vectorPreferences: VectorPreferences override fun bindPref() { findPreference(VectorPreferences.SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY).let { pref -> @@ -84,7 +85,7 @@ class VectorSettingsNotificationPreferenceFragment : VectorSettingsBaseFragment( val switchPref = preference as SwitchPreference if (switchPref.isChecked) { FcmHelper.getFcmToken(requireContext())?.let { - if (VectorPreferences.areNotificationEnabledForDevice(requireContext())) { + if (vectorPreferences.areNotificationEnabledForDevice()) { pushManager.registerPusherWithFcmKey(it) } } 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 92ea21d0..60b8f6a0 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 @@ -44,6 +44,7 @@ class VectorSettingsPreferencesFragment : VectorSettingsBaseFragment() { } @Inject lateinit var vectorConfiguration: VectorConfiguration + @Inject lateinit var vectorPreferences: VectorPreferences override fun injectWith(injector: ScreenComponent) { injector.inject(this) @@ -113,17 +114,17 @@ class VectorSettingsPreferencesFragment : VectorSettingsBaseFragment() { // update keep medias period findPreference(VectorPreferences.SETTINGS_MEDIA_SAVING_PERIOD_KEY).let { - it.summary = VectorPreferences.getSelectedMediasSavingPeriodString(requireContext()) + it.summary = vectorPreferences.getSelectedMediasSavingPeriodString() it.onPreferenceClickListener = Preference.OnPreferenceClickListener { context?.let { context: Context -> AlertDialog.Builder(context) .setSingleChoiceItems(R.array.media_saving_choice, - VectorPreferences.getSelectedMediasSavingPeriod(context)) { d, n -> - VectorPreferences.setSelectedMediasSavingPeriod(context, n) + vectorPreferences.getSelectedMediasSavingPeriod()) { d, n -> + vectorPreferences.setSelectedMediasSavingPeriod(n) d.cancel() - it.summary = VectorPreferences.getSelectedMediasSavingPeriodString(context) + it.summary = vectorPreferences.getSelectedMediasSavingPeriodString() } .show() } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt index 06a33434..8fd24924 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -42,6 +42,7 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.dialogs.ExportKeysDialog import im.vector.riotx.core.intent.ExternalIntentData import im.vector.riotx.core.intent.analyseIntent @@ -57,6 +58,7 @@ import timber.log.Timber import java.text.DateFormat import java.text.SimpleDateFormat import java.util.* +import javax.inject.Inject class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() { @@ -127,6 +129,12 @@ class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() { findPreference(VectorPreferences.SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY) as SwitchPreference } + @Inject lateinit var vectorPreferences: VectorPreferences + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + override fun bindPref() { // Push target refreshPushersList() @@ -142,20 +150,20 @@ class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() { // Analytics tracking management (findPreference(VectorPreferences.SETTINGS_USE_ANALYTICS_KEY) as SwitchPreference).let { // On if the analytics tracking is activated - it.isChecked = VectorPreferences.useAnalytics(requireContext()) + it.isChecked = vectorPreferences.useAnalytics() it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> - VectorPreferences.setUseAnalytics(requireContext(), newValue as Boolean) + vectorPreferences.setUseAnalytics(newValue as Boolean) true } } // Rageshake Management (findPreference(VectorPreferences.SETTINGS_USE_RAGE_SHAKE_KEY) as SwitchPreference).let { - it.isChecked = VectorPreferences.useRageshake(requireContext()) + it.isChecked = vectorPreferences.useRageshake() it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> - VectorPreferences.setUseRageshake(requireContext(), newValue as Boolean) + vectorPreferences.setUseRageshake(newValue as Boolean) true } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/troubleshoot/TestDeviceSettings.kt b/vector/src/main/java/im/vector/riotx/features/settings/troubleshoot/TestDeviceSettings.kt index bef1c834..c39b9415 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/troubleshoot/TestDeviceSettings.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/troubleshoot/TestDeviceSettings.kt @@ -15,7 +15,6 @@ */ package im.vector.riotx.features.settings.troubleshoot -import androidx.appcompat.app.AppCompatActivity import im.vector.riotx.R import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.settings.VectorPreferences @@ -24,20 +23,20 @@ import javax.inject.Inject /** * Checks if notifications are enable in the system settings for this app. */ -class TestDeviceSettings @Inject constructor(private val context: AppCompatActivity, +class TestDeviceSettings @Inject constructor(private val vectorPreferences: VectorPreferences, private val stringProvider: StringProvider) : TroubleshootTest(R.string.settings_troubleshoot_test_device_settings_title) { override fun perform() { - if (VectorPreferences.areNotificationEnabledForDevice(context)) { + if (vectorPreferences.areNotificationEnabledForDevice()) { description = stringProvider.getString(R.string.settings_troubleshoot_test_device_settings_success) quickFix = null status = TestStatus.SUCCESS } else { quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_device_settings_quickfix) { override fun doFix() { - VectorPreferences.setNotificationEnabledForDevice(context, true) + vectorPreferences.setNotificationEnabledForDevice(true) manager?.retry() } } diff --git a/vector/src/main/java/im/vector/riotx/features/version/Version.kt b/vector/src/main/java/im/vector/riotx/features/version/Version.kt deleted file mode 100644 index c058dc26..00000000 --- a/vector/src/main/java/im/vector/riotx/features/version/Version.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.riotx.features.version - -import im.vector.riotx.BuildConfig - -fun getVersion(longFormat: Boolean, useBuildNumber: Boolean): String { - var result = BuildConfig.VERSION_NAME - - var flavor = BuildConfig.SHORT_FLAVOR_DESCRIPTION - - if (flavor.isNotBlank()) { - flavor += "-" - } - - var gitVersion = BuildConfig.GIT_REVISION - val gitRevisionDate = BuildConfig.GIT_REVISION_DATE - val buildNumber = BuildConfig.BUILD_NUMBER - - var useLongFormat = longFormat - - if (useBuildNumber && buildNumber != "0") { - // It's a build from CI - gitVersion = "b$buildNumber" - useLongFormat = false - } - - result += if (useLongFormat) { - " ($flavor$gitVersion-$gitRevisionDate)" - } else { - " ($flavor$gitVersion)" - } - - return result -} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/version/VersionProvider.kt b/vector/src/main/java/im/vector/riotx/features/version/VersionProvider.kt new file mode 100644 index 00000000..4891abaa --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/version/VersionProvider.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.version + +import im.vector.riotx.BuildConfig +import im.vector.riotx.core.resources.VersionCodeProvider +import javax.inject.Inject + +class VersionProvider @Inject constructor(private val versionCodeProvider: VersionCodeProvider) { + + fun getVersion(longFormat: Boolean, useBuildNumber: Boolean): String { + var result = "${BuildConfig.VERSION_NAME} [${versionCodeProvider.getVersionCode()}]" + + var flavor = BuildConfig.SHORT_FLAVOR_DESCRIPTION + + if (flavor.isNotBlank()) { + flavor += "-" + } + + var gitVersion = BuildConfig.GIT_REVISION + val gitRevisionDate = BuildConfig.GIT_REVISION_DATE + val buildNumber = BuildConfig.BUILD_NUMBER + + var useLongFormat = longFormat + + if (useBuildNumber && buildNumber != "0") { + // It's a build from CI + gitVersion = "b$buildNumber" + useLongFormat = false + } + + result += if (useLongFormat) { + " ($flavor$gitVersion-$gitRevisionDate)" + } else { + " ($flavor$gitVersion)" + } + + return result + } +} \ No newline at end of file diff --git a/vector/src/main/res/drawable/ic_refresh_cw.xml b/vector/src/main/res/drawable/ic_refresh_cw.xml new file mode 100644 index 00000000..72c8bd57 --- /dev/null +++ b/vector/src/main/res/drawable/ic_refresh_cw.xml @@ -0,0 +1,22 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_trash.xml b/vector/src/main/res/drawable/ic_trash.xml new file mode 100644 index 00000000..0be5f42d --- /dev/null +++ b/vector/src/main/res/drawable/ic_trash.xml @@ -0,0 +1,14 @@ + + + diff --git a/vector/src/main/res/drawable/ic_view_edit_history.xml b/vector/src/main/res/drawable/ic_view_edit_history.xml new file mode 100644 index 00000000..f94211d4 --- /dev/null +++ b/vector/src/main/res/drawable/ic_view_edit_history.xml @@ -0,0 +1,46 @@ + + + + + + + diff --git a/vector/src/main/res/drawable/ic_warning_small.xml b/vector/src/main/res/drawable/ic_warning_small.xml new file mode 100644 index 00000000..456491ec --- /dev/null +++ b/vector/src/main/res/drawable/ic_warning_small.xml @@ -0,0 +1,14 @@ + + + diff --git a/vector/src/main/res/layout/activity_image_media_viewer.xml b/vector/src/main/res/layout/activity_image_media_viewer.xml index 61d5d286..cfcfa670 100644 --- a/vector/src/main/res/layout/activity_image_media_viewer.xml +++ b/vector/src/main/res/layout/activity_image_media_viewer.xml @@ -1,5 +1,5 @@ - + android:elevation="4dp" + android:transitionName="toolbar" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - + android:layout_height="0dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/imageMediaViewerToolbar"> - + + + + + + + + - - \ No newline at end of file + android:layout_height="50dp" + android:transitionName="composer" + android:background="?riotx_background" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="parent" /> + \ No newline at end of file diff --git a/vector/src/main/res/layout/activity_room_detail.xml b/vector/src/main/res/layout/activity_room_detail.xml index 1dae010e..a02ff1d1 100644 --- a/vector/src/main/res/layout/activity_room_detail.xml +++ b/vector/src/main/res/layout/activity_room_detail.xml @@ -1,8 +1,14 @@ + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/activity_vector_settings.xml b/vector/src/main/res/layout/activity_vector_settings.xml index 55716157..eaba9b33 100755 --- a/vector/src/main/res/layout/activity_vector_settings.xml +++ b/vector/src/main/res/layout/activity_vector_settings.xml @@ -9,8 +9,10 @@ android:layout_height="match_parent" android:orientation="vertical"> + @@ -19,7 +21,7 @@ android:id="@+id/vector_settings_page" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?android:attr/colorBackground" /> + android:background="?riotx_background" /> diff --git a/vector/src/main/res/layout/bottom_sheet_message_actions.xml b/vector/src/main/res/layout/bottom_sheet_message_actions.xml index 9fadcee1..c7d4f5ac 100644 --- a/vector/src/main/res/layout/bottom_sheet_message_actions.xml +++ b/vector/src/main/res/layout/bottom_sheet_message_actions.xml @@ -87,6 +87,38 @@ tools:text="Friday 8pm" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_create_direct_room_directory_users.xml b/vector/src/main/res/layout/fragment_create_direct_room_directory_users.xml new file mode 100644 index 00000000..8416f35d --- /dev/null +++ b/vector/src/main/res/layout/fragment_create_direct_room_directory_users.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index c0e37139..dee37e9d 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -12,6 +12,7 @@ android:layout_width="0dp" android:layout_height="?actionBarSize" android:elevation="4dp" + android:transitionName="toolbar" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -70,21 +71,62 @@ + + + + + + + + + @@ -101,4 +143,5 @@ app:layout_constraintTop_toBottomOf="@+id/roomToolbar" tools:visibility="visible" /> + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_create_direct_room_letter_header.xml b/vector/src/main/res/layout/item_create_direct_room_letter_header.xml new file mode 100644 index 00000000..80a0fc4c --- /dev/null +++ b/vector/src/main/res/layout/item_create_direct_room_letter_header.xml @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_create_direct_room_user.xml b/vector/src/main/res/layout/item_create_direct_room_user.xml new file mode 100644 index 00000000..fa7e7425 --- /dev/null +++ b/vector/src/main/res/layout/item_create_direct_room_user.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_create.xml b/vector/src/main/res/layout/item_timeline_event_create.xml new file mode 100644 index 00000000..d6fc5e4b --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_create.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_media_message_stub.xml b/vector/src/main/res/layout/item_timeline_event_media_message_stub.xml index 5ea117e3..8fe37379 100644 --- a/vector/src/main/res/layout/item_timeline_event_media_message_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_media_message_stub.xml @@ -15,7 +15,19 @@ app:layout_constraintHorizontal_bias="0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:layout_height="300dp" /> + tools:layout_height="300dp" + tools:src="@tools:sample/backgrounds/scenic" /> + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/menu/menu_timeline.xml b/vector/src/main/res/menu/menu_timeline.xml new file mode 100644 index 00000000..82473540 --- /dev/null +++ b/vector/src/main/res/menu/menu_timeline.xml @@ -0,0 +1,29 @@ + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/menu/vector_create_direct_room.xml b/vector/src/main/res/menu/vector_create_direct_room.xml new file mode 100755 index 00000000..8c6eab1c --- /dev/null +++ b/vector/src/main/res/menu/vector_create_direct_room.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/vector/src/main/res/menu/vector_room_message_settings.xml b/vector/src/main/res/menu/vector_room_message_settings.xml deleted file mode 100755 index 7532fe9d..00000000 --- a/vector/src/main/res/menu/vector_room_message_settings.xml +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/transition/image_preview_transition.xml b/vector/src/main/res/transition/image_preview_transition.xml new file mode 100644 index 00000000..3674324c --- /dev/null +++ b/vector/src/main/res/transition/image_preview_transition.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/vector/src/main/res/values-bg/strings.xml b/vector/src/main/res/values-bg/strings.xml index 9f6fad40..7f93b37a 100644 --- a/vector/src/main/res/values-bg/strings.xml +++ b/vector/src/main/res/values-bg/strings.xml @@ -1517,7 +1517,7 @@ Добре дошли в beta версията! Докато RiotX е в ранен стадий на разработка някои функции може да липсват или да има бъгове. - Play Store описание + Play Store описанието Ако намерите бъгове, моля докладвайте ги от най-горното ляво меню на началния екран и ще ги разрешим възможно най-бързо. Импортирай e2e ключове от файл \"%1$s\". @@ -1550,7 +1550,7 @@ Регистрирай токен - Направи предложение + Изпрати предложение Напишете предложението си долу. Опишете предложението тук Благодаря! Предложението ви беше изпратено @@ -1568,4 +1568,36 @@ \n \nЗасега не всички функции на Riot са налични в RiotX. Основни липсващи (и скоро пристигащи!) функции са: • Създаване на нов профил • Настройки на стаи (показване на членове и т.н.) • Създаване на директни чатове • Обаждания • Приспособления • … + Директни съобщения + + Изчакване… + Шифроване на малката снимка… + Изпращане на малка снимка (%1$s / %2$s) + Шифроване на файл… + Изпращане на файл (%1$s / %2$s) + + Изтегляне на файл %1$s… + Файлът %1$s беше изтеглен! + + (редактирано) + + %1$s за да създадете профил. + Използвай старото приложение + + + Редакции на съобщението + Не са открити редакции + + Филтриране на чатове… + Не можете да намерите това, което търсите\? + Създай нова стая + Изпрати ново директно съобщение + Виж директорията със стаи + + Име или ID (#example:matrix.org) + + Активирай отговаряне от списъка с чатове при приплъзване + + Връзката беше копирана + diff --git a/vector/src/main/res/values-es-rMX/strings.xml b/vector/src/main/res/values-es-rMX/strings.xml index 37c885a9..f9196688 100644 --- a/vector/src/main/res/values-es-rMX/strings.xml +++ b/vector/src/main/res/values-es-rMX/strings.xml @@ -33,8 +33,8 @@ Reportar contenido Llamada activa Llamada de conferencia en proceso.\nUnirse con %1$s o %2$s. - voz - video + Voz + Video No se puede iniciar la llamada, inténtalo más tarde Debido a permisos que faltan, algunas características podrían estar ausentes… Necesitas permiso para realizar una llamada de conferencia en esta sala @@ -69,10 +69,10 @@ Salas - Buscar salas - Buscar favoritos - Buscar personas - Buscar salas + Filtrar salas + Filtrar favoritos + Filtrar personas + Filtrar salas Invitaciones @@ -103,7 +103,7 @@ Por favor describe el error. ¿Qué hiciste? ¿Qué esperabas que sucediera, qué sucedió realmente? Describe tu problema aquí Para diagnosticar problemas, informes de este cliente se enviarán junto a este reporte de error. Este informe, inclusive los registros y la captura de pantalla, no será visible públicamente. Si prefieres solamente enviar el texto de arriba, por favor desmarca: - Pareces estar temblando de frustración. ¿Te gustaría enviar un reporte de error? + Pareces estar temblando de frustración. ¿Te gustaría abrir un reporte de error\? El reporte de error ha sido enviado El reporte de error no se pudo enviar (%s) Progreso (%s%%) @@ -114,7 +114,7 @@ Entrar Sala Nombre - Registrar + Crear Cuenta Iniciar sesión Cerrar sesión Enlace de el Servidor @@ -184,7 +184,7 @@ Inscripción falló: error con la red Inscripción falló Inscripción falló : el correo electrónico ya esta utilizado por otra cuenta - Escribe enlace válido + Escribe un enlace válido Nombre/Contraseña inválido El token de acceso especificado no fue reconocido @@ -234,7 +234,7 @@ Disculpe por la inconveniencia. Llamada Entrante Llamada de Video Entrante Llamada de Voz Entrante - Llamada en Processo + Llamada en Curso… El lado remoto no pudo contestar. Conexión de media falló @@ -258,11 +258,11 @@ Disculpe por la inconveniencia. ¿Permitir el acceso a Riot para leer tus contactos ? - Perdon.. Operación no realizada, debido a permisos faltantes + Perdón. Operación no realizada debido a permisos faltantes Guardado - Guardar en Descargas + ¿Guardar en Descargas\? NO Continuar @@ -370,7 +370,8 @@ Disculpe por la inconveniencia. Razón por reportar este contenido - ¿Desea ocultar todos los mensajes de este usuario? + ¿Desea ocultar todos los mensajes de este usuario\? +\nConsidere que esta acción reiniciará la aplicación y tardar un tiempo. Cancelar Subida Cancelar Descarga @@ -398,7 +399,7 @@ Disculpe por la inconveniencia. Buscar directorio - Buscando directorio.. + Buscando directorio… Destacar @@ -440,7 +441,7 @@ Disculpe por la inconveniencia. Sincronización interna Encender sincronización interna Vencimiento de peticiones de sincronización - El tardar entre dos peticiones de sincronización + Demora entre cada petición de sincronización segundo segundos @@ -487,14 +488,14 @@ Disculpe por la inconveniencia. Por favor vea su correo electrónico y sigue el enlace que contiene. Una vez hecho, escoje Continuar. "No fue posible verificar su dirección de correo. Por favor revise su casilla y siga el enlace que contiene. Una vez hecho, haga clic en continuar" - This email address is already in use - Failed to send email: This email address was not found - This phone number is already in use + La dirección de correo electrónico ya está en uso. + Esta dirección de correo electrónico no fue encontrada. + Este teléfono ya está en uso. Cambiar Contraseña - Contraseña anterior + Contraseña actual Contraseña nueva - confirmar contraseña + Confirmar nueva contraseña La actualización de su contraseña falló Su contraseña fue actualizada ¿Mostrar todo los mensajes de %s? @@ -661,7 +662,7 @@ Dispositivos desconocidos: Search for historical - Sincronizando + Sincronizando… Cargando… Descargar diff --git a/vector/src/main/res/values-es/strings.xml b/vector/src/main/res/values-es/strings.xml index a606294d..ce983090 100644 --- a/vector/src/main/res/values-es/strings.xml +++ b/vector/src/main/res/values-es/strings.xml @@ -665,7 +665,7 @@ Advertencia: este archivo puede ser eliminado si la aplicación se desinstala.Verificar dispositivo Para verificar que este dispositivo es confiable, por favor contacta a su dueño por algún otro medio (ej. cara a cara o por teléfono) y pregúntale si la clave que ve en sus Ajustes de Usuario para este dispositivo coincide con la clave a continuación: - Si coincide, presione el botón de verificar a continuación. Si no coincide, entonces alguien más está interceptando este dispositivo y probablemente debería prohibirlo. En el futuro, este proceso de verificación será más sofisticado. + Si coincide, presione el botón de verificar a continuación. Si no coincide, entonces alguien está interceptando este dispositivo y probablemente debería prohibirlo. En el futuro, este proceso de verificación será más sofisticado. Verifico que las claves coinciden Riot ahora admite cifrado de extremo a extremo pero debes volver a iniciar sesión para habilitarlo. @@ -1568,4 +1568,12 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de Error obteniendo información de confiabilidad Error obteniendo claves para copias de respaldo + !Estás al día! + Mientras RiotX esté en desarrollo, algunas características pueden faltar y existir fallas. + Preferencias + Seguridad & Privacidad + Voz & Video + Haz una sugerencia + Por favor escriba su sugerencia a continuación. + Describa su sugerencia aquí diff --git a/vector/src/main/res/values-eu/strings.xml b/vector/src/main/res/values-eu/strings.xml index 0736e1b8..3fa89265 100644 --- a/vector/src/main/res/values-eu/strings.xml +++ b/vector/src/main/res/values-eu/strings.xml @@ -1575,4 +1575,36 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada. \nEz dira oraindik Riot bezeroaren ezaugarri guztiak ezarri RiotX bezeroan. Falta diren (eta laster etorriko direnen) artean nabarmenak dira: • Kontua sortzea • Gelaren ezarpenak (gelako kideak zerrendatzea, eta abar.) • Txat zuzenerako gelak sortzea • Deiak • Trepetak • … app_display_name: + Mezu zuzenak + + Itxaroten… + Iruditxoa zifratzen… + "Iruditxoa bidaltzen (%1$s / %2$s)" + Fitxategia zifratzen… + "Fitxategia bidaltzen (%1$s / %2$s)" + + %1$s fitxategia deskargatzen… + %1$s fitxategia deskargatu da! + + (editatua) + + %1$s kontua sortzeko. + Erabili aplikazio zaharra + + + Mezuen edizioak + Ez da ediziorik aurkitu + + Iragazi elkarrizketak… + Ezin duzu bilatzen ari zarena aurkitu\? + Sortu gela berria + Bidali mezu zuzen berri bat + Ikusi gelen direktorioa + + Izena edo ID-a (#adibidea:matrix.org) + + Gaitu hatza pasatzea denbora-lerroan erantzuteko + + Esteka arbelera kopiatu da + diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml index d0f39271..1b60f5a1 100644 --- a/vector/src/main/res/values-fr/strings.xml +++ b/vector/src/main/res/values-fr/strings.xml @@ -1580,4 +1580,36 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq \n \nToutes les fonctionnalités de Riot ne sont pas encore implémentées dans RiotX. Principales fonctionnalités manquantes (et qui arrivent bientôt !) : • Création de compte • Réglages des salons (lister les membres du salon etc.) • Création de salons de discussion directe • Appels • Widgets • … + Messages directs + + Attente… + Chiffrement de la miniature… + Envoi de la miniature (%1$s / %2$s) + Chiffrement du fichier… + Envoi du fichier (%1$s / %2$s) + + Téléchargement du fichier %1$s… + Le fichier %1$s a été téléchargé ! + + (édité) + + %1$s pour créer un compte. + Utilisez l’ancienne application + + + Éditions de message + Aucune édition trouvée + + Filtrer les conversations… + Vous ne trouvez pas ce que vous cherchez \? + Créer un nouveau salon + Envoyer un nouveau message direct + Voir le répertoire des salons + + Nom ou identifiant (#exemple:matrix.org) + + Activer le balayement pour répondre dans l’historique + + Lien copié dans le presse-papiers + diff --git a/vector/src/main/res/values-hu/strings.xml b/vector/src/main/res/values-hu/strings.xml index 035cae08..9f477da3 100644 --- a/vector/src/main/res/values-hu/strings.xml +++ b/vector/src/main/res/values-hu/strings.xml @@ -1579,4 +1579,36 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró \n \nNem minden Riot funkció támogatott a RiotX-ben jelenleg. A fő hiányzó (és hamarosan elérhető!) funkciók: • Felhasználói fiók létrehozása • Szoba beállítások (szoba tagság mutatása, stb…) • Közvetlen beszélgetések indítása • Hívások • Kisalkalmazások • … + Közvetlen beszélgetés + + Várakozik… + Bélyegképek titkosítása… + Bélyegképek küldése (%1$s / %2$s) + Fájl titkosítása… + Fájl küldése (%1$s / %2$s) + + Fájl letöltése %1$s… + %1$s fájl letöltve! + + (szerkesztve) + + %1$s fiók készítéshez. + Használd a régi alkalmazást + + + Üzenet szerkesztések + Szerkesztések nem találhatók + + Beszélgetések szűrése… + Nem találod amit keresel\? + Új szoba készítése + Új közvetlen üzenet küldése + Szoba lista megjelenítése + + Név vagy azonosító (#pelda:matrix.org) + + Válasz engedélyezése húzással az idővonalon + + Hivatkozás a vágólapra másolva + diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml index cb5141ce..c414f38d 100644 --- a/vector/src/main/res/values-it/strings.xml +++ b/vector/src/main/res/values-it/strings.xml @@ -1623,4 +1623,37 @@ Per essere certo di non perdere nulla, mantieni gli aggiornamenti attivi." + Non hai nulla di nuovo da vedere! + Messaggi diretti + + In attesa… + Criptazione miniatura… + Invio miniatura (%1$s / %2$s) + Criptazione file… + Invio file (%1$s / %2$s) + + Scaricamento file %1$s… + Il file %1$s è stato scaricato! + + (modificato) + + %1$s per creare un account. + Usa la vecchia app + + + Modifiche messaggio + Nessuna modifica trovata + + Filtra conversazioni… + Non trovi quello che cerchi\? + Crea un nuova stanza + Invia un nuovo messaggio diretto + Vedi l\'elenco delle stanze + + Nome o ID (#esempio:matrix.org) + + Attiva swipe per rispondere nella timeline + + Collegamento copiato negli appunti + diff --git a/vector/src/main/res/values-pl/strings.xml b/vector/src/main/res/values-pl/strings.xml index ae7da359..3298e5dd 100644 --- a/vector/src/main/res/values-pl/strings.xml +++ b/vector/src/main/res/values-pl/strings.xml @@ -27,7 +27,7 @@ Aktywne połączenie Przychodzące połączenie grupowe. \nDołącz z %1$s lub %2$s - mikrofonem + Głos kamerą Nie można rozpocząć połączenia, spróbuj ponownie później Ze względu na brak pewnych uprawnień, niektóre funkcje mogą nie działać… @@ -95,7 +95,7 @@ Opisz swój problem tutaj W celu zdiagnozowania problemów, logi z tego klienta zostaną wysłane wraz z tym raportem o błędzie. Ten raport o błędzie, w tym dzienniki i zrzut ekranu, nie będzie publicznie widoczny. Jeśli wolisz wysłać tylko powyższy tekst, odznacz: Wygląda na to, że potrząsasz telefonem ze zdenerwowania. Czy chcesz otworzyć ekran zgłaszania błędu\? - Aplikacja nie została poprawnie zamknięta za ostatnim razem. Czy chcesz zgłosić raport o nieoczekiwanym zamknięciu? + Przy wcześniejszym użyciu aplikacja napotkała błąd. Czy chcesz zgłosić raport o nieoczekiwanym zamknięciu\? Pomyślnie zgłoszono błąd Nie udało się zgłosić błędu (%s) @@ -543,8 +543,8 @@ Zauważ, że ta czynność spowoduje ponowne uruchomienie aplikacji i może to t Utwórz Off Uwaga! - W trakcie synchronizacji - Nasłuchuj zdarzeń + Synchronizowanie… + Nasłuchiwanie zdarzeń Głośne powiadomienia Ciche powiadomienia @@ -614,9 +614,7 @@ Zezwól na dostęp w następnym oknie aby móc wykonać połączenie. Riot wymaga dostępu do kamery i mikrofonu, aby przeprowadzać rozmowy wideo. Przyznaj dostęp w następnym oknie. - Riot wymaga dostępu do kontaktów, aby znajdywać innych użytkowników Matrixa bazując na adresie e-mail i numerze telefonu. - -Proszę przyznaj dostęp w następnym oknie, aby odkryć użytkowników książki adresowej osiągalnych z Riot. + Riot może sprawdzić Twoją książkę adresową, aby znajdywać innych użytkowników Matrixa bazując na ich adresie e-mail i numerze telefonu. Jeśli zgadzasz się na udostępnienie Twojej książki adresowej w tym celu, zezwól na dostęp w następnym okienku. Riot wymaga dostępu do kontaktów, aby znajdywać innych użytkowników Matrixa bazując na adresie e-mail i numerze telefonu. Zezwolić Riot na dostęp do kontaktów? @@ -1132,7 +1130,7 @@ Aby upewnić się, że niczego nie przegapisz, po prostu miej włączone aktuali Usługa Powiadomień jest uruchomiona. Usługa Powiadomień nie jest uruchomiona. Spróbuj uruchomić ponownie aplikację. - W trakcie połączenia wideo… + Połączenie wideo trwa… Kopia Zapasowa Klucza Kopia zapasowa kluczy nie jest zakończona, proszę czekać… @@ -1229,4 +1227,9 @@ Spróbuj uruchomić ponownie aplikację. Pokoje Opublikuj pokój do spisu pokojów + Aby nie utracić dostępu do Twoich zaszyfrowanych wiadomości, powinienieś aktywować kopię zapasową klucza na wszystkich swoich urządzeniach. + Tworzenie kopii zapasowej kluczy… + Utwórz kopię zapasową + Twoje urządzenie używa przestarzałego protokołu bezpieczeństwa TSL, podatnego na ataki, dlatego dla Twojego bezpieczeństwa nie będziesz mógł się połączyć + Kopia zapasowa klucza została prawidłowo skonfigurowana dla tego urządzenia. diff --git a/vector/src/main/res/values-ro/strings.xml b/vector/src/main/res/values-ro/strings.xml new file mode 100644 index 00000000..09561894 --- /dev/null +++ b/vector/src/main/res/values-ro/strings.xml @@ -0,0 +1,20 @@ + + + ro + RO + + Temă luminoasă + Temă întunecată + Temă Neagră + Tema Status.im + + Inițializează serviciu + Sincronizează… + Așteaptă evenimente + Notificări zgomotoase + Notificări liniștite + + Mesaje + Cameră + Setări + diff --git a/vector/src/main/res/values-ru/strings.xml b/vector/src/main/res/values-ru/strings.xml index 838d826b..90c2cba1 100644 --- a/vector/src/main/res/values-ru/strings.xml +++ b/vector/src/main/res/values-ru/strings.xml @@ -264,12 +264,10 @@ Email также позволит вам при необходимости во Riot необходимы разрешения на доступ к камере и микрофону для видеовызовов. Пожалуйста дайте разрешение в следующем окне для звонка. - Райоту необходимы разрешения на доступ к контактам, чтобы найти других пользователей сети по email или телефонному номеру. - -Пожалуйста разрешите доступ в следующем окне, чтобы ваши контакты были доступны в приложении. - Riot необходимы разрешения на доступ к контактам, чтобы найти других пользователей сети по email или телефонному номеру. - -Разрешить Riot доступ к контактам? + Riot необходимы разрешения на доступ к контактам, чтобы найти других пользователей сети по email или телефонному номеру. Пожалуйста разрешите доступ в следующем окне, чтобы ваши контакты были доступны в приложении. + Riot может проверить Вашу адресную книгу, чтобы найти других пользователей сети по email или телефонному номеру. +\n +\nСогласны ли вы поделиться своей адресной книгой для этой цели\? Извините. Действие не выполнено из-за недостаточных разрешений @@ -1509,7 +1507,7 @@ Email также позволит вам при необходимости во Запрос поделится ключем Игнорировать - Недействительный ответ обнаружения сервера + Ошибка отклика сервера Дополнить параметры сервера Riot обнаружил пользовательскую конфигурацию сервера для вашего userID домена\"%1$s\": \n%2$s @@ -1561,4 +1559,31 @@ Email также позволит вам при необходимости во Резервная копия существует на домашнем сервере Похоже, что у вас уже есть резервное копирование ключа настройки с другого устройства. Вы хотите заменить его на тот, который вы создаете\? Заменить + Стоп + + Проверка состояния резервного копирования + Вы вышли из системы из-за недействительных или истекших учетных данных. + + Редактировать + Ответ + + Повторить + Присоединитесь к комнате, чтобы начать использовать приложение. + Отправил вам приглашение + Приглашен %s + + Вы в курсе! + У вас больше нет непрочитанных сообщений + Добро пожаловать домой! + Узнайте больше о непрочитанных сообщениях здесь + Беседа + Ваше прямое сообщение будет отображаться здесь + Комнаты + Ваши комнаты будут отображаться здесь + + Реакция + Соглашаться + Нравиться + Добавить реакцию + Посмотреть реакции diff --git a/vector/src/main/res/values-v21/theme_black.xml b/vector/src/main/res/values-v21/theme_black.xml index 5ab2ed89..74ec2cd9 100644 --- a/vector/src/main/res/values-v21/theme_black.xml +++ b/vector/src/main/res/values-v21/theme_black.xml @@ -7,6 +7,11 @@ true + + + @transition/image_preview_transition + @transition/image_preview_transition + + +