Do not rely only on distributor name but consider value (appId) as well.

This will fix issue when multiple UnifiedPush distributor with the same friendly name are available on the phone.
Fixes #4306
This commit is contained in:
Benoit Marty 2025-02-26 10:55:33 +01:00
parent aca34c0dec
commit 68a76f83a0
7 changed files with 95 additions and 40 deletions

View file

@ -83,19 +83,19 @@ class NotificationSettingsPresenter @Inject constructor(
} }
} }
} }
// List of Distributor names // List of Distributors
val distributorNames = remember { val availableDistributors = remember {
distributors.map { it.second.name }.toImmutableList() distributors.map { it.second }.toImmutableList()
} }
var currentDistributorName by remember { mutableStateOf<AsyncData<String>>(AsyncData.Uninitialized) } var currentDistributor by remember { mutableStateOf<AsyncData<Distributor>>(AsyncData.Uninitialized) }
var refreshPushProvider by remember { mutableIntStateOf(0) } var refreshPushProvider by remember { mutableIntStateOf(0) }
LaunchedEffect(refreshPushProvider) { LaunchedEffect(refreshPushProvider) {
val p = pushService.getCurrentPushProvider() val p = pushService.getCurrentPushProvider()
val name = p?.getCurrentDistributor(matrixClient.sessionId)?.name val distributor = p?.getCurrentDistributor(matrixClient.sessionId)
currentDistributorName = if (name != null) { currentDistributor = if (distributor != null) {
AsyncData.Success(name) AsyncData.Success(distributor)
} else { } else {
AsyncData.Failure(Exception("Failed to get current push provider")) AsyncData.Failure(Exception("Failed to get current push provider"))
} }
@ -108,24 +108,23 @@ class NotificationSettingsPresenter @Inject constructor(
) = launch { ) = launch {
showChangePushProviderDialog = false showChangePushProviderDialog = false
data ?: return@launch data ?: return@launch
// No op if the value is the same. val (pushProvider, distributor) = data
if (data.second.name == currentDistributorName.dataOrNull()) return@launch // No op if the distributor is the same.
currentDistributorName = AsyncData.Loading(currentDistributorName.dataOrNull()) if (distributor == currentDistributor.dataOrNull()) return@launch
data.let { (pushProvider, distributor) -> currentDistributor = AsyncData.Loading(currentDistributor.dataOrNull())
pushService.registerWith( pushService.registerWith(
matrixClient = matrixClient, matrixClient = matrixClient,
pushProvider = pushProvider, pushProvider = pushProvider,
distributor = distributor distributor = distributor
)
.fold(
{
refreshPushProvider++
},
{
currentDistributor = AsyncData.Failure(it)
}
) )
.fold(
{
refreshPushProvider++
},
{
currentDistributorName = AsyncData.Failure(it)
}
)
}
} }
fun handleEvents(event: NotificationSettingsEvents) { fun handleEvents(event: NotificationSettingsEvents) {
@ -162,8 +161,8 @@ class NotificationSettingsPresenter @Inject constructor(
appNotificationsEnabled = appNotificationsEnabled.value appNotificationsEnabled = appNotificationsEnabled.value
), ),
changeNotificationSettingAction = changeNotificationSettingAction.value, changeNotificationSettingAction = changeNotificationSettingAction.value,
currentPushDistributor = currentDistributorName, currentPushDistributor = currentDistributor,
availablePushDistributors = distributorNames, availablePushDistributors = availableDistributors,
showChangePushProviderDialog = showChangePushProviderDialog, showChangePushProviderDialog = showChangePushProviderDialog,
fullScreenIntentPermissionsState = key(refreshFullScreenIntentSettings) { fullScreenIntentPermissionsPresenter.present() }, fullScreenIntentPermissionsState = key(refreshFullScreenIntentSettings) { fullScreenIntentPermissionsPresenter.present() },
eventSink = ::handleEvents eventSink = ::handleEvents

View file

@ -12,6 +12,7 @@ import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.pushproviders.api.Distributor
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@Immutable @Immutable
@ -19,8 +20,8 @@ data class NotificationSettingsState(
val matrixSettings: MatrixSettings, val matrixSettings: MatrixSettings,
val appSettings: AppSettings, val appSettings: AppSettings,
val changeNotificationSettingAction: AsyncAction<Unit>, val changeNotificationSettingAction: AsyncAction<Unit>,
val currentPushDistributor: AsyncData<String>, val currentPushDistributor: AsyncData<Distributor>,
val availablePushDistributors: ImmutableList<String>, val availablePushDistributors: ImmutableList<Distributor>,
val showChangePushProviderDialog: Boolean, val showChangePushProviderDialog: Boolean,
val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState, val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState,
val eventSink: (NotificationSettingsEvents) -> Unit, val eventSink: (NotificationSettingsEvents) -> Unit,

View file

@ -13,6 +13,7 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.pushproviders.api.Distributor
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -24,11 +25,19 @@ open class NotificationSettingsStateProvider : PreviewParameterProvider<Notifica
aValidNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Loading), aValidNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Loading),
aValidNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Failure(Throwable("error"))), aValidNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Failure(Throwable("error"))),
aValidNotificationSettingsState( aValidNotificationSettingsState(
availablePushDistributors = listOf("Firebase"), availablePushDistributors = listOf(aDistributor("Firebase")),
changeNotificationSettingAction = AsyncAction.Failure(Throwable("error")), changeNotificationSettingAction = AsyncAction.Failure(Throwable("error")),
), ),
aValidNotificationSettingsState(availablePushDistributors = listOf("Firebase")), aValidNotificationSettingsState(availablePushDistributors = listOf(aDistributor("Firebase"))),
aValidNotificationSettingsState(showChangePushProviderDialog = true), aValidNotificationSettingsState(showChangePushProviderDialog = true),
aValidNotificationSettingsState(
availablePushDistributors = listOf(
aDistributor("Firebase"),
aDistributor("ntfy", "app.id1"),
aDistributor("ntfy", "app.id2"),
),
showChangePushProviderDialog = true,
),
aValidNotificationSettingsState(currentPushDistributor = AsyncData.Loading()), aValidNotificationSettingsState(currentPushDistributor = AsyncData.Loading()),
aValidNotificationSettingsState(currentPushDistributor = AsyncData.Failure(Exception("Failed to change distributor"))), aValidNotificationSettingsState(currentPushDistributor = AsyncData.Failure(Exception("Failed to change distributor"))),
aInvalidNotificationSettingsState(), aInvalidNotificationSettingsState(),
@ -45,8 +54,11 @@ fun aValidNotificationSettingsState(
inviteForMeNotificationsEnabled: Boolean = true, inviteForMeNotificationsEnabled: Boolean = true,
systemNotificationsEnabled: Boolean = true, systemNotificationsEnabled: Boolean = true,
appNotificationEnabled: Boolean = true, appNotificationEnabled: Boolean = true,
currentPushDistributor: AsyncData<String> = AsyncData.Success("Firebase"), currentPushDistributor: AsyncData<Distributor> = AsyncData.Success(aDistributor("Firebase")),
availablePushDistributors: List<String> = listOf("Firebase", "ntfy"), availablePushDistributors: List<Distributor> = listOf(
aDistributor("Firebase"),
aDistributor("ntfy"),
),
showChangePushProviderDialog: Boolean = false, showChangePushProviderDialog: Boolean = false,
fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(), fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(),
eventSink: (NotificationSettingsEvents) -> Unit = {}, eventSink: (NotificationSettingsEvents) -> Unit = {},
@ -88,3 +100,11 @@ fun aInvalidNotificationSettingsState(
fullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(), fullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(),
eventSink = eventSink, eventSink = eventSink,
) )
fun aDistributor(
name: String = "Name",
value: String = "$name Value",
) = Distributor(
value = value,
name = name,
)

View file

@ -206,7 +206,7 @@ private fun NotificationSettingsContentView(
stringResource(id = CommonStrings.common_error) stringResource(id = CommonStrings.common_error)
) )
is AsyncData.Success -> ListItemContent.Text( is AsyncData.Success -> ListItemContent.Text(
state.currentPushDistributor.dataOrNull() ?: "" state.currentPushDistributor.dataOrNull()?.name ?: ""
) )
}, },
onClick = { onClick = {
@ -219,8 +219,14 @@ private fun NotificationSettingsContentView(
if (state.showChangePushProviderDialog) { if (state.showChangePushProviderDialog) {
SingleSelectionDialog( SingleSelectionDialog(
title = stringResource(id = R.string.screen_advanced_settings_choose_distributor_dialog_title_android), title = stringResource(id = R.string.screen_advanced_settings_choose_distributor_dialog_title_android),
options = state.availablePushDistributors.map { options = state.availablePushDistributors.map { distributor ->
ListOption(title = it) // If there are several distributors with the same name, use the full name
val title = if (state.availablePushDistributors.count { it.name == distributor.name } > 1) {
distributor.fullName
} else {
distributor.name
}
ListOption(title = title)
}.toImmutableList(), }.toImmutableList(),
initialSelection = state.availablePushDistributors.indexOf(state.currentPushDistributor.dataOrNull()), initialSelection = state.availablePushDistributors.indexOf(state.currentPushDistributor.dataOrNull()),
onSelectOption = { index -> onSelectOption = { index ->

View file

@ -240,8 +240,11 @@ class NotificationSettingsPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitLastSequentialItem() val initialState = awaitLastSequentialItem()
assertThat(initialState.currentPushDistributor).isEqualTo(AsyncData.Success("aDistributorName0")) assertThat(initialState.currentPushDistributor).isEqualTo(AsyncData.Success(Distributor(value = "aDistributorValue0", name = "aDistributorName0")))
assertThat(initialState.availablePushDistributors).containsExactly("aDistributorName0", "aDistributorName1") assertThat(initialState.availablePushDistributors).containsExactly(
Distributor(value = "aDistributorValue0", name = "aDistributorName0"),
Distributor(value = "aDistributorValue1", name = "aDistributorName1"),
)
initialState.eventSink.invoke(NotificationSettingsEvents.ChangePushProvider) initialState.eventSink.invoke(NotificationSettingsEvents.ChangePushProvider)
val withDialog = awaitItem() val withDialog = awaitItem()
assertThat(withDialog.showChangePushProviderDialog).isTrue() assertThat(withDialog.showChangePushProviderDialog).isTrue()
@ -257,11 +260,35 @@ class NotificationSettingsPresenterTest {
assertThat(withNewProvider.currentPushDistributor).isInstanceOf(AsyncData.Loading::class.java) assertThat(withNewProvider.currentPushDistributor).isInstanceOf(AsyncData.Loading::class.java)
skipItems(1) skipItems(1)
val lastItem = awaitItem() val lastItem = awaitItem()
assertThat(lastItem.currentPushDistributor).isEqualTo(AsyncData.Success("aDistributorName1")) assertThat(lastItem.currentPushDistributor).isEqualTo(AsyncData.Success(Distributor(value = "aDistributorValue1", name = "aDistributorName1")))
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
} }
@Test
fun `present - change push provider to the same value is no op`() = runTest {
val presenter = createNotificationSettingsPresenter(
pushService = createFakePushService(),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitLastSequentialItem()
assertThat(initialState.currentPushDistributor).isEqualTo(AsyncData.Success(Distributor(value = "aDistributorValue0", name = "aDistributorName0")))
assertThat(initialState.availablePushDistributors).containsExactly(
Distributor(value = "aDistributorValue0", name = "aDistributorName0"),
Distributor(value = "aDistributorValue1", name = "aDistributorName1"),
)
initialState.eventSink.invoke(NotificationSettingsEvents.ChangePushProvider)
assertThat(awaitItem().showChangePushProviderDialog).isTrue()
// Choose the same value (index 0)
initialState.eventSink(NotificationSettingsEvents.SetPushProvider(0))
val withNewProvider = awaitItem()
assertThat(withNewProvider.showChangePushProviderDialog).isFalse()
expectNoEvents()
}
}
@Test @Test
fun `present - RefreshSystemNotificationsEnabled also refreshes fullScreenIntentState`() = runTest { fun `present - RefreshSystemNotificationsEnabled also refreshes fullScreenIntentState`() = runTest {
var lambdaResult = aFullScreenIntentPermissionsState(permissionGranted = false) var lambdaResult = aFullScreenIntentPermissionsState(permissionGranted = false)

View file

@ -267,7 +267,7 @@ class NotificationSettingsViewTest {
state = aValidNotificationSettingsState( state = aValidNotificationSettingsState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
showChangePushProviderDialog = true, showChangePushProviderDialog = true,
availablePushDistributors = listOf("P1", "P2") availablePushDistributors = listOf(aDistributor("P1"), aDistributor("P2"))
), ),
) )
rule.onNodeWithText("P2").performClick() rule.onNodeWithText("P2").performClick()

View file

@ -18,4 +18,6 @@ package io.element.android.libraries.pushproviders.api
data class Distributor( data class Distributor(
val value: String, val value: String,
val name: String, val name: String,
) ) {
val fullName = "$name ($value)"
}