diff --git a/changelog.d/7737.bugfix b/changelog.d/7737.bugfix new file mode 100644 index 0000000000..1477834674 --- /dev/null +++ b/changelog.d/7737.bugfix @@ -0,0 +1 @@ +Fix issue of Scan QR code button sometimes not showing when it should be available diff --git a/changelog.d/7740.feature b/changelog.d/7740.feature new file mode 100644 index 0000000000..6cd2b6c776 --- /dev/null +++ b/changelog.d/7740.feature @@ -0,0 +1 @@ +Handle account data removal diff --git a/changelog.d/7754.feature b/changelog.d/7754.feature new file mode 100644 index 0000000000..0e1b6d0961 --- /dev/null +++ b/changelog.d/7754.feature @@ -0,0 +1 @@ +Delete unused client information from account data diff --git a/changelog.d/7770.bugfix b/changelog.d/7770.bugfix new file mode 100644 index 0000000000..598deb6073 --- /dev/null +++ b/changelog.d/7770.bugfix @@ -0,0 +1 @@ +[Push Notifications] When push notification for threaded message is clicked, thread timeline will be opened instead of room's main timeline diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt index 252c33a8c4..e490311b91 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt @@ -125,12 +125,6 @@ interface AuthenticationService { deviceId: String? = null ): Session - /** - * @param homeServerConnectionConfig the information about the homeserver and other configuration - * Return true if qr code login is supported by the server, false otherwise. - */ - suspend fun isQrLoginSupported(homeServerConnectionConfig: HomeServerConnectionConfig): Boolean - /** * Authenticate using m.login.token method during sign in with QR code. * @param homeServerConnectionConfig the information about the homeserver and other configuration diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt index 5b6c1897bf..5de83033e1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt @@ -22,5 +22,6 @@ data class LoginFlowResult( val isLoginAndRegistrationSupported: Boolean, val homeServerUrl: String, val isOutdatedHomeserver: Boolean, - val isLogoutDevicesSupported: Boolean + val isLogoutDevicesSupported: Boolean, + val isLoginWithQrSupported: Boolean, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/SessionAccountDataService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/SessionAccountDataService.kt index a22dd33774..8addb0782e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/SessionAccountDataService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/SessionAccountDataService.kt @@ -63,4 +63,17 @@ interface SessionAccountDataService { * Update the account data with the provided type and the provided account data content. */ suspend fun updateUserAccountData(type: String, content: Content) + + /** + * Retrieve user account data list whose type starts with the given type. + * @param type the type or the starting part of a type + * @return list of account data whose type starts with the given type + */ + fun getUserAccountDataEventsStartWith(type: String): List + + /** + * Deletes user account data of the given type. + * @param type the type to delete from user account data + */ + suspend fun deleteUserAccountData(type: String) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index 5449c0a735..d9c2afcb40 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -30,7 +30,6 @@ import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.login.LoginWizard import org.matrix.android.sdk.api.auth.registration.RegistrationWizard import org.matrix.android.sdk.api.auth.wellknown.WellknownResult -import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixIdFailure import org.matrix.android.sdk.api.session.Session @@ -299,7 +298,8 @@ internal class DefaultAuthenticationService @Inject constructor( isLoginAndRegistrationSupported = versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl = homeServerUrl, isOutdatedHomeserver = !versions.isSupportedBySdk(), - isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices() + isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices(), + isLoginWithQrSupported = versions.doesServerSupportQrCodeLogin(), ) } @@ -408,20 +408,6 @@ internal class DefaultAuthenticationService @Inject constructor( ) } - override suspend fun isQrLoginSupported(homeServerConnectionConfig: HomeServerConnectionConfig): Boolean { - val authAPI = buildAuthAPI(homeServerConnectionConfig) - val versions = runCatching { - executeRequest(null) { - authAPI.versions() - } - } - return if (versions.isSuccess) { - versions.getOrNull()?.doesServerSupportQrCodeLogin().orFalse() - } else { - false - } - } - override suspend fun loginUsingQrLoginToken( homeServerConnectionConfig: HomeServerConnectionConfig, loginToken: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt index 08bb9e7ff3..0489fe690f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt @@ -21,6 +21,7 @@ import io.realm.RealmQuery import io.realm.kotlin.where import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomEntityFields @@ -44,3 +45,11 @@ internal fun RoomEntity.Companion.where(realm: Realm, membership: Membership? = internal fun RoomEntity.fastContains(eventId: String): Boolean { return EventEntity.where(realm, eventId = eventId).findFirst() != null } + +internal fun RoomEntity.removeAccountData(type: String) { + accountData + .where() + .equalTo(RoomAccountDataEntityFields.TYPE, type) + .findFirst() + ?.deleteFromRealm() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserAccountDataEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserAccountDataEntityQueries.kt new file mode 100644 index 0000000000..b28965aeca --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserAccountDataEntityQueries.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.database.query + +import io.realm.Realm +import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity +import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields + +/** + * Delete an account_data event. + */ +internal fun UserAccountDataEntity.Companion.delete(realm: Realm, type: String) { + realm + .where() + .equalTo(UserAccountDataEntityFields.TYPE, type) + .findFirst() + ?.deleteFromRealm() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index 31bed90b62..4e55b2c40a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -427,6 +427,19 @@ internal interface RoomAPI { @Body content: JsonDict ) + /** + * Remove an account_data event from the room. + * @param userId the user id + * @param roomId the room id + * @param type the type + */ + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc3391/user/{userId}/rooms/{roomId}/account_data/{type}") + suspend fun deleteRoomAccountData( + @Path("userId") userId: String, + @Path("roomId") roomId: String, + @Path("type") type: String + ) + /** * Upgrades the given room to a particular room version. * Errors: diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt index 0f296ded5d..92ebb41ad9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt @@ -45,6 +45,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields import org.matrix.android.sdk.internal.database.model.deleteOnCascade +import org.matrix.android.sdk.internal.database.query.delete import org.matrix.android.sdk.internal.database.query.findAllFrom import org.matrix.android.sdk.internal.database.query.getDirectRooms import org.matrix.android.sdk.internal.database.query.getOrCreate @@ -94,7 +95,7 @@ internal class UserAccountDataSyncHandler @Inject constructor( // If we get some direct chat invites, we synchronize the user account data including those. suspend fun synchronizeWithServerIfNeeded(invites: Map) { - if (invites.isNullOrEmpty()) return + if (invites.isEmpty()) return val directChats = directChatsHelper.getLocalDirectMessages().toMutable() var hasUpdate = false monarchy.doWithRealm { realm -> @@ -252,9 +253,17 @@ internal class UserAccountDataSyncHandler @Inject constructor( } fun handleGenericAccountData(realm: Realm, type: String, content: Content?) { + if (content.isNullOrEmpty()) { + // This is a response for a deleted account data according to + // https://github.com/ShadowJonathan/matrix-doc/blob/account-data-delete/proposals/3391-account-data-delete.md#sync + UserAccountDataEntity.delete(realm, type) + return + } + val existing = realm.where() .equalTo(UserAccountDataEntityFields.TYPE, type) .findFirst() + if (existing != null) { // Update current value existing.contentStr = ContentMapper.map(content) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt index b1b2bfef33..9da12a2c4a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt @@ -27,6 +27,7 @@ import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntity import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.database.query.removeAccountData import org.matrix.android.sdk.internal.session.room.read.FullyReadContent import org.matrix.android.sdk.internal.session.sync.handler.room.RoomFullyReadHandler import org.matrix.android.sdk.internal.session.sync.handler.room.RoomTagHandler @@ -56,6 +57,13 @@ internal class RoomSyncAccountDataHandler @Inject constructor( } private fun handleGeneric(roomEntity: RoomEntity, content: JsonDict?, eventType: String) { + if (content.isNullOrEmpty()) { + // This is a response for a deleted account data according to + // https://github.com/ShadowJonathan/matrix-doc/blob/account-data-delete/proposals/3391-account-data-delete.md#sync + roomEntity.removeAccountData(eventType) + return + } + val existing = roomEntity.accountData.where().equalTo(RoomAccountDataEntityFields.TYPE, eventType).findFirst() if (existing != null) { existing.contentStr = ContentMapper.map(content) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt index b283d51845..1b3d59ac66 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt @@ -18,13 +18,14 @@ package org.matrix.android.sdk.internal.session.user.accountdata import org.matrix.android.sdk.internal.network.NetworkConstants import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.PUT import retrofit2.http.Path internal interface AccountDataAPI { /** - * Set some account_data for the client. + * Set some account_data for the user. * * @param userId the user id * @param type the type @@ -36,4 +37,16 @@ internal interface AccountDataAPI { @Path("type") type: String, @Body params: Any ) + + /** + * Remove an account_data for the user. + * + * @param userId the user id + * @param type the type + */ + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc3391/user/{userId}/account_data/{type}") + suspend fun deleteAccountData( + @Path("userId") userId: String, + @Path("type") type: String + ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt index 3173686a27..463292b9c6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataModule.kt @@ -42,4 +42,7 @@ internal abstract class AccountDataModule { @Binds abstract fun bindUpdateBreadcrumbsTask(task: DefaultUpdateBreadcrumbsTask): UpdateBreadcrumbsTask + + @Binds + abstract fun bindDeleteUserAccountDataTask(task: DefaultDeleteUserAccountDataTask): DeleteUserAccountDataTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt index c73446cf25..304a586a79 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt @@ -34,10 +34,11 @@ import javax.inject.Inject internal class DefaultSessionAccountDataService @Inject constructor( @SessionDatabase private val monarchy: Monarchy, private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val deleteUserAccountDataTask: DeleteUserAccountDataTask, private val userAccountDataSyncHandler: UserAccountDataSyncHandler, private val userAccountDataDataSource: UserAccountDataDataSource, private val roomAccountDataDataSource: RoomAccountDataDataSource, - private val taskExecutor: TaskExecutor + private val taskExecutor: TaskExecutor, ) : SessionAccountDataService { override fun getUserAccountDataEvent(type: String): UserAccountDataEvent? { @@ -78,4 +79,12 @@ internal class DefaultSessionAccountDataService @Inject constructor( userAccountDataSyncHandler.handleGenericAccountData(realm, type, content) } } + + override fun getUserAccountDataEventsStartWith(type: String): List { + return userAccountDataDataSource.getAccountDataEventsStartWith(type) + } + + override suspend fun deleteUserAccountData(type: String) { + deleteUserAccountDataTask.execute(DeleteUserAccountDataTask.Params(type)) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DeleteUserAccountDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DeleteUserAccountDataTask.kt new file mode 100644 index 0000000000..8d155e32cb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DeleteUserAccountDataTask.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.user.accountdata + +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.GlobalErrorReceiver +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface DeleteUserAccountDataTask : Task { + + data class Params( + val type: String, + ) +} + +internal class DefaultDeleteUserAccountDataTask @Inject constructor( + private val accountDataApi: AccountDataAPI, + @UserId private val userId: String, + private val globalErrorReceiver: GlobalErrorReceiver, +) : DeleteUserAccountDataTask { + + override suspend fun execute(params: DeleteUserAccountDataTask.Params) { + return executeRequest(globalErrorReceiver) { + accountDataApi.deleteAccountData(userId, params.type) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt index 39f155096a..01f5d9f708 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt @@ -60,6 +60,16 @@ internal class UserAccountDataDataSource @Inject constructor( ) } + fun getAccountDataEventsStartWith(type: String): List { + return realmSessionProvider.withRealm { realm -> + realm + .where(UserAccountDataEntity::class.java) + .beginsWith(UserAccountDataEntityFields.TYPE, type) + .findAll() + .map(accountDataMapper::map) + } + } + private fun accountDataEventsQuery(realm: Realm, types: Set): RealmQuery { val query = realm.where(UserAccountDataEntity::class.java) if (types.isNotEmpty()) { diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultDeleteUserAccountDataTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultDeleteUserAccountDataTaskTest.kt new file mode 100644 index 0000000000..86580127dc --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultDeleteUserAccountDataTaskTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.user.accountdata + +import io.mockk.coVerify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.android.sdk.test.fakes.FakeAccountDataApi +import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver + +private const val A_TYPE = "a-type" +private const val A_USER_ID = "a-user-id" + +@ExperimentalCoroutinesApi +class DefaultDeleteUserAccountDataTaskTest { + + private val fakeGlobalErrorReceiver = FakeGlobalErrorReceiver() + private val fakeAccountDataApi = FakeAccountDataApi() + + private val deleteUserAccountDataTask = DefaultDeleteUserAccountDataTask( + accountDataApi = fakeAccountDataApi.instance, + userId = A_USER_ID, + globalErrorReceiver = fakeGlobalErrorReceiver + ) + + @Test + fun `given parameters when executing the task then api is called`() = runTest { + // Given + val params = DeleteUserAccountDataTask.Params(type = A_TYPE) + fakeAccountDataApi.givenParamsToDeleteAccountData(A_USER_ID, A_TYPE) + + // When + deleteUserAccountDataTask.execute(params) + + // Then + coVerify { fakeAccountDataApi.instance.deleteAccountData(A_USER_ID, A_TYPE) } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeAccountDataApi.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeAccountDataApi.kt new file mode 100644 index 0000000000..f3acc02458 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeAccountDataApi.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.test.fakes + +import io.mockk.coEvery +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataAPI + +internal class FakeAccountDataApi { + + val instance: AccountDataAPI = mockk() + + fun givenParamsToDeleteAccountData(userId: String, type: String) { + coEvery { instance.deleteAccountData(userId, type) } just runs + } +} diff --git a/vector/src/main/java/im/vector/app/core/session/clientinfo/DeleteUnusedClientInformationUseCase.kt b/vector/src/main/java/im/vector/app/core/session/clientinfo/DeleteUnusedClientInformationUseCase.kt new file mode 100644 index 0000000000..dcd5c58480 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/session/clientinfo/DeleteUnusedClientInformationUseCase.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 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.app.core.session.clientinfo + +import im.vector.app.core.di.ActiveSessionHolder +import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo +import javax.inject.Inject + +class DeleteUnusedClientInformationUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { + + suspend fun execute(deviceInfoList: List): Result = runCatching { + // A defensive approach against local storage reports an empty device list (although it is not a seen situation). + if (deviceInfoList.isEmpty()) return Result.success(Unit) + + val expectedClientInfoKeyList = deviceInfoList.map { MATRIX_CLIENT_INFO_KEY_PREFIX + it.deviceId } + activeSessionHolder + .getSafeActiveSession() + ?.accountDataService() + ?.getUserAccountDataEventsStartWith(MATRIX_CLIENT_INFO_KEY_PREFIX) + ?.map { it.type } + ?.subtract(expectedClientInfoKeyList.toSet()) + ?.forEach { userAccountDataKeyToDelete -> + activeSessionHolder.getSafeActiveSession()?.accountDataService()?.deleteUserAccountData(userAccountDataKeyToDelete) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt index 7d4b953b4b..e859691067 100644 --- a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt @@ -31,10 +31,10 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.core.session.clientinfo.DeleteUnusedClientInformationUseCase import im.vector.app.core.time.Clock import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.sample @@ -67,6 +67,7 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( private val setUnverifiedSessionsAlertShownUseCase: SetUnverifiedSessionsAlertShownUseCase, private val isNewLoginAlertShownUseCase: IsNewLoginAlertShownUseCase, private val setNewLoginAlertShownUseCase: SetNewLoginAlertShownUseCase, + private val deleteUnusedClientInformationUseCase: DeleteUnusedClientInformationUseCase, ) : VectorViewModel(initialState) { sealed class Action : VectorViewModelAction { @@ -103,6 +104,9 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( ) { cryptoList, infoList, pInfo -> // Timber.v("## Detector trigger ${cryptoList.map { "${it.deviceId} ${it.trustLevel}" }}") // Timber.v("## Detector trigger canCrossSign ${pInfo.get().selfSigned != null}") + + deleteUnusedClientInformation(infoList) + infoList .filter { info -> // filter out verified sessions or those which do not support encryption (i.e. without crypto info) @@ -146,6 +150,12 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( } } + private fun deleteUnusedClientInformation(deviceFullInfoList: List) { + viewModelScope.launch { + deleteUnusedClientInformationUseCase.execute(deviceFullInfoList) + } + } + override fun handle(action: Action) { when (action) { is Action.IgnoreDevice -> { diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt index b3f2ef1f75..014b9f0504 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt @@ -26,6 +26,7 @@ import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityThreadsBinding +import im.vector.app.features.MainActivity import im.vector.app.features.analytics.extensions.toAnalyticsInteraction import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.home.AvatarRenderer @@ -143,13 +144,20 @@ class ThreadsActivity : VectorBaseActivity() { context: Context, threadTimelineArgs: ThreadTimelineArgs?, threadListArgs: ThreadListArgs?, - eventIdToNavigate: String? = null + eventIdToNavigate: String? = null, + firstStartMainActivity: Boolean = false ): Intent { - return Intent(context, ThreadsActivity::class.java).apply { + val intent = Intent(context, ThreadsActivity::class.java).apply { putExtra(THREAD_TIMELINE_ARGS, threadTimelineArgs) putExtra(THREAD_EVENT_ID_TO_NAVIGATE, eventIdToNavigate) putExtra(THREAD_LIST_ARGS, threadListArgs) } + + return if (firstStartMainActivity) { + MainActivity.getIntentWithNextIntent(context, intent) + } else { + intent + } } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index bf1b23093d..7bf78bdb95 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -60,6 +60,8 @@ import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.arguments.TimelineArgs +import im.vector.app.features.home.room.threads.ThreadsActivity +import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.troubleshoot.TestNotificationReceiver import im.vector.app.features.themes.ThemeUtils @@ -574,6 +576,7 @@ class NotificationUtils @Inject constructor( fun buildMessagesListNotification( messageStyle: NotificationCompat.MessagingStyle, roomInfo: RoomEventGroupInfo, + threadId: String?, largeIcon: Bitmap?, lastMessageTimestamp: Long, senderDisplayNameForReplyCompat: String?, @@ -581,7 +584,11 @@ class NotificationUtils @Inject constructor( ): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) // Build the pending intent for when the notification is clicked - val openRoomIntent = buildOpenRoomIntent(roomInfo.roomId) + val openIntent = when { + threadId != null && vectorPreferences.areThreadMessagesEnabled() -> buildOpenThreadIntent(roomInfo, threadId) + else -> buildOpenRoomIntent(roomInfo.roomId) + } + val smallIcon = R.drawable.ic_notification val channelID = if (roomInfo.shouldBing) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID @@ -666,8 +673,8 @@ class NotificationUtils @Inject constructor( } } - if (openRoomIntent != null) { - setContentIntent(openRoomIntent) + if (openIntent != null) { + setContentIntent(openIntent) } if (largeIcon != null) { @@ -826,6 +833,45 @@ class NotificationUtils @Inject constructor( ) } + private fun buildOpenThreadIntent(roomInfo: RoomEventGroupInfo, threadId: String?): PendingIntent? { + val threadTimelineArgs = ThreadTimelineArgs( + startsThread = false, + roomId = roomInfo.roomId, + rootThreadEventId = threadId, + showKeyboard = false, + displayName = roomInfo.roomDisplayName, + avatarUrl = null, + roomEncryptionTrustLevel = null, + ) + val threadIntentTap = ThreadsActivity.newIntent( + context = context, + threadTimelineArgs = threadTimelineArgs, + threadListArgs = null, + firstStartMainActivity = true, + ) + threadIntentTap.action = actionIds.tapToView + // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that + threadIntentTap.data = createIgnoredUri("openThread?$threadId") + + val roomIntent = RoomDetailActivity.newIntent( + context = context, + timelineArgs = TimelineArgs( + roomId = roomInfo.roomId, + switchToParentSpace = true + ), + firstStartMainActivity = false + ) + // Recreate the back stack + return TaskStackBuilder.create(context) + .addNextIntentWithParentStack(HomeActivity.newIntent(context, firstStartMainActivity = false)) + .addNextIntentWithParentStack(roomIntent) + .addNextIntent(threadIntentTap) + .getPendingIntent( + clock.epochMillis().toInt(), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) + } + private fun buildOpenHomePendingIntentForSummary(): PendingIntent { val intent = HomeActivity.newIntent(context, firstStartMainActivity = false, clearNotification = true) intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP diff --git a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt index 7fdfa3535e..767f427f39 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/RoomGroupMessageCreator.kt @@ -33,14 +33,14 @@ class RoomGroupMessageCreator @Inject constructor( ) { fun createRoomMessage(events: List, roomId: String, userDisplayName: String, userAvatarUrl: String?): RoomNotification.Message { - val firstKnownRoomEvent = events[0] - val roomName = firstKnownRoomEvent.roomName ?: firstKnownRoomEvent.senderName ?: "" - val roomIsGroup = !firstKnownRoomEvent.roomIsDirect + val lastKnownRoomEvent = events.last() + val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderName ?: "" + val roomIsGroup = !lastKnownRoomEvent.roomIsDirect val style = NotificationCompat.MessagingStyle( Person.Builder() .setName(userDisplayName) .setIcon(bitmapLoader.getUserIcon(userAvatarUrl)) - .setKey(firstKnownRoomEvent.matrixID) + .setKey(lastKnownRoomEvent.matrixID) .build() ).also { it.conversationTitle = roomName.takeIf { roomIsGroup } @@ -75,6 +75,7 @@ class RoomGroupMessageCreator @Inject constructor( it.customSound = events.last().soundName it.isUpdated = events.last().isUpdated }, + threadId = lastKnownRoomEvent.threadId, largeIcon = largeBitmap, lastMessageTimestamp, userDisplayName, diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 7fe73f8087..04487c6198 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -118,7 +118,7 @@ class OnboardingViewModel @AssistedInject constructor( } } - private suspend fun checkQrCodeLoginCapability(config: HomeServerConnectionConfig) { + private fun checkQrCodeLoginCapability() { if (!vectorFeatures.isQrCodeLoginEnabled()) { setState { copy( @@ -133,11 +133,9 @@ class OnboardingViewModel @AssistedInject constructor( ) } } else { - // check if selected server supports MSC3882 first - val canLoginWithQrCode = authenticationService.isQrLoginSupported(config) setState { copy( - canLoginWithQrCode = canLoginWithQrCode + canLoginWithQrCode = selectedHomeserver.isLoginWithQrSupported ) } } @@ -705,7 +703,10 @@ class OnboardingViewModel @AssistedInject constructor( // This is invalid _viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig"))) } else { - startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, postAction) + startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, suspend { + checkQrCodeLoginCapability() + postAction() + }) } } @@ -764,8 +765,6 @@ class OnboardingViewModel @AssistedInject constructor( _viewEvents.post(OnboardingViewEvents.OutdatedHomeserver) } - checkQrCodeLoginCapability(config) - when (trigger) { is OnboardingAction.HomeServerChange.SelectHomeServer -> { onHomeServerSelected(config, serverTypeOverride, authResult) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt index 6e7d58338e..ea0d940952 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt @@ -76,6 +76,7 @@ data class SelectedHomeserverState( val preferredLoginMode: LoginMode = LoginMode.Unknown, val supportedLoginTypes: List = emptyList(), val isLogoutDevicesSupported: Boolean = false, + val isLoginWithQrSupported: Boolean = false, ) : Parcelable @Parcelize diff --git a/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt b/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt index db21a53854..9b8f0a1cc4 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt @@ -47,7 +47,8 @@ class StartAuthenticationFlowUseCase @Inject constructor( upstreamUrl = authFlow.homeServerUrl, preferredLoginMode = preferredLoginMode, supportedLoginTypes = authFlow.supportedLoginTypes, - isLogoutDevicesSupported = authFlow.isLogoutDevicesSupported + isLogoutDevicesSupported = authFlow.isLogoutDevicesSupported, + isLoginWithQrSupported = authFlow.isLoginWithQrSupported, ) private fun LoginFlowResult.findPreferredLoginMode() = when { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index b7a6c5df30..232fcd50f7 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -112,6 +112,7 @@ class DevicesViewModel @AssistedInject constructor( val deviceFullInfoList = async.invoke() val unverifiedSessionsCount = deviceFullInfoList.count { !it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse() } val inactiveSessionsCount = deviceFullInfoList.count { it.isInactive } + copy( devices = async, unverifiedSessionsCount = unverifiedSessionsCount, diff --git a/vector/src/test/java/im/vector/app/core/session/clientinfo/DeleteUnusedClientInformationUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/session/clientinfo/DeleteUnusedClientInformationUseCaseTest.kt new file mode 100644 index 0000000000..8acb7b404b --- /dev/null +++ b/vector/src/test/java/im/vector/app/core/session/clientinfo/DeleteUnusedClientInformationUseCaseTest.kt @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2022 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.app.core.session.clientinfo + +import im.vector.app.test.fakes.FakeActiveSessionHolder +import io.mockk.coVerify +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo + +private const val A_CURRENT_DEVICE_ID = "current-device-id" +private const val A_DEVICE_ID_1 = "a-device-id-1" +private const val A_DEVICE_ID_2 = "a-device-id-2" +private const val A_DEVICE_ID_3 = "a-device-id-3" +private const val A_DEVICE_ID_4 = "a-device-id-4" + +private val A_DEVICE_INFO_1 = DeviceInfo(deviceId = A_DEVICE_ID_1) +private val A_DEVICE_INFO_2 = DeviceInfo(deviceId = A_DEVICE_ID_2) + +class DeleteUnusedClientInformationUseCaseTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + + private val deleteUnusedClientInformationUseCase = DeleteUnusedClientInformationUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance, + ) + + @Before + fun setup() { + fakeActiveSessionHolder.fakeSession.givenSessionId(A_CURRENT_DEVICE_ID) + } + + @Test + fun `given a device list that account data has all of them and extra devices then use case deletes the unused ones`() = runTest { + // Given + val devices = listOf(A_DEVICE_INFO_1, A_DEVICE_INFO_2) + val userAccountDataEventList = listOf( + UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_1, mapOf("key" to "value")), + UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_2, mapOf("key" to "value")), + UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_3, mapOf("key" to "value")), + UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_4, mapOf("key" to "value")), + ) + fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.givenGetUserAccountDataEventsStartWith( + type = MATRIX_CLIENT_INFO_KEY_PREFIX, + userAccountDataEventList = userAccountDataEventList, + ) + + // When + deleteUnusedClientInformationUseCase.execute(devices) + + // Then + coVerify { fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_3) } + coVerify { fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_4) } + } + + @Test + fun `given a device list that account data has exactly all of them then use case does nothing`() = runTest { + // Given + val devices = listOf(A_DEVICE_INFO_1, A_DEVICE_INFO_2) + val userAccountDataEventList = listOf( + UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_1, mapOf("key" to "value")), + UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_2, mapOf("key" to "value")), + ) + fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.givenGetUserAccountDataEventsStartWith( + type = MATRIX_CLIENT_INFO_KEY_PREFIX, + userAccountDataEventList = userAccountDataEventList, + ) + + // When + deleteUnusedClientInformationUseCase.execute(devices) + + // Then + coVerify(exactly = 0) { + fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(any()) + } + } + + @Test + fun `given a device list that account data has missing some of them then use case does nothing`() = runTest { + // Given + val devices = listOf(A_DEVICE_INFO_1, A_DEVICE_INFO_2) + val userAccountDataEventList = listOf( + UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_1, mapOf("key" to "value")), + ) + fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.givenGetUserAccountDataEventsStartWith( + type = MATRIX_CLIENT_INFO_KEY_PREFIX, + userAccountDataEventList = userAccountDataEventList, + ) + + // When + deleteUnusedClientInformationUseCase.execute(devices) + + // Then + coVerify(exactly = 0) { + fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(any()) + } + } + + @Test + fun `given an empty device list that account data has some devices then use case does nothing`() = runTest { + // Given + val devices = emptyList() + val userAccountDataEventList = listOf( + UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_1, mapOf("key" to "value")), + UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_2, mapOf("key" to "value")), + ) + fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.givenGetUserAccountDataEventsStartWith( + type = MATRIX_CLIENT_INFO_KEY_PREFIX, + userAccountDataEventList = userAccountDataEventList, + ) + + // When + deleteUnusedClientInformationUseCase.execute(devices) + + // Then + coVerify(exactly = 0) { + fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(any()) + } + } +} diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index 92083eb50b..c570a75d99 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -83,6 +83,7 @@ private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(FakeUri().instance) private val SELECTED_HOMESERVER_STATE = SelectedHomeserverState(preferredLoginMode = LoginMode.Password, userFacingUrl = A_HOMESERVER_URL) private val SELECTED_HOMESERVER_STATE_SUPPORTED_LOGOUT_DEVICES = SelectedHomeserverState(isLogoutDevicesSupported = true) private val DEFAULT_SELECTED_HOMESERVER_STATE = SELECTED_HOMESERVER_STATE.copy(userFacingUrl = A_DEFAULT_HOMESERVER_URL) +private val DEFAULT_SELECTED_HOMESERVER_STATE_WITH_QR_SUPPORTED = DEFAULT_SELECTED_HOMESERVER_STATE.copy(isLoginWithQrSupported = true) private const val AN_EMAIL = "hello@example.com" private const val A_PASSWORD = "a-password" private const val A_USERNAME = "hello-world" @@ -164,7 +165,7 @@ class OnboardingViewModelTest { fun `given combined login enabled, when handling sign in splash action, then emits OpenCombinedLogin with default homeserver qrCode supported`() = runTest { val test = viewModel.test() fakeVectorFeatures.givenCombinedLoginEnabled() - givenCanSuccessfullyUpdateHomeserver(A_DEFAULT_HOMESERVER_URL, DEFAULT_SELECTED_HOMESERVER_STATE, canLoginWithQrCode = true) + givenCanSuccessfullyUpdateHomeserver(A_DEFAULT_HOMESERVER_URL, DEFAULT_SELECTED_HOMESERVER_STATE_WITH_QR_SUPPORTED) viewModel.handle(OnboardingAction.SplashAction.OnIAlreadyHaveAnAccount(OnboardingFlow.SignIn)) @@ -173,9 +174,9 @@ class OnboardingViewModelTest { initialState, { copy(onboardingFlow = OnboardingFlow.SignIn) }, { copy(isLoading = true) }, - { copy(canLoginWithQrCode = true) }, - { copy(selectedHomeserver = DEFAULT_SELECTED_HOMESERVER_STATE) }, + { copy(selectedHomeserver = DEFAULT_SELECTED_HOMESERVER_STATE_WITH_QR_SUPPORTED) }, { copy(signMode = SignMode.SignIn) }, + { copy(canLoginWithQrCode = true) }, { copy(isLoading = false) } ) .assertEvents(OnboardingViewEvents.OpenCombinedLogin) @@ -1174,13 +1175,11 @@ class OnboardingViewModelTest { resultingState: SelectedHomeserverState, config: HomeServerConnectionConfig = A_HOMESERVER_CONFIG, fingerprint: Fingerprint? = null, - canLoginWithQrCode: Boolean = false, ) { fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, fingerprint, config) fakeStartAuthenticationFlowUseCase.givenResult(config, StartAuthenticationResult(isHomeserverOutdated = false, resultingState)) givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.StartRegistration) fakeHomeServerHistoryService.expectUrlToBeAdded(config.homeServerUri.toString()) - fakeAuthenticationService.givenIsQrLoginSupported(config, canLoginWithQrCode) } private fun givenUpdatingHomeserverErrors(homeserverUrl: String, resultingState: SelectedHomeserverState, error: Throwable) { @@ -1188,7 +1187,6 @@ class OnboardingViewModelTest { fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, resultingState)) givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.Error(error)) fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString()) - fakeAuthenticationService.givenIsQrLoginSupported(A_HOMESERVER_CONFIG, false) } private fun givenUserNameIsAvailable(userName: String) { diff --git a/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt index d15a6cf042..9be22d7ea9 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCaseTest.kt @@ -140,7 +140,8 @@ class StartAuthenticationFlowUseCaseTest { isLoginAndRegistrationSupported = true, homeServerUrl = A_DECLARED_HOMESERVER_URL, isOutdatedHomeserver = false, - isLogoutDevicesSupported = false + isLogoutDevicesSupported = false, + isLoginWithQrSupported = false ) private fun expectedResult( diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt index 5d0e317c57..af53913169 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt @@ -58,10 +58,6 @@ class FakeAuthenticationService : AuthenticationService by mockk() { coEvery { getWellKnownData(matrixId, config) } returns result } - fun givenIsQrLoginSupported(config: HomeServerConnectionConfig, result: Boolean) { - coEvery { isQrLoginSupported(config) } returns result - } - fun givenWellKnownThrows(matrixId: String, config: HomeServerConnectionConfig?, cause: Throwable) { coEvery { getWellKnownData(matrixId, config) } throws cause } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt index f1a0ae7452..cd357ec85a 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt @@ -54,4 +54,8 @@ class FakeSessionAccountDataService : SessionAccountDataService by mockk(relaxed fun verifyUpdateUserAccountDataEventSucceeds(type: String, content: Content, inverse: Boolean = false) { coVerify(inverse = inverse) { updateUserAccountData(type, content) } } + + fun givenGetUserAccountDataEventsStartWith(type: String, userAccountDataEventList: List) { + every { getUserAccountDataEventsStartWith(type) } returns userAccountDataEventList + } }