diff --git a/changelog.d/7281.wip b/changelog.d/7281.wip new file mode 100644 index 0000000000..c457ffbdb9 --- /dev/null +++ b/changelog.d/7281.wip @@ -0,0 +1 @@ +Links "Enable Notifications for this session" setting to enabled value in pusher diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 600b8461b9..f6c4553790 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -1666,6 +1666,7 @@ Create New Room Create New Space No network. Please check your Internet connection. + Something went wrong. Please check your network connection and try again. "Change network" "Please wait…" Updating your data… diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 2693ca474c..aef482ae2e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -54,6 +54,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo034 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo035 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo036 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo037 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo038 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -62,7 +63,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 37L, + schemaVersion = 38L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -109,5 +110,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 35) MigrateSessionTo035(realm).perform() if (oldVersion < 36) MigrateSessionTo036(realm).perform() if (oldVersion < 37) MigrateSessionTo037(realm).perform() + if (oldVersion < 38) MigrateSessionTo038(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo038.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo038.kt new file mode 100644 index 0000000000..b5848f04cd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo038.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 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.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.PusherEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo038(realm: DynamicRealm) : RealmMigrator(realm, 38) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("PusherEntity") + ?.addField(PusherEntityFields.ENABLED, Boolean::class.java) + ?.addField(PusherEntityFields.DEVICE_ID, String::class.java) + ?.transform { obj -> obj.set(PusherEntityFields.ENABLED, true) } + } +} diff --git a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt index 44cccbd3f5..cda6f5bae8 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt @@ -23,6 +23,7 @@ import im.vector.app.core.resources.AppNameProvider import im.vector.app.core.resources.LocaleProvider import im.vector.app.core.resources.StringProvider import org.matrix.android.sdk.api.session.pushers.HttpPusher +import org.matrix.android.sdk.api.session.pushers.Pusher import java.util.UUID import javax.inject.Inject import kotlin.math.abs @@ -90,6 +91,18 @@ class PushersManager @Inject constructor( ) } + fun getPusherForCurrentSession(): Pusher? { + val session = activeSessionHolder.getSafeActiveSession() ?: return null + val deviceId = session.sessionParams.deviceId + return session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId } + } + + suspend fun togglePusherForCurrentSession(enable: Boolean) { + val session = activeSessionHolder.getSafeActiveSession() ?: return + val pusher = getPusherForCurrentSession() ?: return + session.pushersService().togglePusher(pusher, enable) + } + suspend fun unregisterEmailPusher(email: String) { val currentSession = activeSessionHolder.getSafeActiveSession() ?: return currentSession.pushersService().removeEmailPusher(email) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewEntrySwitchView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewEntrySwitchView.kt index bbefd31dfe..24c7725f29 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewEntrySwitchView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewEntrySwitchView.kt @@ -49,6 +49,7 @@ class SessionOverviewEntrySwitchView @JvmOverloads constructor( setTitle(it) setDescription(it) setSwitchedEnabled(it) + setClickListener() } } @@ -67,10 +68,16 @@ class SessionOverviewEntrySwitchView @JvmOverloads constructor( } private fun setSwitchedEnabled(typedArray: TypedArray) { - val enabled = typedArray.getBoolean(R.styleable.SessionOverviewEntrySwitchView_sessionOverviewEntrySwitchEnabled, true) + val enabled = typedArray.getBoolean(R.styleable.SessionOverviewEntrySwitchView_sessionOverviewEntrySwitchEnabled, false) binding.sessionsOverviewEntrySwitch.isChecked = enabled } + private fun setClickListener() { + binding.root.setOnClickListener { + setChecked(!binding.sessionsOverviewEntrySwitch.isChecked) + } + } + fun setChecked(checked: Boolean) { binding.sessionsOverviewEntrySwitch.isChecked = checked } diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index 37d09d02c9..0202acecc7 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -54,6 +54,7 @@ import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsBaseFragment import im.vector.app.features.settings.VectorSettingsFragmentInteractionListener import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.Session @@ -104,6 +105,10 @@ class VectorSettingsNotificationPreferenceFragment : } findPreference(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY)?.let { + pushersManager.getPusherForCurrentSession()?.let { pusher -> + it.isChecked = pusher.enabled + } + it.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked -> if (isChecked) { unifiedPushHelper.register(requireActivity()) { @@ -117,6 +122,16 @@ class VectorSettingsNotificationPreferenceFragment : } findPreference(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY) ?.summary = unifiedPushHelper.getCurrentDistributorName() + lifecycleScope.launch { + val result = runCatching { + pushersManager.togglePusherForCurrentSession(true) + } + + result.exceptionOrNull()?.let { _ -> + Toast.makeText(context, R.string.error_check_network, Toast.LENGTH_SHORT).show() + it.isChecked = false + } + } } } else { unifiedPushHelper.unregister(pushersManager) diff --git a/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt b/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt index 50c9024e86..750e50d578 100644 --- a/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt +++ b/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt @@ -24,8 +24,12 @@ import im.vector.app.test.fakes.FakeLocaleProvider import im.vector.app.test.fakes.FakePushersService import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeStringProvider +import im.vector.app.test.fixtures.CredentialsFixture import im.vector.app.test.fixtures.CryptoDeviceInfoFixture.aCryptoDeviceInfo +import im.vector.app.test.fixtures.PusherFixture +import im.vector.app.test.fixtures.SessionParamsFixture import io.mockk.mockk +import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.Test import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo @@ -82,4 +86,34 @@ class PushersManagerTest { val httpPusher = pushersService.verifyEnqueueAddHttpPusher() httpPusher shouldBeEqualTo expectedHttpPusher } + + @Test + fun `when getPusherForCurrentSession, then return pusher`() { + val deviceId = "device_id" + val sessionParams = SessionParamsFixture.aSessionParams( + credentials = CredentialsFixture.aCredentials(deviceId = deviceId) + ) + session.givenSessionParams(sessionParams) + val expectedPusher = PusherFixture.aPusher(deviceId = deviceId) + pushersService.givenGetPushers(listOf(expectedPusher)) + + val pusher = pushersManager.getPusherForCurrentSession() + + pusher shouldBeEqualTo expectedPusher + } + + @Test + fun `when togglePusherForCurrentSession, then do service toggle pusher`() = runTest { + val deviceId = "device_id" + val sessionParams = SessionParamsFixture.aSessionParams( + credentials = CredentialsFixture.aCredentials(deviceId = deviceId) + ) + session.givenSessionParams(sessionParams) + val pusher = PusherFixture.aPusher(deviceId = deviceId) + pushersService.givenGetPushers(listOf(pusher)) + + pushersManager.togglePusherForCurrentSession(true) + + pushersService.verifyOnlyGetPushersAndTogglePusherCalled(pusher, true) + } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index da602ae227..73c9c5aa95 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -21,6 +21,7 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule +import im.vector.app.R import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase @@ -55,8 +56,10 @@ import org.junit.Test import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth +import javax.net.ssl.HttpsURLConnection import kotlin.coroutines.Continuation private const val A_SESSION_ID_1 = "session-id-1" @@ -203,6 +206,113 @@ class SessionOverviewViewModelTest { .finish() } + @Test + fun `given another session and no reAuth is needed when handling signout action then signout process is performed`() { + // Given + val deviceFullInfo = mockk() + every { deviceFullInfo.isCurrentDevice } returns false + every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) + givenSignoutSuccess(A_SESSION_ID_1) + every { refreshDevicesUseCase.execute() } just runs + val signoutAction = SessionOverviewAction.SignoutOtherSession + givenCurrentSessionIsTrusted() + val expectedViewState = SessionOverviewViewState( + deviceId = A_SESSION_ID_1, + isCurrentSessionTrusted = true, + deviceInfo = Success(deviceFullInfo), + isLoading = false, + pushers = Loading(), + ) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(signoutAction) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is SessionOverviewViewEvent.SignoutSuccess } + .finish() + verify { + refreshDevicesUseCase.execute() + } + } + + @Test + fun `given another session and server error during signout when handling signout action then signout process is performed`() { + // Given + val deviceFullInfo = mockk() + every { deviceFullInfo.isCurrentDevice } returns false + every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) + val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) + givenSignoutError(A_SESSION_ID_1, serverError) + val signoutAction = SessionOverviewAction.SignoutOtherSession + givenCurrentSessionIsTrusted() + val expectedViewState = SessionOverviewViewState( + deviceId = A_SESSION_ID_1, + isCurrentSessionTrusted = true, + deviceInfo = Success(deviceFullInfo), + isLoading = false, + pushers = Loading(), + ) + fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(signoutAction) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is SessionOverviewViewEvent.SignoutError && it.error.message == AUTH_ERROR_MESSAGE } + .finish() + } + + @Test + fun `given another session and unexpected error during signout when handling signout action then signout process is performed`() { + // Given + val deviceFullInfo = mockk() + every { deviceFullInfo.isCurrentDevice } returns false + every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) + val error = Exception() + givenSignoutError(A_SESSION_ID_1, error) + val signoutAction = SessionOverviewAction.SignoutOtherSession + givenCurrentSessionIsTrusted() + val expectedViewState = SessionOverviewViewState( + deviceId = A_SESSION_ID_1, + isCurrentSessionTrusted = true, + deviceInfo = Success(deviceFullInfo), + isLoading = false, + pushers = Loading(), + ) + fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(signoutAction) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is SessionOverviewViewEvent.SignoutError && it.error.message == AN_ERROR_MESSAGE } + .finish() + } + @Test fun `given another session and reAuth is needed during signout when handling signout action then requestReAuth is sent and pending auth is stored`() { // Given 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 60d50eab03..d506f53e60 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 @@ -29,10 +29,21 @@ import org.matrix.android.sdk.api.session.pushers.PushersService class FakePushersService : PushersService by mockk(relaxed = true) { + fun givenGetPushers(pushers: List) { + every { getPushers() } returns pushers + } + fun givenPushersLive(pushers: List) { every { getPushersLive() } returns liveData { emit(pushers) } } + fun verifyOnlyGetPushersAndTogglePusherCalled(pusher: Pusher, enable: Boolean) { + coVerify(ordering = Ordering.ALL) { + getPushers() + togglePusher(pusher, enable) + } + } + fun verifyOnlyTogglePusherCalled(pusher: Pusher, enable: Boolean) { coVerify(ordering = Ordering.ALL) { getPushersLive() // verifies only getPushersLive and the following togglePusher was called diff --git a/vector/src/test/java/im/vector/app/test/fixtures/CredentialsFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/CredentialsFixture.kt new file mode 100644 index 0000000000..1a70806e76 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fixtures/CredentialsFixture.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 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 im.vector.app.test.fixtures + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.DiscoveryInformation + +object CredentialsFixture { + fun aCredentials( + userId: String = "", + accessToken: String = "", + refreshToken: String? = null, + homeServer: String? = null, + deviceId: String? = null, + discoveryInformation: DiscoveryInformation? = null, + ) = Credentials( + userId, + accessToken, + refreshToken, + homeServer, + deviceId, + discoveryInformation, + ) +} diff --git a/vector/src/test/java/im/vector/app/test/fixtures/SessionParamsFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/SessionParamsFixture.kt new file mode 100644 index 0000000000..598a95707b --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fixtures/SessionParamsFixture.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 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 im.vector.app.test.fixtures + +import im.vector.app.test.fixtures.CredentialsFixture.aCredentials +import io.mockk.mockk +import org.matrix.android.sdk.api.auth.LoginType +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.SessionParams + +object SessionParamsFixture { + fun aSessionParams( + credentials: Credentials = aCredentials(), + homeServerConnectionConfig: HomeServerConnectionConfig = mockk(relaxed = true), + isTokenValid: Boolean = false, + loginType: LoginType = LoginType.UNKNOWN, + ) = SessionParams( + credentials, + homeServerConnectionConfig, + isTokenValid, + loginType, + ) +}