From 38fe5569789931b72ca6ecce6571a8317df1caff Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 26 Oct 2022 15:30:44 +0200 Subject: [PATCH 1/7] Adding changelog entry --- changelog.d/7457.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7457.bugfix diff --git a/changelog.d/7457.bugfix b/changelog.d/7457.bugfix new file mode 100644 index 0000000000..9dfbc53329 --- /dev/null +++ b/changelog.d/7457.bugfix @@ -0,0 +1 @@ +[Session manager] Hide push notification toggle when there is no server support From 1acb42f61d95b6dbbe2a9392312c40aeaeb6b933 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 27 Oct 2022 11:34:41 +0200 Subject: [PATCH 2/7] Adding use case to check support for new enabled field support --- .../homeserver/HomeServerCapabilities.kt | 5 ++ .../sdk/internal/auth/version/Versions.kt | 11 ++++ .../database/RealmSessionStoreMigration.kt | 4 +- .../mapper/HomeServerCapabilitiesMapper.kt | 3 +- .../database/migration/MigrateSessionTo042.kt | 31 ++++++++++ .../model/HomeServerCapabilitiesEntity.kt | 1 + .../GetHomeServerCapabilitiesTask.kt | 12 +++- ...TogglePushNotificationsViaPusherUseCase.kt | 35 +++++++++++ ...lePushNotificationsViaPusherUseCaseTest.kt | 61 +++++++++++++++++++ .../fixtures/HomeserverCapabilityFixture.kt | 20 +++--- 10 files changed, 169 insertions(+), 14 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo042.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt index 773e870ffd..11638837cc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt @@ -70,6 +70,11 @@ data class HomeServerCapabilities( * True if the home server supports threaded read receipts and unread notifications. */ val canUseThreadReadReceiptsAndNotifications: Boolean = false, + + /** + * True if the home server supports remote toggle of Pusher for a given device. + */ + val canRemotelyTogglePushNotificationsOfDevices: Boolean = false, ) { enum class RoomCapabilitySupport { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt index 1245d8df4b..bc2d4a5aef 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.auth.version import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.extensions.orFalse /** * Model for https://matrix.org/docs/spec/client_server/latest#get-matrix-client-versions. @@ -56,6 +57,7 @@ private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable" private const val FEATURE_QR_CODE_LOGIN = "org.matrix.msc3882" private const val FEATURE_THREADS_MSC3771 = "org.matrix.msc3771" private const val FEATURE_THREADS_MSC3773 = "org.matrix.msc3773" +private const val FEATURE_PUSH_NOTIFICATIONS_MSC3881 = "org.matrix.msc3881" /** * Return true if the SDK supports this homeserver version. @@ -142,3 +144,12 @@ private fun Versions.getMaxVersion(): HomeServerVersion { ?.maxOrNull() ?: HomeServerVersion.r0_0_0 } + +/** + * Indicate if the server supports MSC3881: https://github.com/matrix-org/matrix-spec-proposals/pull/3881. + * + * @return true if remote toggle of push notifications is supported + */ +internal fun Versions.doesServerSupportRemoteToggleOfPushNotifications(): Boolean { + return unstableFeatures?.get(FEATURE_PUSH_NOTIFICATIONS_MSC3881).orFalse() +} 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 58c015b13b..30836c027e 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 @@ -58,6 +58,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo038 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo039 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo040 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo041 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo042 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -66,7 +67,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 41L, + schemaVersion = 42L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -117,5 +118,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 39) MigrateSessionTo039(realm).perform() if (oldVersion < 40) MigrateSessionTo040(realm).perform() if (oldVersion < 41) MigrateSessionTo041(realm).perform() + if (oldVersion < 42) MigrateSessionTo042(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt index 3528ca0051..89657ad882 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -45,7 +45,8 @@ internal object HomeServerCapabilitiesMapper { canUseThreading = entity.canUseThreading, canControlLogoutDevices = entity.canControlLogoutDevices, canLoginWithQrCode = entity.canLoginWithQrCode, - canUseThreadReadReceiptsAndNotifications = entity.canUseThreadReadReceiptsAndNotifications + canUseThreadReadReceiptsAndNotifications = entity.canUseThreadReadReceiptsAndNotifications, + canRemotelyTogglePushNotificationsOfDevices = entity.canRemotelyTogglePushNotificationsOfDevices, ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo042.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo042.kt new file mode 100644 index 0000000000..8826d894c1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo042.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.HomeServerCapabilitiesEntityFields +import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo042(realm: DynamicRealm) : RealmMigrator(realm, 42) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("HomeServerCapabilitiesEntity") + ?.addField(HomeServerCapabilitiesEntityFields.CAN_REMOTELY_TOGGLE_PUSH_NOTIFICATIONS_OF_DEVICES, Boolean::class.java) + ?.forceRefreshOfHomeServerCapabilities() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt index 89f1e50b30..2b60f7723c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt @@ -33,6 +33,7 @@ internal open class HomeServerCapabilitiesEntity( var canControlLogoutDevices: Boolean = false, var canLoginWithQrCode: Boolean = false, var canUseThreadReadReceiptsAndNotifications: Boolean = false, + var canRemotelyTogglePushNotificationsOfDevices: Boolean = false, ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index a5953d870c..11e86a5c51 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.internal.auth.version.Versions import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices import org.matrix.android.sdk.internal.auth.version.doesServerSupportQrCodeLogin +import org.matrix.android.sdk.internal.auth.version.doesServerSupportRemoteToggleOfPushNotifications import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreadUnreadNotifications import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk @@ -141,13 +142,18 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( } if (getVersionResult != null) { - homeServerCapabilitiesEntity.lastVersionIdentityServerSupported = getVersionResult.isLoginAndRegistrationSupportedBySdk() - homeServerCapabilitiesEntity.canControlLogoutDevices = getVersionResult.doesServerSupportLogoutDevices() + homeServerCapabilitiesEntity.lastVersionIdentityServerSupported = + getVersionResult.isLoginAndRegistrationSupportedBySdk() + homeServerCapabilitiesEntity.canControlLogoutDevices = + getVersionResult.doesServerSupportLogoutDevices() homeServerCapabilitiesEntity.canUseThreading = /* capabilities?.threads?.enabled.orFalse() || */ getVersionResult.doesServerSupportThreads() homeServerCapabilitiesEntity.canUseThreadReadReceiptsAndNotifications = getVersionResult.doesServerSupportThreadUnreadNotifications() - homeServerCapabilitiesEntity.canLoginWithQrCode = getVersionResult.doesServerSupportQrCodeLogin() + homeServerCapabilitiesEntity.canLoginWithQrCode = + getVersionResult.doesServerSupportQrCodeLogin() + homeServerCapabilitiesEntity.canRemotelyTogglePushNotificationsOfDevices = + getVersionResult.doesServerSupportRemoteToggleOfPushNotifications() } if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt new file mode 100644 index 0000000000..0d5bce663a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.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.features.settings.devices.v2.notification + +import im.vector.app.core.di.ActiveSessionHolder +import org.matrix.android.sdk.api.extensions.orFalse +import javax.inject.Inject + +class CheckIfCanTogglePushNotificationsViaPusherUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { + + fun execute(): Boolean { + return activeSessionHolder + .getSafeActiveSession() + ?.homeServerCapabilitiesService() + ?.getHomeServerCapabilities() + ?.canRemotelyTogglePushNotificationsOfDevices + .orFalse() + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt new file mode 100644 index 0000000000..51874be1bc --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt @@ -0,0 +1,61 @@ +/* + * 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.features.settings.devices.v2.notification + +import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fixtures.aHomeServerCapabilities +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test + +private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canRemotelyTogglePushNotificationsOfDevices = true) + +class CheckIfCanTogglePushNotificationsViaPusherUseCaseTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + + private val checkIfCanTogglePushNotificationsViaPusherUseCase = + CheckIfCanTogglePushNotificationsViaPusherUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance, + ) + + @Test + fun `given current session when execute then toggle capability is returned`() { + // Given + fakeActiveSessionHolder + .fakeSession + .fakeHomeServerCapabilitiesService + .givenCapabilities(A_HOMESERVER_CAPABILITIES) + + // When + val result = checkIfCanTogglePushNotificationsViaPusherUseCase.execute() + + // Then + result shouldBeEqualTo A_HOMESERVER_CAPABILITIES.canRemotelyTogglePushNotificationsOfDevices + } + + @Test + fun `given no current session when execute then false is returned`() { + // Given + fakeActiveSessionHolder.givenGetSafeActiveSessionReturns(null) + + // When + val result = checkIfCanTogglePushNotificationsViaPusherUseCase.execute() + + // Then + result shouldBeEqualTo false + } +} diff --git a/vector/src/test/java/im/vector/app/test/fixtures/HomeserverCapabilityFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/HomeserverCapabilityFixture.kt index a4d9869a89..c9f32c2cf2 100644 --- a/vector/src/test/java/im/vector/app/test/fixtures/HomeserverCapabilityFixture.kt +++ b/vector/src/test/java/im/vector/app/test/fixtures/HomeserverCapabilityFixture.kt @@ -27,14 +27,16 @@ fun aHomeServerCapabilities( maxUploadFileSize: Long = 100L, lastVersionIdentityServerSupported: Boolean = false, defaultIdentityServerUrl: String? = null, - roomVersions: RoomVersionCapabilities? = null + roomVersions: RoomVersionCapabilities? = null, + canRemotelyTogglePushNotificationsOfDevices: Boolean = true, ) = HomeServerCapabilities( - canChangePassword, - canChangeDisplayName, - canChangeAvatar, - canChange3pid, - maxUploadFileSize, - lastVersionIdentityServerSupported, - defaultIdentityServerUrl, - roomVersions + canChangePassword = canChangePassword, + canChangeDisplayName = canChangeDisplayName, + canChangeAvatar = canChangeAvatar, + canChange3pid = canChange3pid, + maxUploadFileSize = maxUploadFileSize, + lastVersionIdentityServerSupported = lastVersionIdentityServerSupported, + defaultIdentityServerUrl = defaultIdentityServerUrl, + roomVersions = roomVersions, + canRemotelyTogglePushNotificationsOfDevices = canRemotelyTogglePushNotificationsOfDevices, ) From 62912f891cde60bd07fd73735dff126816d85910 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 27 Oct 2022 14:36:06 +0200 Subject: [PATCH 3/7] Introducing a NotificationsStatus to render the push notification toggle in session overview screen --- ...ePushNotificationsViaAccountDataUseCase.kt | 33 +++++++++ .../GetNotificationsStatusUseCase.kt | 59 +++++++++++++++ .../v2/notification/NotificationsStatus.kt | 23 ++++++ .../TogglePushNotificationUseCase.kt | 16 +++-- .../v2/overview/SessionOverviewFragment.kt | 22 +++--- .../v2/overview/SessionOverviewViewModel.kt | 33 +++------ .../v2/overview/SessionOverviewViewState.kt | 3 +- ...hNotificationsViaAccountDataUseCaseTest.kt | 71 +++++++++++++++++++ .../TogglePushNotificationUseCaseTest.kt | 26 ++++++- .../overview/SessionOverviewViewModelTest.kt | 23 +++--- .../fakes/FakeSessionAccountDataService.kt | 4 +- .../FakeTogglePushNotificationUseCase.kt | 2 +- 12 files changed, 264 insertions(+), 51 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/NotificationsStatus.kt rename vector/src/main/java/im/vector/app/features/settings/devices/v2/{overview => notification}/TogglePushNotificationUseCase.kt (67%) create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt rename vector/src/test/java/im/vector/app/features/settings/devices/v2/{overview => notification}/TogglePushNotificationUseCaseTest.kt (67%) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt new file mode 100644 index 0000000000..dbf9adca14 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.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.features.settings.devices.v2.notification + +import im.vector.app.core.di.ActiveSessionHolder +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import javax.inject.Inject + +class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { + + fun execute(deviceId: String): Boolean { + return activeSessionHolder + .getSafeActiveSession() + ?.accountDataService() + ?.getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) != null + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt new file mode 100644 index 0000000000..1eb612988a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt @@ -0,0 +1,59 @@ +/* + * 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.features.settings.devices.v2.notification + +import im.vector.app.core.di.ActiveSessionHolder +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.unwrap +import javax.inject.Inject + +class GetNotificationsStatusUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, + private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase, + private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, +) { + + // TODO add unit tests + fun execute(deviceId: String): Flow { + val session = activeSessionHolder.getSafeActiveSession() + return when { + session == null -> emptyFlow() + checkIfCanTogglePushNotificationsViaPusherUseCase.execute() -> { + session.flow() + .livePushers() + .map { it.filter { pusher -> pusher.deviceId == deviceId } } + .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } } + .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } + } + checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId) -> { + session.flow() + .liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) + .unwrap() + .map { it.content.toModel()?.isSilenced?.not() } + .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } + } + else -> flowOf(NotificationsStatus.NOT_SUPPORTED) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/NotificationsStatus.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/NotificationsStatus.kt new file mode 100644 index 0000000000..7ff1f04381 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/NotificationsStatus.kt @@ -0,0 +1,23 @@ +/* + * 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.features.settings.devices.v2.notification + +enum class NotificationsStatus { + ENABLED, + DISABLED, + NOT_SUPPORTED, +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt similarity index 67% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCase.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt index 45c234aaef..be9012e9f1 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.settings.devices.v2.overview +package im.vector.app.features.settings.devices.v2.notification import im.vector.app.core.di.ActiveSessionHolder import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent @@ -24,17 +24,21 @@ import javax.inject.Inject class TogglePushNotificationUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, + private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase, + private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, ) { suspend fun execute(deviceId: String, enabled: Boolean) { val session = activeSessionHolder.getSafeActiveSession() ?: return - val devicePusher = session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId } - devicePusher?.let { pusher -> - session.pushersService().togglePusher(pusher, enabled) + + if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute()) { + val devicePusher = session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId } + devicePusher?.let { pusher -> + session.pushersService().togglePusher(pusher, enabled) + } } - val accountData = session.accountDataService().getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) - if (accountData != null) { + if (checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId)) { val newNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = !enabled) session.accountDataService().updateUserAccountData( UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId, diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt index a1cd7ea586..620372f810 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt @@ -24,6 +24,7 @@ import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isGone import androidx.core.view.isVisible import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel @@ -43,6 +44,7 @@ import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet +import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus import im.vector.app.features.workers.signout.SignOutUiWorker import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.extensions.orFalse @@ -177,7 +179,7 @@ class SessionOverviewFragment : updateEntryDetails(state.deviceId) updateSessionInfo(state) updateLoading(state.isLoading) - updatePushNotificationToggle(state.deviceId, state.notificationsEnabled) + updatePushNotificationToggle(state.deviceId, state.notificationsStatus) } private fun updateToolbar(viewState: SessionOverviewViewState) { @@ -218,15 +220,19 @@ class SessionOverviewFragment : } } - private fun updatePushNotificationToggle(deviceId: String, enabled: Boolean) { - views.sessionOverviewPushNotifications.apply { - setOnCheckedChangeListener(null) - setChecked(enabled) - post { - setOnCheckedChangeListener { _, isChecked -> - viewModel.handle(SessionOverviewAction.TogglePushNotifications(deviceId, isChecked)) + private fun updatePushNotificationToggle(deviceId: String, notificationsStatus: NotificationsStatus) { + views.sessionOverviewPushNotifications.isGone = notificationsStatus == NotificationsStatus.NOT_SUPPORTED + when (notificationsStatus) { + NotificationsStatus.ENABLED, NotificationsStatus.DISABLED -> { + views.sessionOverviewPushNotifications.setOnCheckedChangeListener(null) + views.sessionOverviewPushNotifications.setChecked(notificationsStatus == NotificationsStatus.ENABLED) + views.sessionOverviewPushNotifications.post { + views.sessionOverviewPushNotifications.setOnCheckedChangeListener { _, isChecked -> + viewModel.handle(SessionOverviewAction.TogglePushNotifications(deviceId, isChecked)) + } } } + else -> Unit } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index 21054270f8..1aa5f676cc 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -29,6 +29,9 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel +import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase +import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus +import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase @@ -36,21 +39,15 @@ import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSes import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent 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.extensions.orFalse import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel -import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth -import org.matrix.android.sdk.flow.flow -import org.matrix.android.sdk.flow.unwrap import timber.log.Timber import javax.net.ssl.HttpsURLConnection import kotlin.coroutines.Continuation @@ -65,6 +62,7 @@ class SessionOverviewViewModel @AssistedInject constructor( private val pendingAuthHandler: PendingAuthHandler, private val activeSessionHolder: ActiveSessionHolder, private val togglePushNotificationUseCase: TogglePushNotificationUseCase, + private val getNotificationsStatusUseCase: GetNotificationsStatusUseCase, refreshDevicesUseCase: RefreshDevicesUseCase, ) : VectorSessionsListViewModel( initialState, activeSessionHolder, refreshDevicesUseCase @@ -81,7 +79,7 @@ class SessionOverviewViewModel @AssistedInject constructor( refreshPushers() observeSessionInfo(initialState.deviceId) observeCurrentSessionInfo() - observePushers(initialState.deviceId) + observeNotificationsStatus(initialState.deviceId) } private fun refreshPushers() { @@ -107,20 +105,9 @@ class SessionOverviewViewModel @AssistedInject constructor( } } - private fun observePushers(deviceId: String) { - val session = activeSessionHolder.getSafeActiveSession() ?: return - val pusherFlow = session.flow() - .livePushers() - .map { it.filter { pusher -> pusher.deviceId == deviceId } } - .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } } - - val accountDataFlow = session.flow() - .liveUserAccountData(TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) - .unwrap() - .map { it.content.toModel()?.isSilenced?.not() } - - merge(pusherFlow, accountDataFlow) - .onEach { it?.let { setState { copy(notificationsEnabled = it) } } } + private fun observeNotificationsStatus(deviceId: String) { + getNotificationsStatusUseCase.execute(deviceId) + .onEach { setState { copy(notificationsStatus = it) } } .launchIn(viewModelScope) } @@ -233,7 +220,9 @@ class SessionOverviewViewModel @AssistedInject constructor( private fun handleTogglePusherAction(action: SessionOverviewAction.TogglePushNotifications) { viewModelScope.launch { togglePushNotificationUseCase.execute(action.deviceId, action.enabled) - setState { copy(notificationsEnabled = action.enabled) } + // TODO should not be needed => test without + val status = if (action.enabled) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED + setState { copy(notificationsStatus = status) } } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt index 440805bad6..019dd2d724 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt @@ -20,13 +20,14 @@ import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.features.settings.devices.v2.DeviceFullInfo +import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus data class SessionOverviewViewState( val deviceId: String, val isCurrentSessionTrusted: Boolean = false, val deviceInfo: Async = Uninitialized, val isLoading: Boolean = false, - val notificationsEnabled: Boolean = false, + val notificationsStatus: NotificationsStatus = NotificationsStatus.NOT_SUPPORTED, ) : MavericksState { constructor(args: SessionOverviewArgs) : this( deviceId = args.deviceId diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt new file mode 100644 index 0000000000..0303444605 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt @@ -0,0 +1,71 @@ +/* + * 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.features.settings.devices.v2.notification + +import im.vector.app.test.fakes.FakeActiveSessionHolder +import io.mockk.mockk +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes + +private const val A_DEVICE_ID = "device-id" + +class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + + private val checkIfCanTogglePushNotificationsViaAccountDataUseCase = + CheckIfCanTogglePushNotificationsViaAccountDataUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance, + ) + + @Test + fun `given current session and an account data for the device id when execute then result is true`() { + // Given + fakeActiveSessionHolder + .fakeSession + .accountDataService() + .givenGetUserAccountDataEventReturns( + type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID, + content = mockk(), + ) + + // When + val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) + + // Then + result shouldBeEqualTo true + } + + @Test + fun `given current session and NO account data for the device id when execute then result is false`() { + // Given + fakeActiveSessionHolder + .fakeSession + .accountDataService() + .givenGetUserAccountDataEventReturns( + type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID, + content = null, + ) + + // When + val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) + + // Then + result shouldBeEqualTo false + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt similarity index 67% rename from vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCaseTest.kt rename to vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt index dc64c74836..0a649354f9 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/TogglePushNotificationUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt @@ -14,10 +14,12 @@ * limitations under the License. */ -package im.vector.app.features.settings.devices.v2.overview +package im.vector.app.features.settings.devices.v2.notification import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fixtures.PusherFixture +import io.mockk.every +import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Test import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent @@ -27,10 +29,21 @@ import org.matrix.android.sdk.api.session.events.model.toContent class TogglePushNotificationUseCaseTest { private val activeSessionHolder = FakeActiveSessionHolder() - private val togglePushNotificationUseCase = TogglePushNotificationUseCase(activeSessionHolder.instance) + private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = + mockk() + private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase = + mockk() + + private val togglePushNotificationUseCase = + TogglePushNotificationUseCase( + activeSessionHolder = activeSessionHolder.instance, + checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase, + checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase, + ) @Test fun `when execute, then toggle enabled for device pushers`() = runTest { + // Given val sessionId = "a_session_id" val pushers = listOf( PusherFixture.aPusher(deviceId = sessionId, enabled = false), @@ -38,14 +51,19 @@ class TogglePushNotificationUseCaseTest { ) activeSessionHolder.fakeSession.pushersService().givenPushersLive(pushers) activeSessionHolder.fakeSession.pushersService().givenGetPushers(pushers) + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns true + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(sessionId) } returns false + // When togglePushNotificationUseCase.execute(sessionId, true) + // Then activeSessionHolder.fakeSession.pushersService().verifyTogglePusherCalled(pushers.first(), true) } @Test fun `when execute, then toggle local notification settings`() = runTest { + // Given val sessionId = "a_session_id" val pushers = listOf( PusherFixture.aPusher(deviceId = sessionId, enabled = false), @@ -56,9 +74,13 @@ class TogglePushNotificationUseCaseTest { UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId, LocalNotificationSettingsContent(isSilenced = true).toContent() ) + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns false + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(sessionId) } returns true + // When togglePushNotificationUseCase.execute(sessionId, true) + // Then activeSessionHolder.fakeSession.accountDataService().verifyUpdateUserAccountDataEventSucceeds( UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId, LocalNotificationSettingsContent(isSilenced = false).toContent(), 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 544059b77f..c0ba6ce28b 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 @@ -23,6 +23,8 @@ 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.notification.GetNotificationsStatusUseCase +import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase @@ -32,7 +34,6 @@ import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase import im.vector.app.test.fakes.FakeVerificationService -import im.vector.app.test.fixtures.PusherFixture.aPusher import im.vector.app.test.test import im.vector.app.test.testDispatcher import io.mockk.coEvery @@ -87,6 +88,8 @@ class SessionOverviewViewModelTest { private val fakePendingAuthHandler = FakePendingAuthHandler() private val refreshDevicesUseCase = mockk() private val togglePushNotificationUseCase = FakeTogglePushNotificationUseCase() + private val fakeGetNotificationsStatusUseCase = mockk() + private val notificationsStatus = NotificationsStatus.ENABLED private fun createViewModel() = SessionOverviewViewModel( initialState = SessionOverviewViewState(args), @@ -99,6 +102,7 @@ class SessionOverviewViewModelTest { activeSessionHolder = fakeActiveSessionHolder.instance, refreshDevicesUseCase = refreshDevicesUseCase, togglePushNotificationUseCase = togglePushNotificationUseCase.instance, + getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase, ) @Before @@ -108,6 +112,7 @@ class SessionOverviewViewModelTest { every { SystemClock.elapsedRealtime() } returns 1234 givenVerificationService() + every { fakeGetNotificationsStatusUseCase.execute(A_SESSION_ID_1) } returns flowOf(notificationsStatus) } @After @@ -131,7 +136,7 @@ class SessionOverviewViewModelTest { deviceId = A_SESSION_ID_1, deviceInfo = Success(deviceFullInfo), isCurrentSessionTrusted = true, - notificationsEnabled = true, + notificationsStatus = notificationsStatus, ) val viewModel = createViewModel() @@ -227,7 +232,7 @@ class SessionOverviewViewModelTest { isCurrentSessionTrusted = true, deviceInfo = Success(deviceFullInfo), isLoading = false, - notificationsEnabled = true, + notificationsStatus = notificationsStatus, ) // When @@ -264,7 +269,7 @@ class SessionOverviewViewModelTest { isCurrentSessionTrusted = true, deviceInfo = Success(deviceFullInfo), isLoading = false, - notificationsEnabled = true, + notificationsStatus = notificationsStatus, ) fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE) @@ -299,7 +304,7 @@ class SessionOverviewViewModelTest { isCurrentSessionTrusted = true, deviceInfo = Success(deviceFullInfo), isLoading = false, - notificationsEnabled = true, + notificationsStatus = notificationsStatus, ) fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE) @@ -466,13 +471,13 @@ class SessionOverviewViewModelTest { @Test fun `when viewModel init, then observe pushers and emit to state`() { - val pushers = listOf(aPusher(deviceId = A_SESSION_ID_1)) - fakeActiveSessionHolder.fakeSession.pushersService().givenPushersLive(pushers) + val notificationStatus = NotificationsStatus.ENABLED + every { fakeGetNotificationsStatusUseCase.execute(A_SESSION_ID_1) } returns flowOf(notificationStatus) val viewModel = createViewModel() viewModel.test() - .assertLatestState { state -> state.notificationsEnabled } + .assertLatestState { state -> state.notificationsStatus == notificationStatus } .finish() } @@ -483,6 +488,6 @@ class SessionOverviewViewModelTest { viewModel.handle(SessionOverviewAction.TogglePushNotifications(A_SESSION_ID_1, true)) togglePushNotificationUseCase.verifyExecute(A_SESSION_ID_1, true) - viewModel.test().assertLatestState { state -> state.notificationsEnabled }.finish() + viewModel.test().assertLatestState { state -> state.notificationsStatus == NotificationsStatus.ENABLED }.finish() } } 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 615330463b..c44fc4a497 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 @@ -28,8 +28,8 @@ import org.matrix.android.sdk.api.session.events.model.Content class FakeSessionAccountDataService : SessionAccountDataService by mockk(relaxed = true) { - fun givenGetUserAccountDataEventReturns(type: String, content: Content) { - every { getUserAccountDataEvent(type) } returns UserAccountDataEvent(type, content) + fun givenGetUserAccountDataEventReturns(type: String, content: Content?) { + every { getUserAccountDataEvent(type) } returns content?.let { UserAccountDataEvent(type, it) } } fun givenUpdateUserAccountDataEventSucceeds() { diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt index 92e311cfb7..bfbbb87705 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt @@ -16,7 +16,7 @@ package im.vector.app.test.fakes -import im.vector.app.features.settings.devices.v2.overview.TogglePushNotificationUseCase +import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase import io.mockk.coJustRun import io.mockk.coVerify import io.mockk.mockk From e67cc2b2dbb73a37dbadca599159fea8fa5adf0b Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 27 Oct 2022 15:03:11 +0200 Subject: [PATCH 4/7] Adding unit tests on GetNotificationsStatusUseCase --- .../GetNotificationsStatusUseCase.kt | 4 +- .../GetNotificationsStatusUseCaseTest.kt | 135 ++++++++++++++++++ 2 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt index 1eb612988a..091338c46a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt @@ -18,7 +18,6 @@ package im.vector.app.features.settings.devices.v2.notification import im.vector.app.core.di.ActiveSessionHolder import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent @@ -34,11 +33,10 @@ class GetNotificationsStatusUseCase @Inject constructor( private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, ) { - // TODO add unit tests fun execute(deviceId: String): Flow { val session = activeSessionHolder.getSafeActiveSession() return when { - session == null -> emptyFlow() + session == null -> flowOf(NotificationsStatus.NOT_SUPPORTED) checkIfCanTogglePushNotificationsViaPusherUseCase.execute() -> { session.flow() .livePushers() diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt new file mode 100644 index 0000000000..598c8df83f --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt @@ -0,0 +1,135 @@ +/* + * 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.features.settings.devices.v2.notification + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fixtures.PusherFixture +import im.vector.app.test.testDispatcher +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.events.model.toContent + +private const val A_DEVICE_ID = "device-id" + +class GetNotificationsStatusUseCaseTest { + + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = + mockk() + private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase = + mockk() + + private val getNotificationsStatusUseCase = + GetNotificationsStatusUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance, + checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase, + checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase, + ) + + @Before + fun setup() { + Dispatchers.setMain(testDispatcher) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `given NO current session when execute then resulting flow contains NOT_SUPPORTED value`() = runTest { + // Given + fakeActiveSessionHolder.givenGetSafeActiveSessionReturns(null) + + // When + val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID) + + // Then + result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED + } + + @Test + fun `given current session and toggle is not supported when execute then resulting flow contains NOT_SUPPORTED value`() = runTest { + // Given + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns false + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) } returns false + + // When + val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID) + + // Then + result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED + } + + @Test + fun `given current session and toggle via pusher is supported when execute then resulting flow contains status based on pusher value`() = runTest { + // Given + val pushers = listOf( + PusherFixture.aPusher( + deviceId = A_DEVICE_ID, + enabled = true, + ) + ) + fakeActiveSessionHolder.fakeSession.pushersService().givenPushersLive(pushers) + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns true + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) } returns false + + // When + val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID) + + // Then + result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED + } + + @Test + fun `given current session and toggle via account data is supported when execute then resulting flow contains status based on settings value`() = runTest { + // Given + fakeActiveSessionHolder + .fakeSession + .accountDataService() + .givenGetUserAccountDataEventReturns( + type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID, + content = LocalNotificationSettingsContent( + isSilenced = false + ).toContent(), + ) + every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() } returns false + every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) } returns true + + // When + val result = getNotificationsStatusUseCase.execute(A_DEVICE_ID) + + // Then + result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED + } +} From 52a77e074f4d7503dc1a362b6e4b14d0c6f61cf2 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 27 Oct 2022 15:12:17 +0200 Subject: [PATCH 5/7] Renaming const for feature value --- .../org/matrix/android/sdk/internal/auth/version/Versions.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt index bc2d4a5aef..f4de6a9ae9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt @@ -57,7 +57,7 @@ private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable" private const val FEATURE_QR_CODE_LOGIN = "org.matrix.msc3882" private const val FEATURE_THREADS_MSC3771 = "org.matrix.msc3771" private const val FEATURE_THREADS_MSC3773 = "org.matrix.msc3773" -private const val FEATURE_PUSH_NOTIFICATIONS_MSC3881 = "org.matrix.msc3881" +private const val FEATURE_REMOTE_TOGGLE_PUSH_NOTIFICATIONS_MSC3881 = "org.matrix.msc3881" /** * Return true if the SDK supports this homeserver version. @@ -151,5 +151,5 @@ private fun Versions.getMaxVersion(): HomeServerVersion { * @return true if remote toggle of push notifications is supported */ internal fun Versions.doesServerSupportRemoteToggleOfPushNotifications(): Boolean { - return unstableFeatures?.get(FEATURE_PUSH_NOTIFICATIONS_MSC3881).orFalse() + return unstableFeatures?.get(FEATURE_REMOTE_TOGGLE_PUSH_NOTIFICATIONS_MSC3881).orFalse() } From ac05e757beb8d40e4c41782d2230a5d8b6035ad9 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 27 Oct 2022 15:20:43 +0200 Subject: [PATCH 6/7] Small improvement to avoid tou many viewState updates --- .../devices/v2/notification/GetNotificationsStatusUseCase.kt | 3 +++ .../settings/devices/v2/overview/SessionOverviewViewModel.kt | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt index 091338c46a..313c1678cb 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt @@ -18,6 +18,7 @@ package im.vector.app.features.settings.devices.v2.notification import im.vector.app.core.di.ActiveSessionHolder import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent @@ -43,6 +44,7 @@ class GetNotificationsStatusUseCase @Inject constructor( .map { it.filter { pusher -> pusher.deviceId == deviceId } } .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } } .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } + .distinctUntilChanged() } checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId) -> { session.flow() @@ -50,6 +52,7 @@ class GetNotificationsStatusUseCase @Inject constructor( .unwrap() .map { it.content.toModel()?.isSilenced?.not() } .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } + .distinctUntilChanged() } else -> flowOf(NotificationsStatus.NOT_SUPPORTED) } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index 1aa5f676cc..e6aa7c2747 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -30,7 +30,6 @@ import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase -import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult @@ -220,9 +219,6 @@ class SessionOverviewViewModel @AssistedInject constructor( private fun handleTogglePusherAction(action: SessionOverviewAction.TogglePushNotifications) { viewModelScope.launch { togglePushNotificationUseCase.execute(action.deviceId, action.enabled) - // TODO should not be needed => test without - val status = if (action.enabled) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED - setState { copy(notificationsStatus = status) } } } } From e9daef97b660ce8dda1db215517b982b52889ac5 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 3 Nov 2022 11:27:02 +0100 Subject: [PATCH 7/7] Fix order of check to get notification status --- .../GetNotificationsStatusUseCase.kt | 16 ++++++++-------- .../GetNotificationsStatusUseCaseTest.kt | 6 ++++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt index 313c1678cb..69659bf23f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt @@ -38,14 +38,6 @@ class GetNotificationsStatusUseCase @Inject constructor( val session = activeSessionHolder.getSafeActiveSession() return when { session == null -> flowOf(NotificationsStatus.NOT_SUPPORTED) - checkIfCanTogglePushNotificationsViaPusherUseCase.execute() -> { - session.flow() - .livePushers() - .map { it.filter { pusher -> pusher.deviceId == deviceId } } - .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } } - .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } - .distinctUntilChanged() - } checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId) -> { session.flow() .liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) @@ -54,6 +46,14 @@ class GetNotificationsStatusUseCase @Inject constructor( .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } .distinctUntilChanged() } + checkIfCanTogglePushNotificationsViaPusherUseCase.execute() -> { + session.flow() + .livePushers() + .map { it.filter { pusher -> pusher.deviceId == deviceId } } + .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } } + .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } + .distinctUntilChanged() + } else -> flowOf(NotificationsStatus.NOT_SUPPORTED) } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt index 598c8df83f..b13018a20d 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt @@ -22,6 +22,7 @@ import im.vector.app.test.fixtures.PusherFixture import im.vector.app.test.testDispatcher import io.mockk.every import io.mockk.mockk +import io.mockk.verifyOrder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.test.resetMain @@ -89,6 +90,11 @@ class GetNotificationsStatusUseCaseTest { // Then result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED + verifyOrder { + // we should first check account data + fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID) + fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute() + } } @Test