diff --git a/changelog.d/7257.wip b/changelog.d/7257.wip new file mode 100644 index 0000000000..c6f9aefbd8 --- /dev/null +++ b/changelog.d/7257.wip @@ -0,0 +1 @@ +[Device Management] Save "matrix_client_information" events on login/registration 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 df9dcfb903..c73446cf25 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 @@ -66,7 +66,7 @@ internal class DefaultSessionAccountDataService @Inject constructor( override suspend fun updateUserAccountData(type: String, content: Content) { val params = UpdateUserAccountDataTask.AnyParams(type = type, any = content) - awaitCallback { callback -> + awaitCallback { callback -> updateUserAccountDataTask.configureWith(params) { this.retryCount = 5 // TODO Need to refactor retrying out into a helper method. this.callback = callback diff --git a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt index 3f0507305a..c74a945e61 100644 --- a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt +++ b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt @@ -19,10 +19,10 @@ package im.vector.app.core.di import android.content.Context import arrow.core.Option import im.vector.app.ActiveSessionDataSource -import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.extensions.startSyncing import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.services.GuardServiceStarter +import im.vector.app.core.session.ConfigureAndStartSessionUseCase import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.crypto.keysrequest.KeyRequestHandler import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler @@ -50,6 +50,7 @@ class ActiveSessionHolder @Inject constructor( private val sessionInitializer: SessionInitializer, private val applicationContext: Context, private val authenticationService: AuthenticationService, + private val configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase, ) { private var activeSessionReference: AtomicReference = AtomicReference() @@ -109,7 +110,9 @@ class ActiveSessionHolder @Inject constructor( } ?: sessionInitializer.tryInitialize(readCurrentSession = { activeSessionReference.get() }) { session -> setActiveSession(session) - session.configureAndStart(applicationContext, startSyncing = startSync) + runBlocking { + configureAndStartSessionUseCase.execute(session, startSyncing = startSync) + } } } diff --git a/vector/src/main/java/im/vector/app/core/extensions/Session.kt b/vector/src/main/java/im/vector/app/core/extensions/Session.kt index cb1d46efce..4f2d387474 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Session.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Session.kt @@ -24,20 +24,8 @@ import im.vector.app.core.services.VectorSyncAndroidService import im.vector.app.features.session.VectorSessionStore import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState -import org.matrix.android.sdk.api.session.sync.FilterService import timber.log.Timber -fun Session.configureAndStart(context: Context, startSyncing: Boolean = true) { - Timber.i("Configure and start session for $myUserId. startSyncing: $startSyncing") - open() - filterService().setFilter(FilterService.FilterPreset.ElementFilter) - if (startSyncing) { - startSyncing(context) - } - pushersService().refreshPushers() - context.singletonEntryPoint().webRtcCallManager().checkForProtocolsSupportIfNeeded() -} - fun Session.startSyncing(context: Context) { val applicationContext = context.applicationContext if (!syncService().hasAlreadySynced()) { diff --git a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt new file mode 100644 index 0000000000..dfcb92af24 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt @@ -0,0 +1,46 @@ +/* + * 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 + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import im.vector.app.core.extensions.startSyncing +import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase +import im.vector.app.features.call.webrtc.WebRtcCallManager +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.sync.FilterService +import timber.log.Timber +import javax.inject.Inject + +class ConfigureAndStartSessionUseCase @Inject constructor( + @ApplicationContext private val context: Context, + private val webRtcCallManager: WebRtcCallManager, + private val updateMatrixClientInfoUseCase: UpdateMatrixClientInfoUseCase, +) { + + suspend fun execute(session: Session, startSyncing: Boolean = true) { + Timber.i("Configure and start session for ${session.myUserId}. startSyncing: $startSyncing") + session.open() + session.filterService().setFilter(FilterService.FilterPreset.ElementFilter) + if (startSyncing) { + session.startSyncing(context) + } + session.pushersService().refreshPushers() + webRtcCallManager.checkForProtocolsSupportIfNeeded() + updateMatrixClientInfoUseCase.execute(session) + } +} diff --git a/vector/src/main/java/im/vector/app/core/session/clientinfo/GetMatrixClientInfoUseCase.kt b/vector/src/main/java/im/vector/app/core/session/clientinfo/GetMatrixClientInfoUseCase.kt new file mode 100644 index 0000000000..18b731ea09 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/session/clientinfo/GetMatrixClientInfoUseCase.kt @@ -0,0 +1,34 @@ +/* + * 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 org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.toModel +import javax.inject.Inject + +/** + * This use case retrieves the current account data event containing extended client info + * for a given deviceId. + */ +class GetMatrixClientInfoUseCase @Inject constructor() { + + fun execute(session: Session, deviceId: String): MatrixClientInfoContent? { + val type = MATRIX_CLIENT_INFO_KEY_PREFIX + deviceId + val content = session.accountDataService().getUserAccountDataEvent(type)?.content + return content.toModel() + } +} diff --git a/vector/src/main/java/im/vector/app/core/session/clientinfo/MatrixClientInfoContent.kt b/vector/src/main/java/im/vector/app/core/session/clientinfo/MatrixClientInfoContent.kt new file mode 100644 index 0000000000..aacee6edef --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/session/clientinfo/MatrixClientInfoContent.kt @@ -0,0 +1,33 @@ +/* + * 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 com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class MatrixClientInfoContent( + // app name + @Json(name = "name") + val name: String? = null, + // app version + @Json(name = "version") + val version: String? = null, + // app url (optional, applicable only for web) + @Json(name = "url") + val url: String? = null, +) diff --git a/vector/src/main/java/im/vector/app/core/session/clientinfo/NoDeviceIdError.kt b/vector/src/main/java/im/vector/app/core/session/clientinfo/NoDeviceIdError.kt new file mode 100644 index 0000000000..c2b4445205 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/session/clientinfo/NoDeviceIdError.kt @@ -0,0 +1,19 @@ +/* + * 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 + +class NoDeviceIdError : IllegalStateException("device id is empty") diff --git a/vector/src/main/java/im/vector/app/core/session/clientinfo/SessionExtendedInfoConstants.kt b/vector/src/main/java/im/vector/app/core/session/clientinfo/SessionExtendedInfoConstants.kt new file mode 100644 index 0000000000..80f69df1f8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/session/clientinfo/SessionExtendedInfoConstants.kt @@ -0,0 +1,22 @@ +/* + * 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 + +/** + * Prefix for the key account data event which holds client info. + */ +const val MATRIX_CLIENT_INFO_KEY_PREFIX = "io.element.matrix_client_information." diff --git a/vector/src/main/java/im/vector/app/core/session/clientinfo/SetMatrixClientInfoUseCase.kt b/vector/src/main/java/im/vector/app/core/session/clientinfo/SetMatrixClientInfoUseCase.kt new file mode 100644 index 0000000000..5ec428eed0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/session/clientinfo/SetMatrixClientInfoUseCase.kt @@ -0,0 +1,38 @@ +/* + * 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 org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.toContent +import javax.inject.Inject + +/** + * This use case sets the account data event containing extended client info. + */ +class SetMatrixClientInfoUseCase @Inject constructor() { + + suspend fun execute(session: Session, clientInfo: MatrixClientInfoContent): Result = runCatching { + val deviceId = session.sessionParams.deviceId.orEmpty() + if (deviceId.isNotEmpty()) { + val type = MATRIX_CLIENT_INFO_KEY_PREFIX + deviceId + session.accountDataService() + .updateUserAccountData(type, clientInfo.toContent()) + } else { + throw NoDeviceIdError() + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/session/clientinfo/UpdateMatrixClientInfoUseCase.kt b/vector/src/main/java/im/vector/app/core/session/clientinfo/UpdateMatrixClientInfoUseCase.kt new file mode 100644 index 0000000000..a46ca4eedb --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/session/clientinfo/UpdateMatrixClientInfoUseCase.kt @@ -0,0 +1,52 @@ +/* + * 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.resources.AppNameProvider +import im.vector.app.core.resources.BuildMeta +import org.matrix.android.sdk.api.session.Session +import timber.log.Timber +import javax.inject.Inject + +/** + * This use case updates if needed the account data event containing extended client info. + */ +class UpdateMatrixClientInfoUseCase @Inject constructor( + private val appNameProvider: AppNameProvider, + private val buildMeta: BuildMeta, + private val getMatrixClientInfoUseCase: GetMatrixClientInfoUseCase, + private val setMatrixClientInfoUseCase: SetMatrixClientInfoUseCase, +) { + + suspend fun execute(session: Session) = runCatching { + val clientInfo = MatrixClientInfoContent( + name = appNameProvider.getAppName(), + version = buildMeta.versionName + ) + val deviceId = session.sessionParams.deviceId.orEmpty() + if (deviceId.isNotEmpty()) { + val storedClientInfo = getMatrixClientInfoUseCase.execute(session, deviceId) + Timber.d("storedClientInfo=$storedClientInfo, current client info=$clientInfo") + if (clientInfo != storedClientInfo) { + Timber.d("client info need to be updated") + return setMatrixClientInfoUseCase.execute(session, clientInfo) + } + } else { + throw NoDeviceIdError() + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt index 79d06a0864..b46f22c58f 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt @@ -30,9 +30,9 @@ import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider +import im.vector.app.core.session.ConfigureAndStartSessionUseCase import im.vector.app.core.utils.ensureTrailingSlash import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -64,7 +64,8 @@ class LoginViewModel @AssistedInject constructor( private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory, private val reAuthHelper: ReAuthHelper, private val stringProvider: StringProvider, - private val homeServerHistoryService: HomeServerHistoryService + private val homeServerHistoryService: HomeServerHistoryService, + private val configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase, ) : VectorViewModel(initialState) { @AssistedFactory @@ -732,7 +733,7 @@ class LoginViewModel @AssistedInject constructor( activeSessionHolder.setActiveSession(session) authenticationService.reset() - session.configureAndStart(applicationContext) + configureAndStartSessionUseCase.execute(session) setState { copy( asyncLoginAction = Success(Unit) 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 4f8a77f25d..9bb52fb1a5 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 @@ -26,13 +26,13 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.cancelCurrentOnSet -import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.extensions.inferNoConnectivity import im.vector.app.core.extensions.isMatrixId import im.vector.app.core.extensions.toReducedUrl import im.vector.app.core.extensions.vectorStore import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider +import im.vector.app.core.session.ConfigureAndStartSessionUseCase import im.vector.app.core.utils.ensureProtocol import im.vector.app.core.utils.ensureTrailingSlash import im.vector.app.features.VectorFeatures @@ -91,6 +91,7 @@ class OnboardingViewModel @AssistedInject constructor( private val vectorOverrides: VectorOverrides, private val registrationActionHandler: RegistrationActionHandler, private val sdkIntProvider: BuildVersionSdkIntProvider, + private val configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase, ) : VectorViewModel(initialState) { @AssistedFactory @@ -616,7 +617,7 @@ class OnboardingViewModel @AssistedInject constructor( activeSessionHolder.setActiveSession(session) authenticationService.reset() - session.configureAndStart(applicationContext) + configureAndStartSessionUseCase.execute(session) when (authenticationDescription) { is AuthenticationDescription.Register -> { diff --git a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt new file mode 100644 index 0000000000..b879806930 --- /dev/null +++ b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt @@ -0,0 +1,105 @@ +/* + * 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 + +import im.vector.app.core.extensions.startSyncing +import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase +import im.vector.app.test.fakes.FakeContext +import im.vector.app.test.fakes.FakeSession +import im.vector.app.test.fakes.FakeWebRtcCallManager +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.runs +import io.mockk.unmockkAll +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.api.session.sync.FilterService + +class ConfigureAndStartSessionUseCaseTest { + + private val fakeContext = FakeContext() + private val fakeWebRtcCallManager = FakeWebRtcCallManager() + private val fakeUpdateMatrixClientInfoUseCase = mockk() + + private val configureAndStartSessionUseCase = ConfigureAndStartSessionUseCase( + context = fakeContext.instance, + webRtcCallManager = fakeWebRtcCallManager.instance, + updateMatrixClientInfoUseCase = fakeUpdateMatrixClientInfoUseCase, + ) + + @Before + fun setup() { + mockkStatic("im.vector.app.core.extensions.SessionKt") + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given a session and start sync needed when configuring and starting the session then it should be configured properly`() = runTest { + // Given + val fakeSession = givenASession() + fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds() + coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) } + + // When + configureAndStartSessionUseCase.execute(fakeSession, startSyncing = true) + + // Then + verify { fakeSession.startSyncing(fakeContext.instance) } + fakeSession.fakeFilterService.verifySetFilter(FilterService.FilterPreset.ElementFilter) + fakeSession.fakePushersService.verifyRefreshPushers() + fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded() + coVerify { fakeUpdateMatrixClientInfoUseCase.execute(fakeSession) } + } + + @Test + fun `given a session and no start sync needed when configuring and starting the session then it should be configured properly`() = runTest { + // Given + val fakeSession = givenASession() + fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds() + coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) } + + // When + configureAndStartSessionUseCase.execute(fakeSession, startSyncing = false) + + // Then + verify(inverse = true) { fakeSession.startSyncing(fakeContext.instance) } + fakeSession.fakeFilterService.verifySetFilter(FilterService.FilterPreset.ElementFilter) + fakeSession.fakePushersService.verifyRefreshPushers() + fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded() + coVerify { fakeUpdateMatrixClientInfoUseCase.execute(fakeSession) } + } + + private fun givenASession(): FakeSession { + val fakeSession = FakeSession() + every { fakeSession.open() } just runs + fakeSession.fakeFilterService.givenSetFilterSucceeds() + every { fakeSession.startSyncing(any()) } just runs + fakeSession.fakePushersService.givenRefreshPushersSucceeds() + return fakeSession + } +} diff --git a/vector/src/test/java/im/vector/app/core/session/clientinfo/GetMatrixClientInfoUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/session/clientinfo/GetMatrixClientInfoUseCaseTest.kt new file mode 100644 index 0000000000..4be8218ab5 --- /dev/null +++ b/vector/src/test/java/im/vector/app/core/session/clientinfo/GetMatrixClientInfoUseCaseTest.kt @@ -0,0 +1,62 @@ +/* + * 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.FakeSession +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test + +private const val A_DEVICE_ID = "device-id" +private const val A_CLIENT_NAME = "client-name" +private const val A_CLIENT_VERSION = "client-version" +private const val A_CLIENT_URL = "client-url" + +class GetMatrixClientInfoUseCaseTest { + + private val fakeSession = FakeSession() + + private val getMatrixClientInfoUseCase = GetMatrixClientInfoUseCase() + + @Test + fun `given a device id and existing content when getting the info then result should contain that info`() { + // Given + givenClientInfoContent(A_DEVICE_ID) + val expectedClientInfo = MatrixClientInfoContent( + name = A_CLIENT_NAME, + version = A_CLIENT_VERSION, + url = A_CLIENT_URL, + ) + + // When + val result = getMatrixClientInfoUseCase.execute(fakeSession, A_DEVICE_ID) + + // Then + result shouldBeEqualTo expectedClientInfo + } + + private fun givenClientInfoContent(deviceId: String) { + val type = MATRIX_CLIENT_INFO_KEY_PREFIX + deviceId + val content = mapOf( + Pair("name", A_CLIENT_NAME), + Pair("version", A_CLIENT_VERSION), + Pair("url", A_CLIENT_URL), + ) + fakeSession + .fakeSessionAccountDataService + .givenGetUserAccountDataEventReturns(type, content) + } +} diff --git a/vector/src/test/java/im/vector/app/core/session/clientinfo/SetMatrixClientInfoUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/session/clientinfo/SetMatrixClientInfoUseCaseTest.kt new file mode 100644 index 0000000000..4345de45c4 --- /dev/null +++ b/vector/src/test/java/im/vector/app/core/session/clientinfo/SetMatrixClientInfoUseCaseTest.kt @@ -0,0 +1,106 @@ +/* + * 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.FakeSession +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldBeInstanceOf +import org.junit.Test +import org.matrix.android.sdk.api.session.events.model.toContent + +private const val A_DEVICE_ID = "device-id" + +class SetMatrixClientInfoUseCaseTest { + + private val fakeSession = FakeSession() + + private val setMatrixClientInfoUseCase = SetMatrixClientInfoUseCase() + + @Test + fun `given client info and no error when setting the info then account data is correctly updated`() = runTest { + // Given + val type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID + val clientInfo = givenClientInfo() + val content = clientInfo.toContent() + fakeSession + .givenSessionId(A_DEVICE_ID) + fakeSession + .fakeSessionAccountDataService + .givenUpdateUserAccountDataEventSucceeds() + + // When + val result = setMatrixClientInfoUseCase.execute(fakeSession, clientInfo) + + // Then + result.isSuccess shouldBe true + fakeSession + .fakeSessionAccountDataService + .verifyUpdateUserAccountDataEventSucceeds(type, content) + } + + @Test + fun `given client info and error during update when setting the info then result is failure`() = runTest { + // Given + val type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID + val clientInfo = givenClientInfo() + val content = clientInfo.toContent() + fakeSession + .givenSessionId(A_DEVICE_ID) + val error = Exception() + fakeSession + .fakeSessionAccountDataService + .givenUpdateUserAccountDataEventFailsWithError(error) + + // When + val result = setMatrixClientInfoUseCase.execute(fakeSession, clientInfo) + + // Then + result.isFailure shouldBe true + result.exceptionOrNull() shouldBeEqualTo error + fakeSession + .fakeSessionAccountDataService + .verifyUpdateUserAccountDataEventSucceeds(type, content) + } + + @Test + fun `given client info and null device id when setting the info then result is failure`() = runTest { + // Given + val type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID + val clientInfo = givenClientInfo() + val content = clientInfo.toContent() + fakeSession + .givenSessionId(null) + + // When + val result = setMatrixClientInfoUseCase.execute(fakeSession, clientInfo) + + // Then + result.isFailure shouldBe true + result.exceptionOrNull() shouldBeInstanceOf NoDeviceIdError::class + fakeSession + .fakeSessionAccountDataService + .verifyUpdateUserAccountDataEventSucceeds(type, content, inverse = true) + } + + private fun givenClientInfo() = MatrixClientInfoContent( + name = "name", + version = "version", + url = null, + ) +} diff --git a/vector/src/test/java/im/vector/app/core/session/clientinfo/UpdateMatrixClientInfoUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/session/clientinfo/UpdateMatrixClientInfoUseCaseTest.kt new file mode 100644 index 0000000000..7f1727ff37 --- /dev/null +++ b/vector/src/test/java/im/vector/app/core/session/clientinfo/UpdateMatrixClientInfoUseCaseTest.kt @@ -0,0 +1,149 @@ +/* + * 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.resources.BuildMeta +import im.vector.app.test.fakes.FakeAppNameProvider +import im.vector.app.test.fakes.FakeSession +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldBeInstanceOf +import org.junit.Test + +private const val AN_APP_NAME_1 = "app_name_1" +private const val AN_APP_NAME_2 = "app_name_2" +private const val A_VERSION_NAME_1 = "version_name_1" +private const val A_VERSION_NAME_2 = "version_name_2" +private const val A_SESSION_ID = "session-id" + +class UpdateMatrixClientInfoUseCaseTest { + + private val fakeSession = FakeSession() + private val fakeAppNameProvider = FakeAppNameProvider() + private val fakeBuildMeta = mockk() + private val getMatrixClientInfoUseCase = mockk() + private val setMatrixClientInfoUseCase = mockk() + + private val updateMatrixClientInfoUseCase = UpdateMatrixClientInfoUseCase( + appNameProvider = fakeAppNameProvider, + buildMeta = fakeBuildMeta, + getMatrixClientInfoUseCase = getMatrixClientInfoUseCase, + setMatrixClientInfoUseCase = setMatrixClientInfoUseCase, + ) + + @Test + fun `given current client info is different than the stored one when trying to update then new client info is set`() = runTest { + // Given + givenCurrentAppName(AN_APP_NAME_1) + givenCurrentVersionName(A_VERSION_NAME_1) + givenStoredClientInfo(AN_APP_NAME_2, A_VERSION_NAME_2) + givenSetClientInfoSucceeds() + val expectedClientInfoToSet = MatrixClientInfoContent( + name = AN_APP_NAME_1, + version = A_VERSION_NAME_1, + ) + + // When + val result = updateMatrixClientInfoUseCase.execute(fakeSession) + + // Then + result.isSuccess shouldBe true + coVerify { setMatrixClientInfoUseCase.execute(fakeSession, match { it == expectedClientInfoToSet }) } + } + + @Test + fun `given error during set of new client info when trying to update then result is failure`() = runTest { + // Given + givenCurrentAppName(AN_APP_NAME_1) + givenCurrentVersionName(A_VERSION_NAME_1) + givenStoredClientInfo(AN_APP_NAME_2, A_VERSION_NAME_2) + val error = Exception() + givenSetClientInfoFails(error) + val expectedClientInfoToSet = MatrixClientInfoContent( + name = AN_APP_NAME_1, + version = A_VERSION_NAME_1, + ) + + // When + val result = updateMatrixClientInfoUseCase.execute(fakeSession) + + // Then + result.isFailure shouldBe true + result.exceptionOrNull() shouldBeEqualTo error + coVerify { setMatrixClientInfoUseCase.execute(fakeSession, match { it == expectedClientInfoToSet }) } + } + + @Test + fun `given current client info is equal to the stored one when trying to update then nothing is done`() = runTest { + // Given + givenCurrentAppName(AN_APP_NAME_1) + givenCurrentVersionName(A_VERSION_NAME_1) + givenStoredClientInfo(AN_APP_NAME_1, A_VERSION_NAME_1) + + // When + val result = updateMatrixClientInfoUseCase.execute(fakeSession) + + // Then + result.isSuccess shouldBe true + coVerify(inverse = true) { setMatrixClientInfoUseCase.execute(fakeSession, any()) } + } + + @Test + fun `given no session id for current session when trying to update then nothing is done`() = runTest { + // Given + givenCurrentAppName(AN_APP_NAME_1) + givenCurrentVersionName(A_VERSION_NAME_1) + fakeSession.givenSessionId(null) + + // When + val result = updateMatrixClientInfoUseCase.execute(fakeSession) + + // Then + result.isFailure shouldBe true + result.exceptionOrNull() shouldBeInstanceOf NoDeviceIdError::class + coVerify(inverse = true) { setMatrixClientInfoUseCase.execute(fakeSession, any()) } + } + + private fun givenCurrentAppName(appName: String) { + fakeAppNameProvider.givenAppName(appName) + } + + private fun givenCurrentVersionName(versionName: String) { + every { fakeBuildMeta.versionName } returns versionName + } + + private fun givenStoredClientInfo(appName: String, versionName: String) { + fakeSession.givenSessionId(A_SESSION_ID) + every { getMatrixClientInfoUseCase.execute(fakeSession, A_SESSION_ID) } returns MatrixClientInfoContent( + name = appName, + version = versionName, + ) + } + + private fun givenSetClientInfoSucceeds() { + coEvery { setMatrixClientInfoUseCase.execute(any(), any()) } returns Result.success(Unit) + } + + private fun givenSetClientInfoFails(error: Throwable) { + coEvery { setMatrixClientInfoUseCase.execute(any(), any()) } returns Result.failure(error) + } +} 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 c3f6b86cb4..82adc70fe3 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 @@ -20,6 +20,7 @@ import android.net.Uri import android.os.Build import com.airbnb.mvrx.test.MavericksTestRule import im.vector.app.R +import im.vector.app.core.session.ConfigureAndStartSessionUseCase import im.vector.app.features.login.LoginConfig import im.vector.app.features.login.LoginMode import im.vector.app.features.login.ReAuthHelper @@ -50,6 +51,7 @@ import im.vector.app.test.fixtures.a401ServerError import im.vector.app.test.fixtures.aHomeServerCapabilities import im.vector.app.test.fixtures.anUnrecognisedCertificateError import im.vector.app.test.test +import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.Before @@ -111,6 +113,7 @@ class OnboardingViewModelTest { private val fakeStartAuthenticationFlowUseCase = FakeStartAuthenticationFlowUseCase() private val fakeHomeServerHistoryService = FakeHomeServerHistoryService() private val fakeLoginWizard = FakeLoginWizard() + private val fakeConfigureAndStartSessionUseCase = mockk() private var initialState = OnboardingViewState() private lateinit var viewModel: OnboardingViewModel @@ -1093,6 +1096,7 @@ class OnboardingViewModelTest { FakeVectorOverrides(), fakeRegistrationActionHandler.instance, TestBuildVersionSdkIntProvider().also { it.value = Build.VERSION_CODES.O }, + fakeConfigureAndStartSessionUseCase, ).also { viewModel = it initialState = state @@ -1132,7 +1136,7 @@ class OnboardingViewModelTest { private fun givenInitialisesSession(session: Session) { fakeActiveSessionHolder.expectSetsActiveSession(session) fakeAuthenticationService.expectReset() - fakeSession.expectStartsSyncing() + fakeSession.expectStartsSyncing(fakeConfigureAndStartSessionUseCase) } private fun givenRegistrationResultFor(action: RegisterAction, result: RegistrationActionHandler.Result) { diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeFilterService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeFilterService.kt new file mode 100644 index 0000000000..4332368127 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeFilterService.kt @@ -0,0 +1,35 @@ +/* + * 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.test.fakes + +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.verify +import org.matrix.android.sdk.api.session.sync.FilterService + +class FakeFilterService : FilterService by mockk() { + + fun givenSetFilterSucceeds() { + every { setFilter(any()) } just runs + } + + fun verifySetFilter(filterPreset: FilterService.FilterPreset) { + verify { setFilter(filterPreset) } + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakePushersService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakePushersService.kt index d506f53e60..4ad3052538 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakePushersService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakePushersService.kt @@ -20,6 +20,7 @@ import androidx.lifecycle.liveData import io.mockk.Ordering import io.mockk.coVerify import io.mockk.every +import io.mockk.justRun import io.mockk.mockk import io.mockk.slot import io.mockk.verify @@ -56,4 +57,12 @@ class FakePushersService : PushersService by mockk(relaxed = true) { verify { enqueueAddHttpPusher(capture(httpPusherSlot)) } return httpPusherSlot.captured } + + fun givenRefreshPushersSucceeds() { + justRun { refreshPushers() } + } + + fun verifyRefreshPushers() { + verify { refreshPushers() } + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt index bf437c123b..c40e4a8fc4 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt @@ -16,9 +16,9 @@ package im.vector.app.test.fakes -import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.extensions.startSyncing import im.vector.app.core.extensions.vectorStore +import im.vector.app.core.session.ConfigureAndStartSessionUseCase import im.vector.app.features.session.VectorSessionStore import im.vector.app.test.testCoroutineDispatchers import io.mockk.coEvery @@ -43,6 +43,8 @@ class FakeSession( val fakeRoomService: FakeRoomService = FakeRoomService(), val fakePushersService: FakePushersService = FakePushersService(), private val fakeEventService: FakeEventService = FakeEventService(), + val fakeSessionAccountDataService: FakeSessionAccountDataService = FakeSessionAccountDataService(), + val fakeFilterService: FakeFilterService = FakeFilterService(), ) : Session by mockk(relaxed = true) { init { @@ -60,6 +62,8 @@ class FakeSession( override fun roomService() = fakeRoomService override fun eventService() = fakeEventService override fun pushersService() = fakePushersService + override fun accountDataService() = fakeSessionAccountDataService + override fun filterService() = fakeFilterService fun givenVectorStore(vectorSessionStore: VectorSessionStore) { coEvery { @@ -69,9 +73,9 @@ class FakeSession( } } - fun expectStartsSyncing() { + fun expectStartsSyncing(configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase) { coJustRun { - this@FakeSession.configureAndStart(any(), startSyncing = true) + configureAndStartSessionUseCase.execute(this@FakeSession, startSyncing = true) this@FakeSession.startSyncing(any()) } } @@ -80,7 +84,7 @@ class FakeSession( every { this@FakeSession.sessionParams } returns sessionParams } - fun givenSessionId(sessionId: String): SessionParams { + fun givenSessionId(sessionId: String?): SessionParams { val sessionParams = mockk() every { sessionParams.deviceId } returns sessionId givenSessionParams(sessionParams) 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 new file mode 100644 index 0000000000..34a0d8edb3 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt @@ -0,0 +1,46 @@ +/* + * 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.test.fakes + +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import org.matrix.android.sdk.api.session.accountdata.SessionAccountDataService +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.events.model.Content + +class FakeSessionAccountDataService : SessionAccountDataService by mockk() { + + fun givenGetUserAccountDataEventReturns(type: String, content: Content) { + every { getUserAccountDataEvent(type) } returns UserAccountDataEvent(type, content) + } + + fun givenUpdateUserAccountDataEventSucceeds() { + coEvery { updateUserAccountData(any(), any()) } just runs + } + + fun givenUpdateUserAccountDataEventFailsWithError(error: Exception) { + coEvery { updateUserAccountData(any(), any()) } throws error + } + + fun verifyUpdateUserAccountDataEventSucceeds(type: String, content: Content, inverse: Boolean = false) { + coVerify(inverse = inverse) { updateUserAccountData(type, content) } + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeWebRtcCallManager.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeWebRtcCallManager.kt new file mode 100644 index 0000000000..b3664bafa1 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeWebRtcCallManager.kt @@ -0,0 +1,37 @@ +/* + * 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.test.fakes + +import im.vector.app.features.call.webrtc.WebRtcCallManager +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.verify + +class FakeWebRtcCallManager { + + val instance = mockk() + + fun givenCheckForProtocolsSupportIfNeededSucceeds() { + every { instance.checkForProtocolsSupportIfNeeded() } just runs + } + + fun verifyCheckForProtocolsSupportIfNeeded() { + verify { instance.checkForProtocolsSupportIfNeeded() } + } +}