Merge pull request #7476 from vector-im/fix/mna/push-toggle-check-support

[Session manager] Hide push notification toggle when there is no server support (PSG-970)
This commit is contained in:
Maxime NATUREL 2022-11-04 09:19:04 +01:00 committed by GitHub
commit 4621488f21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 572 additions and 65 deletions

1
changelog.d/7457.bugfix Normal file
View File

@ -0,0 +1 @@
[Session manager] Hide push notification toggle when there is no server support

View File

@ -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 {

View File

@ -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_REMOTE_TOGGLE_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_REMOTE_TOGGLE_PUSH_NOTIFICATIONS_MSC3881).orFalse()
}

View File

@ -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()
}
}

View File

@ -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,
)
}

View File

@ -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()
}
}

View File

@ -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

View File

@ -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) {

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -0,0 +1,60 @@
/*
* 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.distinctUntilChanged
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,
) {
fun execute(deviceId: String): Flow<NotificationsStatus> {
val session = activeSessionHolder.getSafeActiveSession()
return when {
session == null -> flowOf(NotificationsStatus.NOT_SUPPORTED)
checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(deviceId) -> {
session.flow()
.liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
.unwrap()
.map { it.content.toModel<LocalNotificationSettingsContent>()?.isSilenced?.not() }
.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)
}
}
}

View File

@ -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,
}

View File

@ -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,

View File

@ -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
}
}

View File

@ -29,6 +29,8 @@ 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.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 +38,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 +61,7 @@ class SessionOverviewViewModel @AssistedInject constructor(
private val pendingAuthHandler: PendingAuthHandler,
private val activeSessionHolder: ActiveSessionHolder,
private val togglePushNotificationUseCase: TogglePushNotificationUseCase,
private val getNotificationsStatusUseCase: GetNotificationsStatusUseCase,
refreshDevicesUseCase: RefreshDevicesUseCase,
) : VectorSessionsListViewModel<SessionOverviewViewState, SessionOverviewAction, SessionOverviewViewEvent>(
initialState, activeSessionHolder, refreshDevicesUseCase
@ -81,7 +78,7 @@ class SessionOverviewViewModel @AssistedInject constructor(
refreshPushers()
observeSessionInfo(initialState.deviceId)
observeCurrentSessionInfo()
observePushers(initialState.deviceId)
observeNotificationsStatus(initialState.deviceId)
}
private fun refreshPushers() {
@ -107,20 +104,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<LocalNotificationSettingsContent>()?.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 +219,6 @@ class SessionOverviewViewModel @AssistedInject constructor(
private fun handleTogglePusherAction(action: SessionOverviewAction.TogglePushNotifications) {
viewModelScope.launch {
togglePushNotificationUseCase.execute(action.deviceId, action.enabled)
setState { copy(notificationsEnabled = action.enabled) }
}
}
}

View File

@ -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<DeviceFullInfo> = Uninitialized,
val isLoading: Boolean = false,
val notificationsEnabled: Boolean = false,
val notificationsStatus: NotificationsStatus = NotificationsStatus.NOT_SUPPORTED,
) : MavericksState {
constructor(args: SessionOverviewArgs) : this(
deviceId = args.deviceId

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,141 @@
/*
* 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 io.mockk.verifyOrder
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<CheckIfCanTogglePushNotificationsViaPusherUseCase>()
private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase =
mockk<CheckIfCanTogglePushNotificationsViaAccountDataUseCase>()
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
verifyOrder {
// we should first check account data
fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(A_DEVICE_ID)
fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute()
}
}
@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
}
}

View File

@ -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<CheckIfCanTogglePushNotificationsViaPusherUseCase>()
private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase =
mockk<CheckIfCanTogglePushNotificationsViaAccountDataUseCase>()
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(),

View File

@ -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<RefreshDevicesUseCase>()
private val togglePushNotificationUseCase = FakeTogglePushNotificationUseCase()
private val fakeGetNotificationsStatusUseCase = mockk<GetNotificationsStatusUseCase>()
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()
}
}

View File

@ -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() {

View File

@ -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

View File

@ -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,
)