diff --git a/CHANGES.md b/CHANGES.md index 1ba5b11e..08566926 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,10 +2,14 @@ Changes in RiotX 0.3.0 (2019-XX-XX) =================================================== Features: - - + - Create Direct Room flow 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 Other changes: - @@ -14,6 +18,8 @@ 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 ) Translations: - diff --git a/matrix-sdk-android-rx/build.gradle b/matrix-sdk-android-rx/build.gradle index 546922f2..655df2c2 100644 --- a/matrix-sdk-android-rx/build.gradle +++ b/matrix-sdk-android-rx/build.gradle @@ -38,6 +38,8 @@ dependencies { 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..1755fb50 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -110,7 +110,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 +126,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/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 1fc37cd4..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) 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 7fd4d109..cdc1c952 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 @@ -127,8 +127,8 @@ class CreateRoomParams { contentMap["algorithm"] = algorithm val algoEvent = Event(type = EventType.ENCRYPTION, - stateKey = "", - content = contentMap.toContent() + stateKey = "", + content = contentMap.toContent() ) if (null == initialStates) { @@ -161,8 +161,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 = mutableListOf(historyVisibilityEvent) @@ -201,8 +201,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()) } /** @@ -222,14 +222,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..e9f22da4 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 @@ -41,4 +41,8 @@ enum class SendState { return this == UNDELIVERED || this == FAILED_UNKNOWN_DEVICES } + fun isSending(): Boolean { + return this == UNSENT || this == ENCRYPTING || this == SENDING + } + } 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..acab75df 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 ) { 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..d9387ad3 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 @@ -506,25 +505,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 +594,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 +608,12 @@ internal class MXOlmDevice @Inject constructor( sessions.add(session) } Unit + }, + { + // Session does not already exist, add it + sessions.add(session) } + ) } @@ -648,61 +648,57 @@ 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() - } - - 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 (null != timeline) { + if (!inboundGroupSessionMessageIndexes.containsKey(timeline)) { + inboundGroupSessionMessageIndexes[timeline] = HashMap() } + + 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") + throw 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") + 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 +762,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 +794,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 ebed85ae..38d3e695 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 javax.inject.Inject internal class ObjectSigner @Inject constructor(private val credentials: Credentials, 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 0a0a158c..60f319ac 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 @@ -40,29 +39,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 @@ -72,14 +70,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) @@ -87,73 +83,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 5a4783c4..edd2e190 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 @@ -202,31 +201,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) } @@ -241,27 +241,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) } } @@ -353,15 +355,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) } @@ -452,7 +453,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 @@ -490,27 +492,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) } } @@ -756,49 +759,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) } } @@ -853,45 +859,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) } @@ -1234,69 +1242,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 12c618d0..eabd5dff 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 7fd48f8b..5655e653 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 @@ -62,9 +62,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 36fa3701..7f3f4878 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 javax.inject.Inject import kotlin.random.Random @@ -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 ac30f025..35a23535 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 274a14ce..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,8 +20,6 @@ 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 @@ -72,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 1411d70c..5bf04dda 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/EventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt index 60344f60..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,7 +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 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/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index c178711c..08c9eabc 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,6 +17,7 @@ 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 @@ -32,6 +33,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,12 +41,19 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", ) : RealmObject() { private var membershipStr: String = Membership.NONE.name + private var versioningStateStr: String = VersioningState.NONE.name + @delegate:Ignore var membership: Membership by Delegates.observable(Membership.valueOf(membershipStr)) { _, _, newValue -> membershipStr = newValue.name } + @delegate:Ignore + var versioningState: VersioningState by Delegates.observable(VersioningState.valueOf(versioningStateStr)) { _, _, newValue -> + versioningStateStr = newValue.name + } + companion object } \ No newline at end of file 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..5a48e022 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,12 +16,8 @@ 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.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 @@ -65,11 +61,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 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 7397f0ee..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 @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.pushrules.Action import im.vector.matrix.android.api.pushrules.PushRuleService 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.query.where @@ -80,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) } 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 d196e563..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,48 +40,45 @@ internal class DefaultProcessEventForPushTask @Inject constructor( private val sessionParams: SessionParams ) : ProcessEventForPushTask { - - override suspend fun execute(params: ProcessEventForPushTask.Params): Try { - return Try { - // 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() + 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 d4449fe1..995d9331 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..8b65be24 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,34 @@ 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.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 +59,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 +80,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 +104,168 @@ 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 -> + RoomEntity.where(realm, roomId).findFirst()?.let { room -> + room.sendingTimelineEvents.filter { + it.root?.sendState?.hasFailed() ?: false + }.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 12d747fe..d757d69a 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.UUID @@ -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..42595051 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -29,6 +29,7 @@ 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.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 +134,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 +207,23 @@ internal class DefaultTimeline( } } + override fun pendingEventCount(): Int { + var count = 0 + Realm.getInstance(realmConfiguration).use { + count = RoomEntity.where(it, roomId).findFirst()?.sendingTimelineEvents?.count() ?: 0 + } + return count + } + + override fun failedToDeliverEventCount(): Int { + var count = 0 + Realm.getInstance(realmConfiguration).use { + count = RoomEntity.where(it, roomId).findFirst()?.sendingTimelineEvents?.filter { + it.root?.sendState?.hasFailed() ?: false + }?.count() ?: 0 + } + return count + } override fun start() { if (isStarted.compareAndSet(false, true)) { @@ -388,24 +406,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 85f4ce78..e1b9d83d 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 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..0b9365dc 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 @@ -102,39 +102,40 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, Timber.v("[$this] Execute sync request with timeout $DEFAULT_LONG_POOL_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() + + cancelableTask = syncTask.configureWith(params) { + this.callbackThread = TaskThread.SYNC + this.executionThread = TaskThread.SYNC + this.callback = object : MatrixCallback { + + override fun onSuccess(data: Unit) { + latch.countDown() + } + + 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) } - 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) - } - - if (failure !is Failure.NetworkConnection - || failure.cause is JsonEncodingException) { - // Wait 10s before retrying - 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() + if (failure !is Failure.NetworkConnection + || failure.cause is JsonEncodingException) { + // Wait 10s before retrying + 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() + } + } + } .executeBy(taskExecutor) latch.await() 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/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml index 5459cf91..8e5fc6d6 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 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..1010c83b 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,5 @@ - + Sending message… + Clear sending queue \ No newline at end of file diff --git a/vector/build.gradle b/vector/build.gradle index db9ad6d6..85a4dae1 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -148,7 +148,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 +193,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 +210,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/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/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index a42eec49..35cda2e6 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 @@ -73,6 +77,8 @@ interface ScreenComponent { fun rageShake(): RageShake + fun navigator(): Navigator + fun inject(activity: HomeActivity) fun inject(roomDetailFragment: RoomDetailFragment) @@ -153,6 +159,12 @@ interface ScreenComponent { 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/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/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..e9c60942 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 @@ -121,6 +124,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector { 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 @@ -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/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/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 1ff2c67d..a277f9a5 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,6 +58,7 @@ 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.events.model.Event import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.message.* @@ -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 @@ -184,6 +189,8 @@ class RoomDetailFragment : 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 +208,7 @@ class RoomDetailFragment : setupComposer() setupAttachmentButton() setupInviteView() + setupNotificationView() roomDetailViewModel.subscribe { renderState(it) } textComposerViewModel.subscribe { renderTextComposerState(it) } roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) } @@ -218,6 +226,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)) @@ -237,12 +249,60 @@ class RoomDetailFragment : } } + 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() { commandAutocompletePolicy.enabled = true 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,7 +385,7 @@ 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 @@ -517,7 +577,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,7 +590,14 @@ 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) { @@ -551,6 +617,26 @@ 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, @@ -584,7 +670,7 @@ class RoomDetailFragment : .show() } - // TimelineEventController.Callback ************************************************************ +// TimelineEventController.Callback ************************************************************ override fun onUrlClicked(url: String): Boolean { return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor { @@ -623,8 +709,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) { @@ -703,115 +805,128 @@ class RoomDetailFragment : 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.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) { @@ -862,7 +977,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..2bb7327d 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,22 +29,30 @@ 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 @@ -94,8 +103,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro init { observeRoomSummary() observeEventDisplayedActions() - observeInvitationState() - cancelableBag += room.loadRoomMembersIfNeeded() + observeSummaryState() + room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() timeline.start() setState { copy(timeline = this@RoomDetailViewModel.timeline) } } @@ -105,7 +114,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 +126,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 +181,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro val nonBlockingPopAlert: LiveData>>> get() = _nonBlockingPopAlert - private val _sendMessageResultLiveData = MutableLiveData>() val sendMessageResultLiveData: LiveData> get() = _sendMessageResultLiveData @@ -155,6 +194,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 +220,62 @@ 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)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) } 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 +307,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 +332,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,7 +342,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro sendMode = SendMode.REGULAR ) } - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) } } @@ -318,29 +371,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 +441,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 +452,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 +461,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 +505,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 +546,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - _navigateToEvent.postValue(LiveEvent(targetEventId)) + _navigateToEvent.postLiveEvent(targetEventId) } else { // change timeline timeline.dispose() @@ -518,10 +571,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. @@ -547,7 +640,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 +649,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..77c1c63c 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,7 +19,9 @@ 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.model.tombstone.RoomTombstoneContent 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.user.model.User @@ -46,7 +48,9 @@ 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 ) : 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/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 0b78f815..e8268bac 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 @@ -54,6 +54,7 @@ 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) @@ -158,7 +159,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim synchronized(modelCache) { for (i in 0 until modelCache.size) { if (modelCache[i]?.eventId == eventIdToHighlight - || modelCache[i]?.eventId == this.eventIdToHighlight) { + || modelCache[i]?.eventId == this.eventIdToHighlight) { modelCache[i] = null } } @@ -219,8 +220,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim // Should be build if not cached or if cached but contains mergedHeader or formattedDay // We then are sure we always have items up to date. if (modelCache[position] == null - || modelCache[position]?.mergedHeaderModel != null - || modelCache[position]?.formattedDayModel != null) { + || modelCache[position]?.mergedHeaderModel != null + || modelCache[position]?.formattedDayModel != null) { modelCache[position] = buildItemModels(position, currentSnapshot) } } @@ -294,7 +295,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim // => handle case where paginating from mergeable events and we get more val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull() val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) - ?: true + ?: 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..0cc95af7 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,11 +15,14 @@ */ 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 @@ -35,7 +38,24 @@ 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 MessageMenuState( val roomId: String, @@ -67,22 +87,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 +100,70 @@ 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 (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 +171,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 +228,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 +259,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 +269,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 c330b85d..c9da3ce6 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 @@ -41,11 +41,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 +65,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,7 +92,26 @@ 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() @@ -99,7 +121,7 @@ class MessageItemFactory @Inject constructor( event.annotations?.editSummary, highlight, callback) - is MessageTextContent -> buildTextMessageItem(event.sendState, + is MessageTextContent -> buildTextMessageItem(event.root.sendState, messageContent, informationData, event.annotations?.editSummary, 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..5cd873ff 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 @@ -64,7 +64,7 @@ class MessageInformationDataFactory @Inject constructor(private val timelineDate return MessageInformationData( eventId = eventId, senderId = event.root.senderId ?: "", - sendState = event.sendState, + sendState = event.root.sendState, time = time, avatarUrl = avatarUrl, memberName = formattedMemberName, @@ -74,7 +74,8 @@ class MessageInformationDataFactory @Inject constructor(private val timelineDate ?.map { ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty()) }, - hasBeenEdited = hasBeenEdited + hasBeenEdited = 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/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/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_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/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..81a5b33d 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -11,6 +11,7 @@ style="@style/VectorToolbarStyle" android:layout_width="0dp" android:layout_height="?actionBarSize" + android:transitionName="toolbar" android:elevation="4dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -74,17 +75,35 @@ android:id="@+id/recyclerView" android:layout_width="0dp" android:layout_height="0dp" - app:layout_constraintBottom_toTopOf="@+id/composerLayout" + app:layout_constraintBottom_toTopOf="@+id/recyclerViewBarrier" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/roomToolbar" tools:listitem="@layout/item_timeline_event_base" /> + + + @@ -101,4 +120,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-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 +