Merge pull request #2596 from element-hq/feature/bma/troubleshootNotification

Troubleshoot notifications screen
This commit is contained in:
Benoit Marty 2024-04-02 20:50:52 +02:00 committed by GitHub
commit f4076dcb7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
235 changed files with 4554 additions and 708 deletions

View file

@ -65,6 +65,7 @@ dependencies {
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.features.login.impl)
testImplementation(projects.tests.testutils)

View file

@ -22,13 +22,10 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider
import io.element.android.libraries.push.test.FakePushService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import kotlinx.coroutines.test.runTest
@ -73,20 +70,7 @@ class LoggedInPresenterTest {
return LoggedInPresenter(
matrixClient = FakeMatrixClient(roomListService = roomListService),
networkMonitor = FakeNetworkMonitor(networkStatus),
pushService = object : PushService {
override fun notificationStyleChanged() {
}
override fun getAvailablePushProviders(): List<PushProvider> {
return emptyList()
}
override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) {
}
override suspend fun testPush() {
}
}
pushService = FakePushService(),
)
}
}

1
changelog.d/2601.feature Normal file
View file

@ -0,0 +1 @@
Add a notification troubleshoot screen

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invites_decline_chat_message">"Вы ўпэўненыя, што жадаеце адхіліць запрашэнне ў %1$s?"</string>
<string name="screen_invites_decline_chat_message">"Вы ўпэўненыя, што хочаце адхіліць запрашэнне ў %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Адхіліць запрашэнне"</string>
<string name="screen_invites_decline_direct_chat_message">"Вы ўпэўненыя, што жадаеце адмовіцца ад прыватных зносін з %1$s?"</string>
<string name="screen_invites_decline_direct_chat_message">"Вы ўпэўненыя, што хочаце адмовіцца ад прыватных зносін з %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Адхіліць чат"</string>
<string name="screen_invites_empty_list">"Няма запрашэнняў"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) запрасіў вас"</string>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"Вы ўпэўнены, што хочаце пакінуць гэту размову? Гэта размова не з\'яўляецца публічнай, і вы не зможаце далучыцца зноў без запрашэння."</string>
<string name="leave_room_alert_empty_subtitle">"Вы ўпэўнены, што жадаеце пакінуць гэты пакой? Вы тут адзіны карыстальнік. Калі вы выйдзеце, ніхто не зможа далучыцца ў будучыні, у тым ліку і вы."</string>
<string name="leave_room_alert_private_subtitle">"Вы ўпэўнены, што жадаеце пакінуць гэты пакой? Гэты пакой не агульнадаступны, і вы не зможаце далучыцца да яго зноў без запрашэння."</string>
<string name="leave_room_alert_empty_subtitle">"Вы ўпэўнены, што хочаце пакінуць гэты пакой? Вы тут адзіны карыстальнік. Калі вы выйдзеце, ніхто не зможа далучыцца ў будучыні, у тым ліку і вы."</string>
<string name="leave_room_alert_private_subtitle">"Вы ўпэўнены, што жхочаце пакінуць гэты пакой? Гэты пакой не агульнадаступны, і вы не зможаце далучыцца да яго зноў без запрашэння."</string>
<string name="leave_room_alert_subtitle">"Вы ўпэўнены, што хочаце пакінуць пакой?"</string>
</resources>

View file

@ -7,7 +7,7 @@
<string name="screen_app_lock_settings_change_pin">"Змяніць PIN-код"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Дазволіць біяметрычную разблакіроўку"</string>
<string name="screen_app_lock_settings_remove_pin">"Выдаліць PIN-код"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Вы ўпэўнены, што жадаеце выдаліць PIN-код?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Вы ўпэўнены, што хочаце выдаліць PIN-код?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Выдаліць PIN-код?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Дазволіць %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Я хацеў бы выкарыстоўваць PIN-код"</string>

View file

@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_settings_change_pin">"Byt PIN-kod"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Tillåt biometrisk upplåsning"</string>
<string name="screen_app_lock_settings_remove_pin">"Ta bort PIN-kod"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Ta bort PIN-koden?"</string>
<string name="screen_signout_in_progress_dialog_content">"Loggar ut …"</string>
</resources>

View file

@ -9,7 +9,7 @@
<string name="emoji_picker_category_places">"Падарожжы &amp; Месцы"</string>
<string name="emoji_picker_category_symbols">"Сімвалы"</string>
<string name="screen_report_content_block_user">"Заблакіраваць карыстальніка"</string>
<string name="screen_report_content_block_user_hint">"Адзначце, ці жадаеце вы схаваць усе бягучыя і будучыя паведамленні ад гэтага карыстальніка"</string>
<string name="screen_report_content_block_user_hint">"Адзначце, ці хочаце вы схаваць усе бягучыя і будучыя паведамленні ад гэтага карыстальніка"</string>
<string name="screen_report_content_explanation">"Гэтае паведамленне будзе перададзена адміністратару вашага хатняга сервера. Яны не змогуць прачытаць зашыфраваныя паведамленні."</string>
<string name="screen_report_content_hint">"Прычына, па якой вы паскардзіліся на гэты змест"</string>
<string name="screen_room_attachment_source_camera">"Камера"</string>

View file

@ -4,7 +4,7 @@
<string name="screen_create_poll_anonymous_desc">"Паказаць вынікі толькі пасля заканчэння апытання"</string>
<string name="screen_create_poll_anonymous_headline">"Схаваць галасы"</string>
<string name="screen_create_poll_answer_hint">"Варыянт %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Вашы змены не былі захаваны. Вы ўпэўнены, што жадаеце вярнуцца?"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Вашы змены не былі захаваны. Вы ўпэўнены, што хочаце вярнуцца?"</string>
<string name="screen_create_poll_question_desc">"Пытанне або тэма"</string>
<string name="screen_create_poll_question_hint">"Пра што апытанне?"</string>
<string name="screen_create_poll_title">"Стварэнне апытання"</string>

View file

@ -23,6 +23,11 @@ plugins {
android {
namespace = "io.element.android.features.preferences.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
anvil {
@ -44,12 +49,14 @@ dependencies {
implementation(projects.libraries.pushstore.api)
implementation(projects.libraries.indicator.api)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.troubleshoot.api)
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.mediapickers.api)
implementation(projects.libraries.mediaupload.api)
implementation(projects.libraries.permissions.api)
implementation(projects.libraries.push.api)
implementation(projects.features.rageshake.api)
implementation(projects.features.lockscreen.api)
implementation(projects.features.analytics.api)
@ -71,12 +78,14 @@ dependencies {
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(libs.test.mockk)
testImplementation(libs.test.robolectric)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.mediapickers.test)
testImplementation(projects.libraries.mediaupload.test)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushstore.test)
testImplementation(projects.features.rageshake.test)
testImplementation(projects.features.rageshake.impl)
@ -86,4 +95,6 @@ dependencies {
testImplementation(projects.services.toolbox.test)
testImplementation(projects.features.analytics.impl)
testImplementation(projects.tests.testutils)
testImplementation(libs.androidx.compose.ui.test.junit)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
}

View file

@ -24,6 +24,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@ -47,6 +48,7 @@ import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.troubleshoot.api.NotificationTroubleShootEntryPoint
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@ -54,6 +56,7 @@ class PreferencesFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val lockScreenEntryPoint: LockScreenEntryPoint,
private val notificationTroubleShootEntryPoint: NotificationTroubleShootEntryPoint,
private val logoutEntryPoint: LogoutEntryPoint,
) : BaseFlowNode<PreferencesFlowNode.NavTarget>(
backstack = BackStack(
@ -85,6 +88,9 @@ class PreferencesFlowNode @AssistedInject constructor(
@Parcelize
data object NotificationSettings : NavTarget
@Parcelize
data object TroubleshootNotifications : NavTarget
@Parcelize
data object LockScreenSettings : NavTarget
@ -177,9 +183,22 @@ class PreferencesFlowNode @AssistedInject constructor(
override fun editDefaultNotificationMode(isOneToOne: Boolean) {
backstack.push(NavTarget.EditDefaultNotificationSetting(isOneToOne))
}
override fun onTroubleshootNotificationsClicked() {
backstack.push(NavTarget.TroubleshootNotifications)
}
}
createNode<NotificationSettingsNode>(buildContext, listOf(notificationSettingsCallback))
}
NavTarget.TroubleshootNotifications -> {
notificationTroubleShootEntryPoint.nodeBuilder(this, buildContext)
.callback(object : NotificationTroubleShootEntryPoint.Callback {
override fun onDone() {
backstack.pop()
}
})
.build()
}
is NavTarget.EditDefaultNotificationSetting -> {
val callback = object : EditDefaultNotificationSettingNode.Callback {
override fun openRoomNotificationSettings(roomId: RoomId) {

View file

@ -35,6 +35,7 @@ class NotificationSettingsNode @AssistedInject constructor(
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun editDefaultNotificationMode(isOneToOne: Boolean)
fun onTroubleshootNotificationsClicked()
}
private val callbacks = plugins<Callback>()
@ -43,6 +44,10 @@ class NotificationSettingsNode @AssistedInject constructor(
callbacks.forEach { it.editDefaultNotificationMode(isOneToOne) }
}
private fun onTroubleshootNotificationsClicked() {
callbacks.forEach { it.onTroubleshootNotificationsClicked() }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
@ -50,6 +55,7 @@ class NotificationSettingsNode @AssistedInject constructor(
state = state,
onOpenEditDefault = { openEditDefault(isOneToOne = it) },
onBackPressed = ::navigateUp,
onTroubleshootNotificationsClicked = ::onTroubleshootNotificationsClicked,
modifier = modifier,
)
}

View file

@ -48,7 +48,7 @@ class NotificationSettingsPresenter @Inject constructor(
) : Presenter<NotificationSettingsState> {
@Composable
override fun present(): NotificationSettingsState {
val userPushStore = remember { userPushStoreFactory.create(matrixClient.sessionId) }
val userPushStore = remember { userPushStoreFactory.getOrCreate(matrixClient.sessionId) }
val systemNotificationsEnabled: MutableState<Boolean> = remember {
mutableStateOf(systemNotificationsEnabledProvider.notificationsEnabled())
}

View file

@ -23,26 +23,48 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode
open class NotificationSettingsStateProvider : PreviewParameterProvider<NotificationSettingsState> {
override val values: Sequence<NotificationSettingsState>
get() = sequenceOf(
aNotificationSettingsState(),
aNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Loading),
aNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Failure(Throwable("error"))),
aValidNotificationSettingsState(),
aValidNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Loading),
aValidNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Failure(Throwable("error"))),
aInvalidNotificationSettingsState(),
aInvalidNotificationSettingsState(fixFailed = true),
)
}
fun aNotificationSettingsState(
fun aValidNotificationSettingsState(
changeNotificationSettingAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
atRoomNotificationsEnabled: Boolean = true,
callNotificationsEnabled: Boolean = true,
inviteForMeNotificationsEnabled: Boolean = true,
appNotificationEnabled: Boolean = true,
eventSink: (NotificationSettingsEvents) -> Unit = {},
) = NotificationSettingsState(
matrixSettings = NotificationSettingsState.MatrixSettings.Valid(
atRoomNotificationsEnabled = true,
callNotificationsEnabled = true,
inviteForMeNotificationsEnabled = true,
atRoomNotificationsEnabled = atRoomNotificationsEnabled,
callNotificationsEnabled = callNotificationsEnabled,
inviteForMeNotificationsEnabled = inviteForMeNotificationsEnabled,
defaultGroupNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
defaultOneToOneNotificationMode = RoomNotificationMode.ALL_MESSAGES,
),
appSettings = NotificationSettingsState.AppSettings(
systemNotificationsEnabled = false,
appNotificationsEnabled = true,
appNotificationsEnabled = appNotificationEnabled,
),
changeNotificationSettingAction = changeNotificationSettingAction,
eventSink = {}
eventSink = eventSink,
)
fun aInvalidNotificationSettingsState(
fixFailed: Boolean = false,
eventSink: (NotificationSettingsEvents) -> Unit = {},
) = NotificationSettingsState(
matrixSettings = NotificationSettingsState.MatrixSettings.Invalid(
fixFailed = fixFailed,
),
appSettings = NotificationSettingsState.AppSettings(
systemNotificationsEnabled = false,
appNotificationsEnabled = true,
),
changeNotificationSettingAction = AsyncAction.Uninitialized,
eventSink = eventSink,
)

View file

@ -46,6 +46,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun NotificationSettingsView(
state: NotificationSettingsState,
onOpenEditDefault: (isOneToOne: Boolean) -> Unit,
onTroubleshootNotificationsClicked: () -> Unit,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
@ -77,6 +78,7 @@ fun NotificationSettingsView(
// TODO We are removing the call notification toggle until support for call notifications has been added
// onCallsNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetCallNotificationsEnabled(it)) },
onInviteForMeNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetInviteForMeNotificationsEnabled(it)) },
onTroubleshootNotificationsClicked = onTroubleshootNotificationsClicked,
)
}
AsyncActionView(
@ -99,6 +101,7 @@ private fun NotificationSettingsContentView(
// TODO We are removing the call notification toggle until support for call notifications has been added
// onCallsNotificationsChanged: (Boolean) -> Unit,
onInviteForMeNotificationsChanged: (Boolean) -> Unit,
onTroubleshootNotificationsClicked: () -> Unit,
) {
val context = LocalContext.current
if (systemSettings.appNotificationsEnabled && !systemSettings.systemNotificationsEnabled) {
@ -163,6 +166,13 @@ private fun NotificationSettingsContentView(
onCheckedChange = onInviteForMeNotificationsChanged
)
}
PreferenceCategory(title = stringResource(id = R.string.troubleshoot_notifications_entry_point_section)) {
PreferenceText(
modifier = Modifier,
title = stringResource(id = R.string.troubleshoot_notifications_entry_point_title),
onClick = onTroubleshootNotificationsClicked
)
}
}
}
@ -204,15 +214,6 @@ internal fun NotificationSettingsViewPreview(@PreviewParameter(NotificationSetti
state = state,
onBackPressed = {},
onOpenEditDefault = {},
)
}
@PreviewsDayNight
@Composable
internal fun InvalidNotificationSettingsViewPreview() = ElementPreview {
InvalidNotificationSettingsView(
showError = false,
onContinueClicked = {},
onDismissError = {},
onTroubleshootNotificationsClicked = {},
)
}

View file

@ -49,4 +49,6 @@
<string name="screen_notification_settings_system_notifications_action_required_content_link">"налады сістэмы"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Сістэмныя апавяшчэнні выключаны"</string>
<string name="screen_notification_settings_title">"Апавяшчэнні"</string>
<string name="troubleshoot_notifications_entry_point_section">"Выпраўленне непаладак"</string>
<string name="troubleshoot_notifications_entry_point_title">"Выпраўленне непаладак з апавяшчэннямі"</string>
</resources>

View file

@ -51,4 +51,6 @@ Pokud budete pokračovat, některá nastavení se mohou změnit."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"systémová nastavení"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Systémová oznámení byla vypnuta"</string>
<string name="screen_notification_settings_title">"Oznámení"</string>
<string name="troubleshoot_notifications_entry_point_section">"Odstraňování problémů"</string>
<string name="troubleshoot_notifications_entry_point_title">"Odstraňování problémů s upozorněními"</string>
</resources>

View file

@ -49,4 +49,6 @@ Wenn du fortfährst, können sich einige deiner Einstellungen ändern."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"Systemeinstellungen"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Systembenachrichtigungen deaktiviert"</string>
<string name="screen_notification_settings_title">"Benachrichtigungen"</string>
<string name="troubleshoot_notifications_entry_point_section">"Fehlerbehebung"</string>
<string name="troubleshoot_notifications_entry_point_title">"Fehlerbehebung für Benachrichtigungen"</string>
</resources>

View file

@ -49,4 +49,6 @@ Si vous continuez, il est possible que certains de vos paramètres soient modifi
<string name="screen_notification_settings_system_notifications_action_required_content_link">"paramètres du système"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Les notifications du système sont désactivées"</string>
<string name="screen_notification_settings_title">"Notifications"</string>
<string name="troubleshoot_notifications_entry_point_section">"Dépannage"</string>
<string name="troubleshoot_notifications_entry_point_title">"Résoudre les problèmes liés aux notifications"</string>
</resources>

View file

@ -49,4 +49,6 @@ Ha folytatja, egyes beállítások megváltozhatnak."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"rendszerbeállításokat"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"A rendszerértesítések ki vannak kapcsolva"</string>
<string name="screen_notification_settings_title">"Értesítések"</string>
<string name="troubleshoot_notifications_entry_point_section">"Hibaelhárítás"</string>
<string name="troubleshoot_notifications_entry_point_title">"Értesítések hibaelhárítása"</string>
</resources>

View file

@ -51,4 +51,6 @@ Jika Anda melanjutkan, beberapa pengaturan Anda dapat berubah."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"pengaturan sistem"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Pemberitahuan sistem dimatikan"</string>
<string name="screen_notification_settings_title">"Notifikasi"</string>
<string name="troubleshoot_notifications_entry_point_section">"Pemecahan masalah"</string>
<string name="troubleshoot_notifications_entry_point_title">"Pecahkan masalah notifikasi"</string>
</resources>

View file

@ -49,4 +49,6 @@
<string name="screen_notification_settings_system_notifications_action_required_content_link">"настройки системы"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Системные уведомления выключены"</string>
<string name="screen_notification_settings_title">"Уведомления"</string>
<string name="troubleshoot_notifications_entry_point_section">"Устранение неполадок"</string>
<string name="troubleshoot_notifications_entry_point_title">"Уведомления об устранении неполадок"</string>
</resources>

View file

@ -51,4 +51,6 @@ Ak budete pokračovať, niektoré z vašich nastavení sa môžu zmeniť."</stri
<string name="screen_notification_settings_system_notifications_action_required_content_link">"nastavenia systému"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Systémové oznámenia sú vypnuté"</string>
<string name="screen_notification_settings_title">"Oznámenia"</string>
<string name="troubleshoot_notifications_entry_point_section">"Riešenie problémov"</string>
<string name="troubleshoot_notifications_entry_point_title">"Oznámenia riešení problémov"</string>
</resources>

View file

@ -49,4 +49,6 @@ If you proceed, some of your settings may change."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"system settings"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"System notifications turned off"</string>
<string name="screen_notification_settings_title">"Notifications"</string>
<string name="troubleshoot_notifications_entry_point_section">"Troubleshoot"</string>
<string name="troubleshoot_notifications_entry_point_title">"Troubleshoot notifications"</string>
</resources>

View file

@ -0,0 +1,267 @@
/*
* Copyright (c) 2024 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 io.element.android.features.preferences.impl.notifications
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.preferences.impl.R
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class)
class NotificationSettingsViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on back invokes the expected callback`() {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
ensureCalledOnce {
rule.setNotificationSettingsView(
state = aValidNotificationSettingsState(
eventSink = eventsRecorder
),
onBackPressed = it
)
rule.pressBack()
}
eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled)
}
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on troubleshoot notification invokes the expected callback`() {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
ensureCalledOnce {
rule.setNotificationSettingsView(
state = aValidNotificationSettingsState(
eventSink = eventsRecorder
),
onTroubleshootNotificationsClicked = it
)
rule.clickOn(R.string.troubleshoot_notifications_entry_point_title)
}
eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled)
}
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on group chats invokes the expected callback`() {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
ensureCalledOnceWithParam(false) {
rule.setNotificationSettingsView(
state = aValidNotificationSettingsState(
eventSink = eventsRecorder
),
onOpenEditDefault = it
)
rule.clickOn(R.string.screen_notification_settings_group_chats)
}
eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled)
}
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on direct chats invokes the expected callback`() {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
ensureCalledOnceWithParam(true) {
rule.setNotificationSettingsView(
state = aValidNotificationSettingsState(
eventSink = eventsRecorder
),
onOpenEditDefault = it
)
rule.clickOn(R.string.screen_notification_settings_direct_chats)
}
eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled)
}
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on disable notifications emits the expected events`() {
testNotificationToggle(true)
}
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on enable notifications emits the expected events`() {
testNotificationToggle(false)
}
private fun testNotificationToggle(initialState: Boolean) {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
rule.setNotificationSettingsView(
state = aValidNotificationSettingsState(
appNotificationEnabled = initialState,
eventSink = eventsRecorder
),
)
rule.clickOn(R.string.screen_notification_settings_enable_notifications)
eventsRecorder.assertList(
listOf(
NotificationSettingsEvents.RefreshSystemNotificationsEnabled,
NotificationSettingsEvents.SetNotificationsEnabled(!initialState)
)
)
}
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on disable notify me on at room emits the expected events`() {
testAtRoomToggle(true)
}
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on enable notify me on at room emits the expected events`() {
testAtRoomToggle(false)
}
private fun testAtRoomToggle(initialState: Boolean) {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
rule.setNotificationSettingsView(
state = aValidNotificationSettingsState(
atRoomNotificationsEnabled = initialState,
eventSink = eventsRecorder
),
)
rule.clickOn(R.string.screen_notification_settings_room_mention_label)
eventsRecorder.assertList(
listOf(
NotificationSettingsEvents.RefreshSystemNotificationsEnabled,
NotificationSettingsEvents.SetAtRoomNotificationsEnabled(!initialState)
)
)
}
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on disable notify me on invitation emits the expected events`() {
testInvitationToggle(true)
}
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on enable notify me on invitation emits the expected events`() {
testInvitationToggle(false)
}
private fun testInvitationToggle(initialState: Boolean) {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
rule.setNotificationSettingsView(
state = aValidNotificationSettingsState(
inviteForMeNotificationsEnabled = initialState,
eventSink = eventsRecorder
),
)
rule.clickOn(R.string.screen_notification_settings_invite_for_me_label)
eventsRecorder.assertList(
listOf(
NotificationSettingsEvents.RefreshSystemNotificationsEnabled,
NotificationSettingsEvents.SetInviteForMeNotificationsEnabled(!initialState)
)
)
}
@Config(qualifiers = "h1024dp")
@Test
fun `with an error configuration, clicking on continue emits the expected events`() {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
rule.setNotificationSettingsView(
state = aValidNotificationSettingsState(
changeNotificationSettingAction = AsyncAction.Failure(AN_EXCEPTION),
eventSink = eventsRecorder
),
)
rule.clickOn(CommonStrings.action_ok)
eventsRecorder.assertList(
listOf(
NotificationSettingsEvents.RefreshSystemNotificationsEnabled,
NotificationSettingsEvents.ClearNotificationChangeError
)
)
}
@Config(qualifiers = "h1024dp")
@Test
fun `with invalid configuration, clicking on continue emits the expected events`() {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
rule.setNotificationSettingsView(
state = aInvalidNotificationSettingsState(
fixFailed = false,
eventSink = eventsRecorder
),
)
rule.clickOn(CommonStrings.action_continue)
eventsRecorder.assertList(
listOf(
NotificationSettingsEvents.RefreshSystemNotificationsEnabled,
NotificationSettingsEvents.FixConfigurationMismatch
)
)
}
@Config(qualifiers = "h1024dp")
@Test
fun `with invalid configuration and error, clicking on OK emits the expected events`() {
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
rule.setNotificationSettingsView(
state = aInvalidNotificationSettingsState(
fixFailed = true,
eventSink = eventsRecorder
),
)
rule.clickOn(CommonStrings.action_ok)
eventsRecorder.assertList(
listOf(
NotificationSettingsEvents.RefreshSystemNotificationsEnabled,
NotificationSettingsEvents.ClearConfigurationMismatchError
)
)
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setNotificationSettingsView(
state: NotificationSettingsState,
onOpenEditDefault: (isOneToOne: Boolean) -> Unit = EnsureNeverCalledWithParam(),
onTroubleshootNotificationsClicked: () -> Unit = EnsureNeverCalled(),
onBackPressed: () -> Unit = EnsureNeverCalled(),
) {
setContent {
NotificationSettingsView(
state = state,
onOpenEditDefault = onOpenEditDefault,
onTroubleshootNotificationsClicked = onTroubleshootNotificationsClicked,
onBackPressed = onBackPressed,
)
}
}

View file

@ -23,25 +23,31 @@ import kotlinx.coroutines.test.runTest
import org.junit.Test
class VersionFormatterTest {
@Test
fun `version formatter should return simplified version for other branch`() = runTest {
val sut = DefaultVersionFormatter(
stringProvider = FakeStringProvider(defaultResult = VERSION),
buildMeta = aBuildMeta(gitBranchName = "main")
)
assertThat(sut.get()).isEqualTo(VERSION)
}
@Test
fun `version formatter should return simplified version for main branch`() = runTest {
val sut = DefaultVersionFormatter(
stringProvider = FakeStringProvider(defaultResult = VERSION),
buildMeta = aBuildMeta(
gitBranchName = "main",
versionName = "versionName",
versionCode = 123
)
)
assertThat(sut.get()).isEqualTo("${VERSION}versionName, 123")
}
@Test
fun `version formatter should return simplified version for other branch`() = runTest {
val sut = DefaultVersionFormatter(
stringProvider = FakeStringProvider(defaultResult = VERSION),
buildMeta = aBuildMeta(
versionName = "versionName",
versionCode = 123,
gitBranchName = "branch",
gitRevision = "1234567890",
)
)
assertThat(sut.get()).isEqualTo("$VERSION\nbranch (1234567890)")
assertThat(sut.get()).isEqualTo("${VERSION}versionName, 123\nbranch (1234567890)")
}
companion object {

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="crash_detection_dialog_content">"Пры апошнім выкарыстанні %1$s адбыўся збой. Жадаеце падзяліцца справаздачай аб збоі?"</string>
<string name="rageshake_detection_dialog_content">"Падобна, што вы трасеце тэлефон. Жадаеце адкрыць экран паведамлення пра памылку?"</string>
<string name="crash_detection_dialog_content">"Пры апошнім выкарыстанні %1$s адбыўся збой. Хочаце падзяліцца справаздачай аб збоі?"</string>
<string name="rageshake_detection_dialog_content">"Падобна, што вы трасеце тэлефон. Хочаце адкрыць экран паведамлення пра памылку?"</string>
<string name="settings_rageshake">"Rageshake"</string>
<string name="settings_rageshake_detection_threshold">"Парог выяўлення"</string>
</resources>

View file

@ -12,6 +12,6 @@
<string name="screen_bug_report_include_logs">"Дазволіць журналы"</string>
<string name="screen_bug_report_include_screenshot">"Адправіць здымак экрана"</string>
<string name="screen_bug_report_logs_description">"Каб пераканацца, што ўсё працуе правільна, у паведамленне будуць уключаны часопісы. Каб адправіць паведамленне без часопісаў, адключыце гэтую наладу."</string>
<string name="screen_bug_report_rash_logs_alert_title">"Пры апошнім выкарыстанні %1$s адбыўся збой. Жадаеце падзяліцца справаздачай аб збоі?"</string>
<string name="screen_bug_report_rash_logs_alert_title">"Пры апошнім выкарыстанні %1$s адбыўся збой. Хочаце падзяліцца справаздачай аб збоі?"</string>
<string name="screen_bug_report_view_logs">"Прагляд журналаў"</string>
</resources>

View file

@ -30,6 +30,7 @@
<string name="screen_room_change_role_confirm_demote_self_description">"Вы не зможаце адмяніць гэтае змяненне, бо паніжаеце сябе. Калі вы апошні адміністратар у пакоі, вярнуць права будзе немагчыма."</string>
<string name="screen_room_change_role_confirm_demote_self_title">"Панізіць сябе?"</string>
<string name="screen_room_change_role_invited_member_name">"%1$s (У чаканні)"</string>
<string name="screen_room_change_role_invited_member_name_android">"(У чаканні)"</string>
<string name="screen_room_change_role_moderators_title">"Рэдагаваць мадэратараў"</string>
<string name="screen_room_change_role_section_administrators">"Адміністратары"</string>
<string name="screen_room_change_role_section_moderators">"Мадэратары"</string>

View file

@ -30,6 +30,7 @@
<string name="screen_room_change_role_confirm_demote_self_description">"Vous ne pourrez pas annuler ce changement car vous vous rétrogradez, si vous êtes le dernier utilisateur privilégié du salon il sera impossible de retrouver les privilèges."</string>
<string name="screen_room_change_role_confirm_demote_self_title">"Vous rétrograder ?"</string>
<string name="screen_room_change_role_invited_member_name">"%1$s (En attente)"</string>
<string name="screen_room_change_role_invited_member_name_android">"(En attente)"</string>
<string name="screen_room_change_role_moderators_title">"Modifier les modérateurs"</string>
<string name="screen_room_change_role_section_administrators">"Administrateurs"</string>
<string name="screen_room_change_role_section_moderators">"Modérateurs"</string>

View file

@ -30,6 +30,7 @@
<string name="screen_room_change_role_confirm_demote_self_description">"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges."</string>
<string name="screen_room_change_role_confirm_demote_self_title">"Demote yourself?"</string>
<string name="screen_room_change_role_invited_member_name">"%1$s (Pending)"</string>
<string name="screen_room_change_role_invited_member_name_android">"(Pending)"</string>
<string name="screen_room_change_role_moderators_title">"Edit Moderators"</string>
<string name="screen_room_change_role_section_administrators">"Admins"</string>
<string name="screen_room_change_role_section_moderators">"Moderators"</string>

View file

@ -15,7 +15,7 @@
<string name="screen_key_backup_disable_description">"Адключэнне рэзервовага капіравання прывядзе да выдалення бягучай рэзервовай копіі ключа шыфравання і адключэння іншых функцый бяспекі. У гэтым выпадку вы:"</string>
<string name="screen_key_backup_disable_description_point_1">"Няма зашыфраванай гісторыі паведамленняў на новых прыладах"</string>
<string name="screen_key_backup_disable_description_point_2">"Калі вы выходзіце з сістэмы, то губляеце доступ да зашыфраваных паведамленняў %1$s усюды"</string>
<string name="screen_key_backup_disable_title">"Вы ўпэўнены, што жадаеце адключыць рэзервовае капіраванне?"</string>
<string name="screen_key_backup_disable_title">"Вы ўпэўнены, што хочаце адключыць рэзервовае капіраванне?"</string>
<string name="screen_recovery_key_change_description">"Атрымайце новы ключ аднаўлення, калі вы страцілі існуючы. Пасля змены ключа аднаўлення ваш стары больш не будзе працаваць."</string>
<string name="screen_recovery_key_change_generate_key">"Стварыць новы ключ аднаўлення"</string>
<string name="screen_recovery_key_change_generate_key_description">"Пераканайцеся, што вы можаце захаваць ключ аднаўлення ў бяспечным месцы"</string>

View file

@ -177,7 +177,9 @@ kotlinpoet = "com.squareup:kotlinpoet:1.16.0"
# Analytics
posthog = "com.posthog:posthog-android:3.1.16"
sentry = "io.sentry:sentry-android:7.6.0"
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.14.0"
# Note: only 0.19.0 will compile properly
# main branch can be tested replacing the version with main-SNAPSHOT
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.15.0"
# Emojibase
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.1.3"

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.androidutils.system
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
@ -73,6 +74,9 @@ fun Context.startNotificationSettingsIntent(activityResultLauncher: ActivityResu
val intent = Intent()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
if (this !is Activity && activityResultLauncher == null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
} else {
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
@ -154,6 +158,9 @@ fun Context.openUrlInExternalApp(
errorMessage: String = getString(R.string.error_no_compatible_app_found),
) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
if (this !is Activity) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
try {
startActivity(intent)
} catch (activityNotFoundException: ActivityNotFoundException) {

View file

@ -24,7 +24,6 @@ android {
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)

View file

@ -46,8 +46,10 @@ dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.troubleshoot.api)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
implementation(projects.services.toolbox.api)
api(projects.libraries.permissions.api)
testImplementation(libs.test.junit)
@ -57,6 +59,7 @@ dependencies {
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.services.toolbox.test)
testImplementation(projects.tests.testutils)
ksp(libs.showkase.processor)

View file

@ -17,6 +17,8 @@
package io.element.android.libraries.permissions.impl
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
@ -33,7 +35,10 @@ class DefaultPermissionStateProvider @Inject constructor(
private val permissionsStore: PermissionsStore,
) : PermissionStateProvider {
override fun isPermissionGranted(permission: String): Boolean {
return context.checkSelfPermission(permission) == android.content.pm.PackageManager.PERMISSION_GRANTED
return ContextCompat.checkSelfPermission(
context,
permission,
) == PackageManager.PERMISSION_GRANTED
}
override suspend fun setPermissionDenied(permission: String, value: Boolean) = permissionsStore.setPermissionDenied(permission, value)

View file

@ -18,7 +18,7 @@ package io.element.android.libraries.permissions.impl.action
import android.content.Context
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.androidutils.system.openAppSettingsPage
import io.element.android.libraries.androidutils.system.startNotificationSettingsIntent
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import javax.inject.Inject
@ -28,6 +28,6 @@ class AndroidPermissionActions @Inject constructor(
@ApplicationContext private val context: Context
) : PermissionActions {
override fun openSettings() {
context.openAppSettingsPage()
context.startNotificationSettingsIntent()
}
}

View file

@ -0,0 +1,69 @@
/*
* Copyright (c) 2024 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 io.element.android.libraries.permissions.impl.troubleshoot
import android.Manifest
import android.os.Build
import com.squareup.anvil.annotations.ContributesMultibinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.permissions.api.PermissionStateProvider
import io.element.android.libraries.permissions.impl.R
import io.element.android.libraries.permissions.impl.action.PermissionActions
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
import io.element.android.services.toolbox.api.strings.StringProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@ContributesMultibinding(AppScope::class)
class NotificationTroubleshootCheckPermissionTest @Inject constructor(
private val permissionStateProvider: PermissionStateProvider,
private val sdkVersionProvider: BuildVersionSdkIntProvider,
private val permissionActions: PermissionActions,
private val stringProvider: StringProvider,
) : NotificationTroubleshootTest {
override val order: Int = 0
private val delegate = NotificationTroubleshootTestDelegate(
defaultName = stringProvider.getString(R.string.troubleshoot_notifications_test_check_permission_title),
defaultDescription = stringProvider.getString(R.string.troubleshoot_notifications_test_check_permission_description),
hasQuickFix = true,
fakeDelay = NotificationTroubleshootTestDelegate.SHORT_DELAY,
)
override val state: StateFlow<NotificationTroubleshootTestState> = delegate.state
override suspend fun run(coroutineScope: CoroutineScope) {
delegate.start()
val result = if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
permissionStateProvider.isPermissionGranted(Manifest.permission.POST_NOTIFICATIONS)
} else {
true
}
delegate.done(result)
}
override suspend fun reset() = delegate.reset()
override suspend fun quickFix(coroutineScope: CoroutineScope) {
// Do not bother about asking the permission inline, just lead the user to the settings
permissionActions.openSettings()
}
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Пераканайцеся, што праграма можа паказваць апавяшчэнні."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Праверце дазволы"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Ujistěte se, že aplikace může zobrazovat oznámení."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Kontrola oprávnění"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Stelle sicher, dass die Anwendung Benachrichtigungen anzeigen kann."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Berechtigungen überprüfen"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Vérifie que lapplication peut afficher des notifications."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Vérifier les autorisations"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Ellenőrizze, hogy az alkalmazás képes-e értesítéseket megjeleníteni."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Engedélyek ellenőrzése"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Убедитесь, что приложение может показывать уведомления."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Проверка разрешений"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Uistite sa, že aplikácia dokáže zobrazovať upozornenia."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Skontrolovať povolenia"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="troubleshoot_notifications_test_check_permission_description">"Check that the application can show notifications."</string>
<string name="troubleshoot_notifications_test_check_permission_title">"Check permissions"</string>
</resources>

View file

@ -16,11 +16,14 @@
package io.element.android.libraries.permissions.impl.action
class FakePermissionActions : PermissionActions {
class FakePermissionActions(
val openSettingsAction: () -> Unit = {}
) : PermissionActions {
var openSettingsCalled = false
private set
override fun openSettings() {
openSettingsAction()
openSettingsCalled = true
}
}

View file

@ -0,0 +1,104 @@
/*
* Copyright (c) 2024 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 io.element.android.libraries.permissions.impl.troubleshoot
import android.os.Build
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
import io.element.android.libraries.permissions.impl.action.FakePermissionActions
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Test
class NotificationTroubleshootCheckPermissionTestTest {
@Test
fun `test NotificationTroubleshootCheckPermissionTest below TIRAMISU success`() = runTest {
val sut = NotificationTroubleshootCheckPermissionTest(
permissionStateProvider = FakePermissionStateProvider(),
sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkInt = Build.VERSION_CODES.TIRAMISU - 1),
permissionActions = FakePermissionActions(),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
}
}
@Test
fun `test NotificationTroubleshootCheckPermissionTest TIRAMISU success`() = runTest {
val sut = NotificationTroubleshootCheckPermissionTest(
permissionStateProvider = FakePermissionStateProvider(),
sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkInt = Build.VERSION_CODES.TIRAMISU),
permissionActions = FakePermissionActions(),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
}
}
@Test
fun `test NotificationTroubleshootCheckPermissionTest TIRAMISU error`() = runTest {
val permissionStateProvider = FakePermissionStateProvider(
permissionGranted = false
)
val actions = FakePermissionActions(
openSettingsAction = {
permissionStateProvider.setPermissionGranted()
}
)
val sut = NotificationTroubleshootCheckPermissionTest(
permissionStateProvider = permissionStateProvider,
sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkInt = Build.VERSION_CODES.TIRAMISU),
permissionActions = actions,
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true))
// Quick fix
launch {
sut.quickFix(this)
// Run the test again (IRL it will be done thanks to the resuming of the application)
sut.run(this)
}
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
}
}
}

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2024 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 io.element.android.libraries.push.api
interface GetCurrentPushProvider {
suspend fun getCurrentPushProvider(): String?
}

View file

@ -37,6 +37,8 @@ interface PushService {
*/
suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor)
// TODO Move away
suspend fun testPush()
/**
* Return false in case of early error.
*/
suspend fun testPush(): Boolean
}

View file

@ -16,6 +16,6 @@
package io.element.android.libraries.push.api.gateway
sealed class PushGatewayFailure : Throwable(cause = null) {
data object PusherRejected : PushGatewayFailure()
sealed class PushGatewayFailure : Exception() {
class PusherRejected : PushGatewayFailure()
}

View file

@ -53,6 +53,7 @@ dependencies {
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.troubleshoot.api)
api(projects.libraries.pushproviders.api)
api(projects.libraries.pushstore.api)
api(projects.libraries.push.api)
@ -69,6 +70,8 @@ dependencies {
testImplementation(libs.coil.test)
testImplementation(libs.coroutines.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushproviders.test)
testImplementation(projects.tests.testutils)
testImplementation(projects.services.appnavstate.test)
testImplementation(projects.services.toolbox.impl)

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2024 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 io.element.android.libraries.push.impl
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.push.api.GetCurrentPushProvider
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import io.element.android.services.appnavstate.api.AppNavigationStateService
import io.element.android.services.appnavstate.api.currentSessionId
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultGetCurrentPushProvider @Inject constructor(
private val pushStoreFactory: UserPushStoreFactory,
private val appNavigationStateService: AppNavigationStateService,
) : GetCurrentPushProvider {
override suspend fun getCurrentPushProvider(): String? {
return appNavigationStateService
.appNavigationState
.value
.navigationState
.currentSessionId()
?.let { pushStoreFactory.getOrCreate(it) }
?.getPushProviderName()
}
}

View file

@ -19,6 +19,7 @@ package io.element.android.libraries.push.impl
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.push.api.GetCurrentPushProvider
import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager
import io.element.android.libraries.pushproviders.api.Distributor
@ -32,6 +33,7 @@ class DefaultPushService @Inject constructor(
private val pushersManager: PushersManager,
private val userPushStoreFactory: UserPushStoreFactory,
private val pushProviders: Set<@JvmSuppressWildcards PushProvider>,
private val getCurrentPushProvider: GetCurrentPushProvider,
) : PushService {
override fun notificationStyleChanged() {
defaultNotificationDrawerManager.notificationStyleChanged()
@ -47,7 +49,7 @@ class DefaultPushService @Inject constructor(
* Get current push provider, compare with provided one, then unregister and register if different, and store change.
*/
override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) {
val userPushStore = userPushStoreFactory.create(matrixClient.sessionId)
val userPushStore = userPushStoreFactory.getOrCreate(matrixClient.sessionId)
val currentPushProviderName = userPushStore.getPushProviderName()
if (currentPushProviderName != pushProvider.name) {
// Unregister previous one if any
@ -58,7 +60,11 @@ class DefaultPushService @Inject constructor(
userPushStore.setPushProviderName(pushProvider.name)
}
override suspend fun testPush() {
pushersManager.testPush()
override suspend fun testPush(): Boolean {
val currentPushProvider = getCurrentPushProvider.getCurrentPushProvider()
val pushProvider = pushProviders.find { it.name == currentPushProvider } ?: return false
val config = pushProvider.getCurrentUserPushConfig() ?: return false
pushersManager.testPush(config)
return true
}
}

View file

@ -23,9 +23,11 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest
import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig
import io.element.android.libraries.pushproviders.api.PusherSubscriber
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
@ -45,16 +47,14 @@ class PushersManager @Inject constructor(
private val pushClientSecret: PushClientSecret,
private val userPushStoreFactory: UserPushStoreFactory,
) : PusherSubscriber {
// TODO Move this to the PushProvider API
suspend fun testPush() {
suspend fun testPush(config: CurrentUserPushConfig) {
pushGatewayNotifyRequest.execute(
PushGatewayNotifyRequest.Params(
// unifiedPushHelper.getPushGateway() ?: return
url = "TODO",
url = config.url,
appId = PushConfig.PUSHER_APP_ID,
// unifiedPushHelper.getEndpointOrToken().orEmpty()
pushKey = "TODO",
eventId = TEST_EVENT_ID
pushKey = config.pushKey,
eventId = TEST_EVENT_ID,
roomId = TEST_ROOM_ID,
)
)
}
@ -63,7 +63,7 @@ class PushersManager @Inject constructor(
* Register a pusher to the server if not done yet.
*/
override suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) {
val userDataStore = userPushStoreFactory.create(matrixClient.sessionId)
val userDataStore = userPushStoreFactory.getOrCreate(matrixClient.sessionId)
if (userDataStore.getCurrentRegisteredPushKey() == pushKey) {
Timber.tag(loggerTag.value)
.d("Unnecessary to register again the same pusher, but do it in case the pusher has been removed from the server")
@ -112,5 +112,6 @@ class PushersManager @Inject constructor(
companion object {
val TEST_EVENT_ID = EventId("\$THIS_IS_A_FAKE_EVENT_ID")
val TEST_ROOM_ID = RoomId("!room:domain")
}
}

View file

@ -17,7 +17,6 @@
package io.element.android.libraries.push.impl.notifications
import android.Manifest
import android.annotation.SuppressLint
import android.app.Notification
import android.content.Context
import android.content.pm.PackageManager
@ -32,12 +31,13 @@ class NotificationDisplayer @Inject constructor(
) {
private val notificationManager = NotificationManagerCompat.from(context)
fun showNotificationMessage(tag: String?, id: Int, notification: Notification) {
fun showNotificationMessage(tag: String?, id: Int, notification: Notification): Boolean {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
Timber.w("Not allowed to notify.")
return
return false
}
notificationManager.notify(tag, id, notification)
return true
}
fun cancelNotificationMessage(tag: String?, id: Int) {
@ -53,15 +53,21 @@ class NotificationDisplayer @Inject constructor(
}
}
@SuppressLint("LaunchActivityFromNotification")
fun displayDiagnosticNotification(notification: Notification) {
showNotificationMessage(
fun displayDiagnosticNotification(notification: Notification): Boolean {
return showNotificationMessage(
tag = "DIAGNOSTIC",
id = NOTIFICATION_ID_DIAGNOSTIC,
notification = notification
)
}
fun dismissDiagnosticNotification() {
cancelNotificationMessage(
tag = "DIAGNOSTIC",
id = NOTIFICATION_ID_DIAGNOSTIC
)
}
/**
* Cancel the foreground notification service.
*/

View file

@ -19,9 +19,15 @@ package io.element.android.libraries.push.impl.notifications
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.push.impl.troubleshoot.NotificationClickHandler
import javax.inject.Inject
class TestNotificationReceiver : BroadcastReceiver() {
@Inject lateinit var notificationClickHandler: NotificationClickHandler
override fun onReceive(context: Context, intent: Intent) {
// TODO The test notification has been clicked, notify the ui
context.bindings<TestNotificationReceiverBinding>().inject(this)
notificationClickHandler.handleNotificationClick()
}
}

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2023 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 io.element.android.libraries.push.impl.notifications
import com.squareup.anvil.annotations.ContributesTo
import io.element.android.libraries.di.AppScope
@ContributesTo(AppScope::class)
interface TestNotificationReceiverBinding {
fun inject(service: TestNotificationReceiver)
}

View file

@ -299,6 +299,7 @@ class NotificationCreator @Inject constructor(
}
fun createDiagnosticNotification(): Notification {
val intent = pendingIntentFactory.createTestPendingIntent()
return NotificationCompat.Builder(context, notificationChannels.getChannelIdForTest())
.setContentTitle(buildMeta.applicationName)
.setContentText(stringProvider.getString(R.string.notification_test_push_notification_content))
@ -308,7 +309,8 @@ class NotificationCreator @Inject constructor(
.setPriority(NotificationCompat.PRIORITY_MAX)
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setAutoCancel(true)
.setContentIntent(pendingIntentFactory.createTestPendingIntent())
.setContentIntent(intent)
.setDeleteIntent(intent)
.build()
}

View file

@ -27,6 +27,7 @@ import io.element.android.libraries.push.impl.PushersManager
import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
import io.element.android.libraries.push.impl.store.DefaultPushDataStore
import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler
import io.element.android.libraries.pushproviders.api.PushData
import io.element.android.libraries.pushproviders.api.PushHandler
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
@ -51,6 +52,7 @@ class DefaultPushHandler @Inject constructor(
// private val actionIds: NotificationActionIds,
private val buildMeta: BuildMeta,
private val matrixAuthenticationService: MatrixAuthenticationService,
private val diagnosticPushHandler: DiagnosticPushHandler,
) : PushHandler {
private val coroutineScope = CoroutineScope(SupervisorJob())
@ -75,8 +77,7 @@ class DefaultPushHandler @Inject constructor(
// Diagnostic Push
if (pushData.eventId == PushersManager.TEST_EVENT_ID) {
// val intent = Intent(actionIds.push)
// TODO The test push has been received, notify the ui
diagnosticPushHandler.handlePush()
return
}
@ -121,7 +122,7 @@ class DefaultPushHandler @Inject constructor(
return
}
val userPushStore = userPushStoreFactory.create(userId)
val userPushStore = userPushStoreFactory.getOrCreate(userId)
if (!userPushStore.getNotificationEnabledForDevice().first()) {
// TODO We need to check if this is an incoming call
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")

View file

@ -23,6 +23,8 @@ import kotlinx.serialization.Serializable
internal data class PushGatewayNotification(
@SerialName("event_id")
val eventId: String,
@SerialName("room_id")
val roomId: String,
/**
* Required. This is an array of devices that the notification should be sent to.
*/

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.push.impl.pushgateway
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.network.RetrofitFactory
import io.element.android.libraries.push.api.gateway.PushGatewayFailure
import javax.inject.Inject
@ -27,7 +28,8 @@ class PushGatewayNotifyRequest @Inject constructor(
val url: String,
val appId: String,
val pushKey: String,
val eventId: EventId
val eventId: EventId,
val roomId: RoomId,
)
suspend fun execute(params: Params) {
@ -40,6 +42,7 @@ class PushGatewayNotifyRequest @Inject constructor(
PushGatewayNotifyBody(
PushGatewayNotification(
eventId = params.eventId.value,
roomId = params.roomId.value,
devices = listOf(
PushGatewayDevice(
params.appId,
@ -51,7 +54,7 @@ class PushGatewayNotifyRequest @Inject constructor(
)
if (response.rejectedPushKeys.contains(params.pushKey)) {
throw PushGatewayFailure.PusherRejected
throw PushGatewayFailure.PusherRejected()
}
}
}

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2024 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 io.element.android.libraries.push.impl.troubleshoot
import com.squareup.anvil.annotations.ContributesMultibinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.push.api.GetCurrentPushProvider
import io.element.android.libraries.push.impl.R
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.services.toolbox.api.strings.StringProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@ContributesMultibinding(AppScope::class)
class CurrentPushProviderTest @Inject constructor(
private val getCurrentPushProvider: GetCurrentPushProvider,
private val stringProvider: StringProvider,
) : NotificationTroubleshootTest {
override val order = 110
private val delegate = NotificationTroubleshootTestDelegate(
defaultName = stringProvider.getString(R.string.troubleshoot_notifications_test_current_push_provider_title),
defaultDescription = stringProvider.getString(R.string.troubleshoot_notifications_test_current_push_provider_description),
fakeDelay = NotificationTroubleshootTestDelegate.SHORT_DELAY,
)
override val state: StateFlow<NotificationTroubleshootTestState> = delegate.state
override suspend fun run(coroutineScope: CoroutineScope) {
delegate.start()
val provider = getCurrentPushProvider.getCurrentPushProvider()
if (provider != null) {
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_current_push_provider_success, provider),
status = NotificationTroubleshootTestState.Status.Success
)
} else {
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_current_push_provider_failure),
status = NotificationTroubleshootTestState.Status.Failure(false)
)
}
}
override suspend fun reset() = delegate.reset()
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2024 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 io.element.android.libraries.push.impl.troubleshoot
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import javax.inject.Inject
@SingleIn(AppScope::class)
class DiagnosticPushHandler @Inject constructor() {
private val _state = MutableSharedFlow<Unit>()
val state: SharedFlow<Unit> = _state
suspend fun handlePush() {
_state.emit(Unit)
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2024 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 io.element.android.libraries.push.impl.troubleshoot
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import javax.inject.Inject
@SingleIn(AppScope::class)
class NotificationClickHandler @Inject constructor() {
private val _state = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
val state: SharedFlow<Unit> = _state
fun handleNotificationClick() {
_state.tryEmit(Unit)
}
}

View file

@ -0,0 +1,101 @@
/*
* Copyright (c) 2024 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 io.element.android.libraries.push.impl.troubleshoot
import com.squareup.anvil.annotations.ContributesMultibinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.NotificationDisplayer
import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.services.toolbox.api.strings.StringProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import timber.log.Timber
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
@ContributesMultibinding(AppScope::class)
class NotificationTest @Inject constructor(
private val notificationCreator: NotificationCreator,
private val notificationDisplayer: NotificationDisplayer,
private val notificationClickHandler: NotificationClickHandler,
private val stringProvider: StringProvider,
) : NotificationTroubleshootTest {
override val order = 50
private val delegate = NotificationTroubleshootTestDelegate(
defaultName = stringProvider.getString(R.string.troubleshoot_notifications_test_display_notification_title),
defaultDescription = stringProvider.getString(R.string.troubleshoot_notifications_test_display_notification_description),
fakeDelay = NotificationTroubleshootTestDelegate.SHORT_DELAY,
)
override val state: StateFlow<NotificationTroubleshootTestState> = delegate.state
override suspend fun run(coroutineScope: CoroutineScope) {
delegate.start()
val notification = notificationCreator.createDiagnosticNotification()
val result = notificationDisplayer.displayDiagnosticNotification(notification)
if (result) {
coroutineScope.listenToNotificationClick()
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_display_notification_waiting),
status = NotificationTroubleshootTestState.Status.WaitingForUser
)
} else {
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_display_notification_permission_failure),
status = NotificationTroubleshootTestState.Status.Failure(false)
)
}
}
private fun CoroutineScope.listenToNotificationClick() = launch {
val job = launch {
notificationClickHandler.state.first()
Timber.d("Notification clicked!")
}
runCatching {
withTimeout(30.seconds) {
job.join()
}
}.fold(
onSuccess = {
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_display_notification_success),
status = NotificationTroubleshootTestState.Status.Success
)
},
onFailure = {
job.cancel()
notificationDisplayer.dismissDiagnosticNotification()
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_display_notification_failure),
status = NotificationTroubleshootTestState.Status.Failure(false)
)
}
)
}.invokeOnCompletion {
// Ensure that the notification is cancelled when the screen is left
notificationDisplayer.dismissDiagnosticNotification()
}
override suspend fun reset() = delegate.reset()
}

View file

@ -0,0 +1,109 @@
/*
* Copyright (c) 2024 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 io.element.android.libraries.push.impl.troubleshoot
import com.squareup.anvil.annotations.ContributesMultibinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.push.api.gateway.PushGatewayFailure
import io.element.android.libraries.push.impl.R
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.services.toolbox.api.strings.StringProvider
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import timber.log.Timber
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
@ContributesMultibinding(AppScope::class)
class PushLoopbackTest @Inject constructor(
private val pushService: PushService,
private val diagnosticPushHandler: DiagnosticPushHandler,
private val clock: SystemClock,
private val stringProvider: StringProvider,
) : NotificationTroubleshootTest {
override val order = 500
private val delegate = NotificationTroubleshootTestDelegate(
defaultName = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_title),
defaultDescription = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_description),
)
override val state: StateFlow<NotificationTroubleshootTestState> = delegate.state
override suspend fun run(coroutineScope: CoroutineScope) {
delegate.start()
val startTime = clock.epochMillis()
val completable = CompletableDeferred<Long>()
val job = coroutineScope.launch {
diagnosticPushHandler.state.first()
completable.complete(clock.epochMillis() - startTime)
}
val testPushResult = try {
pushService.testPush()
} catch (pusherRejected: PushGatewayFailure.PusherRejected) {
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_1),
status = NotificationTroubleshootTestState.Status.Failure(false)
)
job.cancel()
return
} catch (e: Exception) {
Timber.e(e, "Failed to test push")
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_2, e.message),
status = NotificationTroubleshootTestState.Status.Failure(false)
)
job.cancel()
return
}
if (!testPushResult) {
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_3),
status = NotificationTroubleshootTestState.Status.Failure(false)
)
job.cancel()
return
}
runCatching {
withTimeout(10.seconds) {
completable.await()
}
}.fold(
onSuccess = { duration ->
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_success, duration),
status = NotificationTroubleshootTestState.Status.Success
)
},
onFailure = {
job.cancel()
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_4),
status = NotificationTroubleshootTestState.Status.Failure(false)
)
}
)
}
override suspend fun reset() = delegate.reset()
}

View file

@ -0,0 +1,67 @@
/*
* Copyright (c) 2024 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 io.element.android.libraries.push.impl.troubleshoot
import com.squareup.anvil.annotations.ContributesMultibinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.push.impl.R
import io.element.android.libraries.pushproviders.api.PushProvider
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.services.toolbox.api.strings.StringProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@ContributesMultibinding(AppScope::class)
class PushProvidersTest @Inject constructor(
pushProviders: Set<@JvmSuppressWildcards PushProvider>,
private val stringProvider: StringProvider,
) : NotificationTroubleshootTest {
private val sortedPushProvider = pushProviders.sortedBy { it.index }
override val order = 100
private val delegate = NotificationTroubleshootTestDelegate(
defaultName = stringProvider.getString(R.string.troubleshoot_notifications_test_detect_push_provider_title),
defaultDescription = stringProvider.getString(R.string.troubleshoot_notifications_test_detect_push_provider_description),
fakeDelay = NotificationTroubleshootTestDelegate.SHORT_DELAY,
)
override val state: StateFlow<NotificationTroubleshootTestState> = delegate.state
override suspend fun run(coroutineScope: CoroutineScope) {
delegate.start()
val result = sortedPushProvider.isNotEmpty()
if (result) {
delegate.updateState(
description = stringProvider.getQuantityString(
resId = R.plurals.troubleshoot_notifications_test_detect_push_provider_success,
quantity = sortedPushProvider.size,
sortedPushProvider.size,
sortedPushProvider.joinToString { it.name }
),
status = NotificationTroubleshootTestState.Status.Success
)
} else {
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_detect_push_provider_failure),
status = NotificationTroubleshootTestState.Status.Failure(false)
)
}
}
override suspend fun reset() = delegate.reset()
}

View file

@ -56,4 +56,29 @@
<string name="push_distributor_background_sync_android">"Фонавая сінхранізацыя"</string>
<string name="push_distributor_firebase_android">"Сэрвісы Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Службы Google Play не знойдзены. Апавяшчэнні могуць не працаваць належным чынам."</string>
<string name="troubleshoot_notifications_test_current_push_provider_description">"Атрымаць назву бягучага пастаўшчыка."</string>
<string name="troubleshoot_notifications_test_current_push_provider_failure">"Пастаўшчыкі push-апавяшчэнняў не выбраны."</string>
<string name="troubleshoot_notifications_test_current_push_provider_success">"Бягучы пастаўшчык push-апавяшчэнняў: %1$s."</string>
<string name="troubleshoot_notifications_test_current_push_provider_title">"Бягучы пастаўшчык push-апавяшчэнняў"</string>
<string name="troubleshoot_notifications_test_detect_push_provider_description">"Пераканайцеся, што ў праграме ёсць хаця б адзін пастаўшчык push-апавяшчэнняў."</string>
<string name="troubleshoot_notifications_test_detect_push_provider_failure">"Пастаўшчыкі push-апавяшчэнняў не знойдзены."</string>
<plurals name="troubleshoot_notifications_test_detect_push_provider_success">
<item quantity="one">"Знайшлі %1$d пастаўшчыка push-апавяшчэнняў: %2$s"</item>
<item quantity="few">"Знайшлі %1$d пастаўшчыкоў push-апавяшчэнняў: %2$s"</item>
<item quantity="many">"Знайшлі %1$d пастаўшчыкоў push-апавяшчэнняў: %2$s"</item>
</plurals>
<string name="troubleshoot_notifications_test_detect_push_provider_title">"Выяўленне пастаўшчыкоў push-паслуг"</string>
<string name="troubleshoot_notifications_test_display_notification_description">"Праверце, ці можа праграма паказваць апавяшчэнні."</string>
<string name="troubleshoot_notifications_test_display_notification_failure">"Апавяшчэнне не было націснута."</string>
<string name="troubleshoot_notifications_test_display_notification_permission_failure">"Немагчыма паказаць апавяшчэнне."</string>
<string name="troubleshoot_notifications_test_display_notification_success">"Апавяшчэнне было націснута!"</string>
<string name="troubleshoot_notifications_test_display_notification_title">"Паказаць апавяшчэнне"</string>
<string name="troubleshoot_notifications_test_display_notification_waiting">"Націсніце на апавяшчэнне, каб працягнуць тэст."</string>
<string name="troubleshoot_notifications_test_push_loop_back_description">"Пераканайцеся, што праграма атрымлівае push-апавяшчэнні."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_1">"Памылка: pusher адхіліў запыт."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_2">"Памылка: %1$s."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_3">"Памылка, немагчыма праверыць push-апавяшчэнне."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_4">"Памылка, тайм-аўт у чаканні push-апавяшчэння."</string>
<string name="troubleshoot_notifications_test_push_loop_back_success">"Зварот цыклу назад заняў %1$d мс."</string>
<string name="troubleshoot_notifications_test_push_loop_back_title">"Тэст Націсніце кнопку вярнуцца"</string>
</resources>

View file

@ -36,4 +36,5 @@
<item quantity="one">"%d стая"</item>
<item quantity="other">"%d стаи"</item>
</plurals>
<string name="troubleshoot_notifications_test_push_loop_back_failure_2">"Грешка: %1$s"</string>
</resources>

View file

@ -56,4 +56,29 @@
<string name="push_distributor_background_sync_android">"Synchronizace na pozadí"</string>
<string name="push_distributor_firebase_android">"Služby Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Nebyly nalezeny žádné funkční služby Google Play. Oznámení nemusí fungovat správně."</string>
<string name="troubleshoot_notifications_test_current_push_provider_description">"Získat název aktuálního poskytovatele."</string>
<string name="troubleshoot_notifications_test_current_push_provider_failure">"Nebyli vybráni žádní push poskytovatelé."</string>
<string name="troubleshoot_notifications_test_current_push_provider_success">"Aktuální push poskytovatel: %1$s."</string>
<string name="troubleshoot_notifications_test_current_push_provider_title">"Aktuální push poskytovatel"</string>
<string name="troubleshoot_notifications_test_detect_push_provider_description">"Ujistěte se, že aplikace má alespoň jednoho push poskytovatele."</string>
<string name="troubleshoot_notifications_test_detect_push_provider_failure">"Nebyli nalezeni žádní push poskytovatelé."</string>
<plurals name="troubleshoot_notifications_test_detect_push_provider_success">
<item quantity="one">"Nalezen %1$d push poskytovatel: %2$s"</item>
<item quantity="few">"Nalezeni %1$d push poskytovatelé: %2$s"</item>
<item quantity="other">"Nalezeno %1$d push poskytovatelů: %2$s"</item>
</plurals>
<string name="troubleshoot_notifications_test_detect_push_provider_title">"Zjistit push poskytovatele"</string>
<string name="troubleshoot_notifications_test_display_notification_description">"Zkontrolujte, zda aplikace může zobrazit oznámení."</string>
<string name="troubleshoot_notifications_test_display_notification_failure">"Na oznámení nebylo kliknuto."</string>
<string name="troubleshoot_notifications_test_display_notification_permission_failure">"Oznámení nelze zobrazit."</string>
<string name="troubleshoot_notifications_test_display_notification_success">"Na oznámení bylo kliknuto!"</string>
<string name="troubleshoot_notifications_test_display_notification_title">"Zobrazit oznámení"</string>
<string name="troubleshoot_notifications_test_display_notification_waiting">"Kliknutím na oznámení pokračujte v testu."</string>
<string name="troubleshoot_notifications_test_push_loop_back_description">"Ujistěte se, že aplikace přijímá push."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_1">"Chyba: pusher odmítl požadavek."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_2">"Chyba: %1$s."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_3">"Chyba, nelze otestovat push."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_4">"Chyba, časový limit čekání na push."</string>
<string name="troubleshoot_notifications_test_push_loop_back_success">"Push zpětná smyčka trvala %1$d ms."</string>
<string name="troubleshoot_notifications_test_push_loop_back_title">"Otestovat push zpětnou smyčku"</string>
</resources>

View file

@ -50,4 +50,28 @@
<string name="push_distributor_background_sync_android">"Hintergrundsynchronisation"</string>
<string name="push_distributor_firebase_android">"Google-Dienste"</string>
<string name="push_no_valid_google_play_services_apk_android">"Keine gültigen Google Play-Dienste gefunden. Benachrichtigungen funktionieren möglicherweise nicht richtig."</string>
<string name="troubleshoot_notifications_test_current_push_provider_description">"Ermittele den Namen des aktuellen Anbieters."</string>
<string name="troubleshoot_notifications_test_current_push_provider_failure">"Kein Push-Anbieter ausgewählt."</string>
<string name="troubleshoot_notifications_test_current_push_provider_success">"Aktueller Push-Anbieter:%1$s."</string>
<string name="troubleshoot_notifications_test_current_push_provider_title">"Aktueller Push-Anbieter"</string>
<string name="troubleshoot_notifications_test_detect_push_provider_description">"Stelle sicher, dass die Anwendung mindestens einen Push-Anbieter hat."</string>
<string name="troubleshoot_notifications_test_detect_push_provider_failure">"Keine Push-Anbieter gefunden."</string>
<plurals name="troubleshoot_notifications_test_detect_push_provider_success">
<item quantity="one">"%1$d Push-Anbieter gefunden: %2$s"</item>
<item quantity="other">"%1$d Push-Anbieter gefunden: %2$s"</item>
</plurals>
<string name="troubleshoot_notifications_test_detect_push_provider_title">"Push-Anbieter erkennen"</string>
<string name="troubleshoot_notifications_test_display_notification_description">"Prüfe, ob die Anwendung Benachrichtigungen anzeigen kann."</string>
<string name="troubleshoot_notifications_test_display_notification_failure">"Die Benachrichtigung wurde nicht angeklickt."</string>
<string name="troubleshoot_notifications_test_display_notification_permission_failure">"Die Benachrichtigung kann nicht angezeigt werden."</string>
<string name="troubleshoot_notifications_test_display_notification_success">"Die Benachrichtigung wurde angeklickt!"</string>
<string name="troubleshoot_notifications_test_display_notification_title">"Benachrichtigung anzeigen"</string>
<string name="troubleshoot_notifications_test_display_notification_waiting">"Bitte klicke auf die Benachrichtigung, um den Test fortzusetzen."</string>
<string name="troubleshoot_notifications_test_push_loop_back_description">"Stelle sicher, dass die Anwendung Push-Nachrichten empfängt."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_1">"Fehler: Der Pusher hat die Anfrage abgelehnt."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_2">"Fehler:%1$s."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_3">"Fehler: Push kann nicht getestet werden."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_4">"Fehler: Timeout beim Warten auf Push."</string>
<string name="troubleshoot_notifications_test_push_loop_back_success">"Push-Loop-Back Dauer: %1$d ms."</string>
<string name="troubleshoot_notifications_test_push_loop_back_title">"Teste Push-Loop-Back"</string>
</resources>

View file

@ -50,4 +50,28 @@
<string name="push_distributor_background_sync_android">"Synchronisation en arrière-plan"</string>
<string name="push_distributor_firebase_android">"Services Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Aucun service Google Play valide na été trouvé. Les notifications peuvent ne pas fonctionner correctement."</string>
<string name="troubleshoot_notifications_test_current_push_provider_description">"Obtenir le nom du fournisseur de Push actuel."</string>
<string name="troubleshoot_notifications_test_current_push_provider_failure">"Aucun fournisseur de Push nest sélectionné."</string>
<string name="troubleshoot_notifications_test_current_push_provider_success">"Fournisseur de Push actuel : %1$s."</string>
<string name="troubleshoot_notifications_test_current_push_provider_title">"Fournisseur de Push actuel"</string>
<string name="troubleshoot_notifications_test_detect_push_provider_description">"Vérifier que lapplication possède au moins un fournisseur de Push."</string>
<string name="troubleshoot_notifications_test_detect_push_provider_failure">"Aucun fournisseur de Push na été trouvé."</string>
<plurals name="troubleshoot_notifications_test_detect_push_provider_success">
<item quantity="one">"%1$d fournisseur de Push détecté : %2$s"</item>
<item quantity="other">"%1$d fournisseurs de Push détectés : %2$s"</item>
</plurals>
<string name="troubleshoot_notifications_test_detect_push_provider_title">"Détecter les fournisseurs de Push"</string>
<string name="troubleshoot_notifications_test_display_notification_description">"Vérifier que lapplication peut afficher des notifications."</string>
<string name="troubleshoot_notifications_test_display_notification_failure">"Vous navez pas cliqué sur la notification."</string>
<string name="troubleshoot_notifications_test_display_notification_permission_failure">"Impossible dafficher la notification."</string>
<string name="troubleshoot_notifications_test_display_notification_success">"Vous avez cliqué sur la notification!"</string>
<string name="troubleshoot_notifications_test_display_notification_title">"Affichage des notifications"</string>
<string name="troubleshoot_notifications_test_display_notification_waiting">"Veuillez cliquer sur la notification pour continuer le test."</string>
<string name="troubleshoot_notifications_test_push_loop_back_description">"Vérifier que lapplication reçoit les Push."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_1">"Erreur : le Pusher a rejeté la demande."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_2">"Erreur :%1$s."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_3">"Erreur, impossible de tester les Push."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_4">"Erreur, le délai dattente du Push est dépassé."</string>
<string name="troubleshoot_notifications_test_push_loop_back_success">"La demande denvoi de Push et sa réception ont pris %1$d ms."</string>
<string name="troubleshoot_notifications_test_push_loop_back_title">"Tester la réception des Push"</string>
</resources>

View file

@ -50,4 +50,28 @@
<string name="push_distributor_background_sync_android">"Háttérszinkronizálás"</string>
<string name="push_distributor_firebase_android">"Google szolgáltatások"</string>
<string name="push_no_valid_google_play_services_apk_android">"A Google Play szolgáltatások nem találhatók. Előfordulhat, hogy az értesítések nem működnek megfelelően."</string>
<string name="troubleshoot_notifications_test_current_push_provider_description">"A jelenlegi szolgáltató nevének lekérdezése."</string>
<string name="troubleshoot_notifications_test_current_push_provider_failure">"Nincs kiválasztva leküldéses értesítési szolgáltató."</string>
<string name="troubleshoot_notifications_test_current_push_provider_success">"Jelenlegi leküldéses értesítési szolgáltató: %1$s."</string>
<string name="troubleshoot_notifications_test_current_push_provider_title">"Jelenlegi leküldéses értesítési szolgáltató"</string>
<string name="troubleshoot_notifications_test_detect_push_provider_description">"Győződjön meg arról, hogy az alkalmazás legalább egy leküldéses értesítési szolgáltatóval rendelkezik."</string>
<string name="troubleshoot_notifications_test_detect_push_provider_failure">"Nem található leküldéses értesítési szolgáltató."</string>
<plurals name="troubleshoot_notifications_test_detect_push_provider_success">
<item quantity="one">"%1$d leküldéses szolgáltató találva: %2$s"</item>
<item quantity="other">"%1$d leküldéses szolgáltató találva: %2$s"</item>
</plurals>
<string name="troubleshoot_notifications_test_detect_push_provider_title">"Leküldéses értesítési szolgáltatók észlelése"</string>
<string name="troubleshoot_notifications_test_display_notification_description">"Ellenőrizze, hogy az alkalmazás képes-e megjeleníteni az értesítést."</string>
<string name="troubleshoot_notifications_test_display_notification_failure">"Az értesítésre nem kattintottak rá."</string>
<string name="troubleshoot_notifications_test_display_notification_permission_failure">"Az értesítés nem jeleníthető meg."</string>
<string name="troubleshoot_notifications_test_display_notification_success">"Az értesítésre rákattintottak!"</string>
<string name="troubleshoot_notifications_test_display_notification_title">"Értesítés megjelenítése"</string>
<string name="troubleshoot_notifications_test_display_notification_waiting">"A teszt folytatásához kattintson az értesítésre."</string>
<string name="troubleshoot_notifications_test_push_loop_back_description">"Győződjön meg arról, hogy az alkalmazás megkapja-e a leküldéses értesítést."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_1">"Hiba: a leküldő elutasította a kérést."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_2">"Hiba: %1$s."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_3">"Hiba, nem lehet tesztelni a leküldéses értesítést."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_4">"Hiba, időtúllépés a leküldéses értesítésre való várakozás során."</string>
<string name="troubleshoot_notifications_test_push_loop_back_success">"A leküldéses értesítés folyamata %1$d ezredmásodpercig tartott."</string>
<string name="troubleshoot_notifications_test_push_loop_back_title">"Tesztelje a leküldéses értesítés folyamatát"</string>
</resources>

View file

@ -44,4 +44,27 @@
<string name="push_distributor_background_sync_android">"Sinkronisasi latar belakang"</string>
<string name="push_distributor_firebase_android">"Layanan Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Tidak ditemukan Layanan Google Play yang valid. Pemberitahuan mungkin tidak berfungsi dengan baik."</string>
<string name="troubleshoot_notifications_test_current_push_provider_description">"Dapatkan nama penyedia saat ini."</string>
<string name="troubleshoot_notifications_test_current_push_provider_failure">"Tidak ada penyedia notifikasi dorongan yang dipilih."</string>
<string name="troubleshoot_notifications_test_current_push_provider_success">"Penyedia notifikasi dorongan saat ini: %1$s."</string>
<string name="troubleshoot_notifications_test_current_push_provider_title">"Penyedia notifikasi dorongan saat ini"</string>
<string name="troubleshoot_notifications_test_detect_push_provider_description">"Pastikan aplikasi memiliki setidaknya satu penyedia notifikasi dorongan."</string>
<string name="troubleshoot_notifications_test_detect_push_provider_failure">"Tidak ada penyedia notifikasi dorongan yang ditemukan."</string>
<plurals name="troubleshoot_notifications_test_detect_push_provider_success">
<item quantity="other">"Ditemukan %1$d penyedia notifikasi dorongan: %2$s"</item>
</plurals>
<string name="troubleshoot_notifications_test_detect_push_provider_title">"Deteksi penyedia notifikasi dorongan"</string>
<string name="troubleshoot_notifications_test_display_notification_description">"Periksa apakah aplikasi dapat menampilkan notifikasi."</string>
<string name="troubleshoot_notifications_test_display_notification_failure">"Notifikasi belum diklik."</string>
<string name="troubleshoot_notifications_test_display_notification_permission_failure">"Tidak dapat menampilkan notifikasi."</string>
<string name="troubleshoot_notifications_test_display_notification_success">"Notifikasi telah diklik!"</string>
<string name="troubleshoot_notifications_test_display_notification_title">"Tampilan notifikasi"</string>
<string name="troubleshoot_notifications_test_display_notification_waiting">"Silakan klik pada notifikasi untuk melanjutkan tes."</string>
<string name="troubleshoot_notifications_test_push_loop_back_description">"Pastikan aplikasi menerima notifikasi dorongan."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_1">"Kesalahan: pendorong telah menolak permintaan."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_2">"Kesalahan: %1$s."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_3">"Terjadi kesalahan, tidak dapat menguji notifikasi dorongan."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_4">"Terjadi kesalahan, melebihi batas waktu menunggu notifikasi dorongan."</string>
<string name="troubleshoot_notifications_test_push_loop_back_success">"Ulangan notifikasi dorongan membutuhkan %1$d ms."</string>
<string name="troubleshoot_notifications_test_push_loop_back_title">"Uji ulangan notifikasi dorongan lagi"</string>
</resources>

View file

@ -56,4 +56,29 @@
<string name="push_distributor_background_sync_android">"Фоновая синхронизация"</string>
<string name="push_distributor_firebase_android">"Сервисы Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Не найдены действующие службы Google Play. Уведомления могут работать некорректно."</string>
<string name="troubleshoot_notifications_test_current_push_provider_description">"Получение имени текущего поставщика."</string>
<string name="troubleshoot_notifications_test_current_push_provider_failure">"Поставщики push-уведомлений не выбраны."</string>
<string name="troubleshoot_notifications_test_current_push_provider_success">"Текущий поставщик push-уведомлений: %1$s."</string>
<string name="troubleshoot_notifications_test_current_push_provider_title">"Текущий поставщик push-уведомлений"</string>
<string name="troubleshoot_notifications_test_detect_push_provider_description">"Убедитесь, что у приложения есть хотя бы один поставщик push-сообщений."</string>
<string name="troubleshoot_notifications_test_detect_push_provider_failure">"Поставщики push-уведомлений не найдены."</string>
<plurals name="troubleshoot_notifications_test_detect_push_provider_success">
<item quantity="one">"Найден %1$d push-провайдер: %2$s"</item>
<item quantity="few">"Найдено %1$d push-провайдеров: %2$s"</item>
<item quantity="many">"Найдено %1$d push-провайдеров: %2$s"</item>
</plurals>
<string name="troubleshoot_notifications_test_detect_push_provider_title">"Обнаружение поставщиков push-уведомлений"</string>
<string name="troubleshoot_notifications_test_display_notification_description">"Убедитесь, что приложение может отображать уведомление."</string>
<string name="troubleshoot_notifications_test_display_notification_failure">"Уведомление не было нажато."</string>
<string name="troubleshoot_notifications_test_display_notification_permission_failure">"Невозможно отобразить уведомление."</string>
<string name="troubleshoot_notifications_test_display_notification_success">"Уведомление было нажато!"</string>
<string name="troubleshoot_notifications_test_display_notification_title">"Отобразить уведомление"</string>
<string name="troubleshoot_notifications_test_display_notification_waiting">"Нажмите на уведомление, чтобы продолжить тест."</string>
<string name="troubleshoot_notifications_test_push_loop_back_description">"Убедитесь, что приложение получает push-сообщение."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_1">"Ошибка: pusher отклонил запрос."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_2">"Ошибка: %1$s."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_3">"Ошибка, невозможно протестировать отправку."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_4">"Ошибка, тайм-аут ожидания push-уведомления."</string>
<string name="troubleshoot_notifications_test_push_loop_back_success">"Обратная отправка push-уведомления, заняла %1$d мс."</string>
<string name="troubleshoot_notifications_test_push_loop_back_title">"Тест обратной отправки push-уведомления"</string>
</resources>

View file

@ -56,4 +56,29 @@
<string name="push_distributor_background_sync_android">"Synchronizácia na pozadí"</string>
<string name="push_distributor_firebase_android">"Služby Google"</string>
<string name="push_no_valid_google_play_services_apk_android">"Nenašli sa žiadne platné služby Google Play. Oznámenia nemusia fungovať správne."</string>
<string name="troubleshoot_notifications_test_current_push_provider_description">"Získaťe názov aktuálneho poskytovateľa."</string>
<string name="troubleshoot_notifications_test_current_push_provider_failure">"Nie sú vybraní žiadni poskytovatelia push."</string>
<string name="troubleshoot_notifications_test_current_push_provider_success">"Aktuálny poskytovateľ push: %1$s."</string>
<string name="troubleshoot_notifications_test_current_push_provider_title">"Aktuálny poskytovateľ push"</string>
<string name="troubleshoot_notifications_test_detect_push_provider_description">"Uistite sa, že aplikácia má aspoň jedného poskytovateľa push."</string>
<string name="troubleshoot_notifications_test_detect_push_provider_failure">"Nenašli sa žiadni poskytovatelia push."</string>
<plurals name="troubleshoot_notifications_test_detect_push_provider_success">
<item quantity="one">"Nájdený %1$d poskytovateľ služby push: %2$s"</item>
<item quantity="few">"Nájdení %1$d poskytovatelia služby push: %2$s"</item>
<item quantity="other">"Nájdených %1$d poskytovateľov služby push: %2$s"</item>
</plurals>
<string name="troubleshoot_notifications_test_detect_push_provider_title">"Zistiť poskytovateľov push"</string>
<string name="troubleshoot_notifications_test_display_notification_description">"Skontrolujte, či aplikácia dokáže zobraziť upozornenie."</string>
<string name="troubleshoot_notifications_test_display_notification_failure">"Na oznámenie nebolo kliknuté."</string>
<string name="troubleshoot_notifications_test_display_notification_permission_failure">"Nie je možné zobraziť upozornenie."</string>
<string name="troubleshoot_notifications_test_display_notification_success">"Na oznámenie bolo kliknuté!"</string>
<string name="troubleshoot_notifications_test_display_notification_title">"Zobraziť upozornenie"</string>
<string name="troubleshoot_notifications_test_display_notification_waiting">"Kliknite na upozornenie a pokračujte v teste."</string>
<string name="troubleshoot_notifications_test_push_loop_back_description">"Uistite sa, že aplikácia prijíma push oznámenia."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_1">"Chyba: pusher odmietol požiadavku."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_2">"Chyba: %1$s."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_3">"Chyba, nie je možné testovať push."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_4">"Chyba, časový limit na push vypršal."</string>
<string name="troubleshoot_notifications_test_push_loop_back_success">"Push loop back trvalo %1$d ms."</string>
<string name="troubleshoot_notifications_test_push_loop_back_title">"Testovať Push loop back"</string>
</resources>

View file

@ -50,4 +50,28 @@
<string name="push_distributor_background_sync_android">"Background synchronization"</string>
<string name="push_distributor_firebase_android">"Google Services"</string>
<string name="push_no_valid_google_play_services_apk_android">"No valid Google Play Services found. Notifications may not work properly."</string>
<string name="troubleshoot_notifications_test_current_push_provider_description">"Get the name of the current provider."</string>
<string name="troubleshoot_notifications_test_current_push_provider_failure">"No push providers selected."</string>
<string name="troubleshoot_notifications_test_current_push_provider_success">"Current push provider: %1$s."</string>
<string name="troubleshoot_notifications_test_current_push_provider_title">"Current push provider"</string>
<string name="troubleshoot_notifications_test_detect_push_provider_description">"Ensure that the application has at least one push provider."</string>
<string name="troubleshoot_notifications_test_detect_push_provider_failure">"No push providers found."</string>
<plurals name="troubleshoot_notifications_test_detect_push_provider_success">
<item quantity="one">"Found %1$d push provider: %2$s"</item>
<item quantity="other">"Found %1$d push providers: %2$s"</item>
</plurals>
<string name="troubleshoot_notifications_test_detect_push_provider_title">"Detect push providers"</string>
<string name="troubleshoot_notifications_test_display_notification_description">"Check that the application can display notification."</string>
<string name="troubleshoot_notifications_test_display_notification_failure">"The notification has not been clicked."</string>
<string name="troubleshoot_notifications_test_display_notification_permission_failure">"Cannot display the notification."</string>
<string name="troubleshoot_notifications_test_display_notification_success">"The notification has been clicked!"</string>
<string name="troubleshoot_notifications_test_display_notification_title">"Display notification"</string>
<string name="troubleshoot_notifications_test_display_notification_waiting">"Please click on the notification to continue the test."</string>
<string name="troubleshoot_notifications_test_push_loop_back_description">"Ensure that the application is receiving push."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_1">"Error: pusher has rejected the request."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_2">"Error: %1$s."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_3">"Error, cannot test push."</string>
<string name="troubleshoot_notifications_test_push_loop_back_failure_4">"Error, timeout waiting for push."</string>
<string name="troubleshoot_notifications_test_push_loop_back_success">"Push loop back took %1$d ms."</string>
<string name="troubleshoot_notifications_test_push_loop_back_title">"Test Push loop back"</string>
</resources>

View file

@ -23,10 +23,10 @@ import io.element.android.libraries.matrix.test.A_SPACE_ID
import io.element.android.libraries.matrix.test.A_THREAD_ID
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.push.impl.notifications.fake.FakeAndroidNotificationFactory
import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoaderHolder
import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.fake.MockkNotificationCreator
import io.element.android.libraries.push.impl.notifications.fake.MockkRoomGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.fake.MockkSummaryGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.services.appnavstate.api.AppNavigationState
@ -115,12 +115,12 @@ class DefaultNotificationDrawerManagerTest {
appNavigationStateService = appNavigationStateService
),
notificationRenderer = NotificationRenderer(
NotificationIdProvider(),
NotificationDisplayer(context),
NotificationFactory(
FakeAndroidNotificationFactory().instance,
FakeRoomGroupMessageCreator().instance,
FakeSummaryGroupMessageCreator().instance,
notificationIdProvider = NotificationIdProvider(),
notificationDisplayer = NotificationDisplayer(context),
notificationFactory = NotificationFactory(
notificationCreator = MockkNotificationCreator().instance,
roomGroupMessageCreator = MockkRoomGroupMessageCreator().instance,
summaryGroupMessageCreator = MockkSummaryGroupMessageCreator().instance,
)
),
notificationEventPersistence = InMemoryNotificationEventPersistence(initialData = initialData),

View file

@ -25,7 +25,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_SPACE_ID
import io.element.android.libraries.matrix.test.A_THREAD_ID
import io.element.android.libraries.push.impl.notifications.fake.FakeOutdatedEventDetector
import io.element.android.libraries.push.impl.notifications.fake.MockkOutdatedEventDetector
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent
import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
@ -42,7 +42,7 @@ private val VIEWING_A_ROOM = aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_I
private val VIEWING_A_THREAD = aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID, A_THREAD_ID)
class NotifiableEventProcessorTest {
private val outdatedDetector = FakeOutdatedEventDetector()
private val mockkOutdatedDetector = MockkOutdatedEventDetector()
@Test
fun `given simple events when processing then keep simple events`() {
@ -97,7 +97,7 @@ class NotifiableEventProcessorTest {
@Test
fun `given out of date message event when processing then removes message event`() {
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID))
outdatedDetector.givenEventIsOutOfDate(events[0])
mockkOutdatedDetector.givenEventIsOutOfDate(events[0])
val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM)
@ -113,7 +113,7 @@ class NotifiableEventProcessorTest {
@Test
fun `given in date message event when processing then keep message event`() {
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID))
outdatedDetector.givenEventIsInDate(events[0])
mockkOutdatedDetector.givenEventIsInDate(events[0])
val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM)
val result = eventProcessor.process(events, renderedEvents = emptyList())
@ -128,7 +128,7 @@ class NotifiableEventProcessorTest {
@Test
fun `given viewing the same room main timeline when processing main timeline message event then removes message`() {
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = null))
events.forEach { outdatedDetector.givenEventIsOutOfDate(it) }
events.forEach { mockkOutdatedDetector.givenEventIsOutOfDate(it) }
val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_ROOM)
val result = eventProcessor.process(events, renderedEvents = emptyList())
@ -143,7 +143,7 @@ class NotifiableEventProcessorTest {
@Test
fun `given viewing the same thread timeline when processing thread message event then removes message`() {
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = A_THREAD_ID))
events.forEach { outdatedDetector.givenEventIsOutOfDate(it) }
events.forEach { mockkOutdatedDetector.givenEventIsOutOfDate(it) }
val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_THREAD)
val result = eventProcessor.process(events, renderedEvents = emptyList())
@ -158,7 +158,7 @@ class NotifiableEventProcessorTest {
@Test
fun `given viewing main timeline of the same room when processing thread timeline message event then keep message`() {
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = A_THREAD_ID))
outdatedDetector.givenEventIsInDate(events[0])
mockkOutdatedDetector.givenEventIsInDate(events[0])
val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_ROOM)
val result = eventProcessor.process(events, renderedEvents = emptyList())
@ -173,7 +173,7 @@ class NotifiableEventProcessorTest {
@Test
fun `given viewing thread timeline of the same room when processing main timeline message event then keep message`() {
val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID))
outdatedDetector.givenEventIsInDate(events[0])
mockkOutdatedDetector.givenEventIsInDate(events[0])
val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_THREAD)
val result = eventProcessor.process(events, renderedEvents = emptyList())
@ -213,8 +213,8 @@ class NotifiableEventProcessorTest {
navigationState: NavigationState
): NotifiableEventProcessor {
return NotifiableEventProcessor(
outdatedDetector.instance,
FakeAppNavigationStateService(MutableStateFlow(AppNavigationState(navigationState, isInForeground))),
outdatedDetector = mockkOutdatedDetector.instance,
appNavigationStateService = FakeAppNavigationStateService(MutableStateFlow(AppNavigationState(navigationState, isInForeground))),
)
}
}

View file

@ -22,10 +22,10 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.push.impl.notifications.fake.FakeAndroidNotificationFactory
import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader
import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.fake.MockkNotificationCreator
import io.element.android.libraries.push.impl.notifications.fake.MockkRoomGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.fake.MockkSummaryGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent
import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
@ -41,19 +41,19 @@ private val A_MESSAGE_EVENT = aNotifiableMessageEvent(eventId = AN_EVENT_ID, roo
@RunWith(RobolectricTestRunner::class)
class NotificationFactoryTest {
private val androidNotificationFactory = FakeAndroidNotificationFactory()
private val roomGroupMessageCreator = FakeRoomGroupMessageCreator()
private val summaryGroupMessageCreator = FakeSummaryGroupMessageCreator()
private val mockkNotificationCreator = MockkNotificationCreator()
private val mockkRoomGroupMessageCreator = MockkRoomGroupMessageCreator()
private val mockkSummaryGroupMessageCreator = MockkSummaryGroupMessageCreator()
private val notificationFactory = NotificationFactory(
androidNotificationFactory.instance,
roomGroupMessageCreator.instance,
summaryGroupMessageCreator.instance
notificationCreator = mockkNotificationCreator.instance,
roomGroupMessageCreator = mockkRoomGroupMessageCreator.instance,
summaryGroupMessageCreator = mockkSummaryGroupMessageCreator.instance
)
@Test
fun `given a room invitation when mapping to notification then is Append`() = testWith(notificationFactory) {
val expectedNotification = androidNotificationFactory.givenCreateRoomInvitationNotificationFor(AN_INVITATION_EVENT)
val expectedNotification = mockkNotificationCreator.givenCreateRoomInvitationNotificationFor(AN_INVITATION_EVENT)
val roomInvitation = listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, AN_INVITATION_EVENT))
val result = roomInvitation.toNotifications()
@ -90,7 +90,7 @@ class NotificationFactoryTest {
@Test
fun `given a simple event when mapping to notification then is Append`() = testWith(notificationFactory) {
val expectedNotification = androidNotificationFactory.givenCreateSimpleInvitationNotificationFor(A_SIMPLE_EVENT)
val expectedNotification = mockkNotificationCreator.givenCreateSimpleInvitationNotificationFor(A_SIMPLE_EVENT)
val roomInvitation = listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_SIMPLE_EVENT))
val result = roomInvitation.toNotifications()
@ -128,7 +128,7 @@ class NotificationFactoryTest {
@Test
fun `given room with message when mapping to notification then delegates to room group message creator`() = testWith(notificationFactory) {
val events = listOf(A_MESSAGE_EVENT)
val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor(
val expectedNotification = mockkRoomGroupMessageCreator.givenCreatesRoomMessageFor(
MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
events,
A_ROOM_ID
@ -197,7 +197,7 @@ class NotificationFactoryTest {
)
)
val withRedactedRemoved = listOf(A_MESSAGE_EVENT.copy(eventId = EventId("\$not-redacted")))
val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor(
val expectedNotification = mockkRoomGroupMessageCreator.givenCreatesRoomMessageFor(
MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
withRedactedRemoved,
A_ROOM_ID,

View file

@ -22,8 +22,8 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationFactory
import io.element.android.libraries.push.impl.notifications.fake.MockkNotificationDisplayer
import io.element.android.libraries.push.impl.notifications.fake.MockkNotificationFactory
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
@ -51,14 +51,14 @@ private val ONE_SHOT_META = OneShotNotification.Append.Meta(key = "ignored", sum
@RunWith(RobolectricTestRunner::class)
class NotificationRendererTest {
private val notificationDisplayer = FakeNotificationDisplayer()
private val notificationFactory = FakeNotificationFactory()
private val mockkNotificationDisplayer = MockkNotificationDisplayer()
private val mockkNotificationFactory = MockkNotificationFactory()
private val notificationIdProvider = NotificationIdProvider()
private val notificationRenderer = NotificationRenderer(
notificationIdProvider = notificationIdProvider,
notificationDisplayer = notificationDisplayer.instance,
notificationFactory = notificationFactory.instance,
notificationDisplayer = mockkNotificationDisplayer.instance,
notificationFactory = mockkNotificationFactory.instance,
)
@Test
@ -67,8 +67,8 @@ class NotificationRendererTest {
renderEventsAsNotifications()
notificationDisplayer.verifySummaryCancelled()
notificationDisplayer.verifyNoOtherInteractions()
mockkNotificationDisplayer.verifySummaryCancelled()
mockkNotificationDisplayer.verifyNoOtherInteractions()
}
@Test
@ -77,7 +77,7 @@ class NotificationRendererTest {
renderEventsAsNotifications()
notificationDisplayer.verifyInOrder {
mockkNotificationDisplayer.verifyInOrder {
cancelNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID))
cancelNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID))
}
@ -89,7 +89,7 @@ class NotificationRendererTest {
renderEventsAsNotifications()
notificationDisplayer.verifyInOrder {
mockkNotificationDisplayer.verifyInOrder {
cancelNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID))
showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification)
}
@ -108,7 +108,7 @@ class NotificationRendererTest {
renderEventsAsNotifications()
notificationDisplayer.verifyInOrder {
mockkNotificationDisplayer.verifyInOrder {
showNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), A_NOTIFICATION)
showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification)
}
@ -120,7 +120,7 @@ class NotificationRendererTest {
renderEventsAsNotifications()
notificationDisplayer.verifyInOrder {
mockkNotificationDisplayer.verifyInOrder {
cancelNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID))
cancelNotificationMessage(tag = AN_EVENT_ID.value, notificationIdProvider.getRoomEventNotificationId(A_SESSION_ID))
}
@ -132,7 +132,7 @@ class NotificationRendererTest {
renderEventsAsNotifications()
notificationDisplayer.verifyInOrder {
mockkNotificationDisplayer.verifyInOrder {
cancelNotificationMessage(tag = AN_EVENT_ID.value, notificationIdProvider.getRoomEventNotificationId(A_SESSION_ID))
showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification)
}
@ -151,7 +151,7 @@ class NotificationRendererTest {
renderEventsAsNotifications()
notificationDisplayer.verifyInOrder {
mockkNotificationDisplayer.verifyInOrder {
showNotificationMessage(tag = AN_EVENT_ID.value, notificationIdProvider.getRoomEventNotificationId(A_SESSION_ID), A_NOTIFICATION)
showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification)
}
@ -163,7 +163,7 @@ class NotificationRendererTest {
renderEventsAsNotifications()
notificationDisplayer.verifyInOrder {
mockkNotificationDisplayer.verifyInOrder {
cancelNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID))
cancelNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID))
}
@ -175,7 +175,7 @@ class NotificationRendererTest {
renderEventsAsNotifications()
notificationDisplayer.verifyInOrder {
mockkNotificationDisplayer.verifyInOrder {
cancelNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID))
showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification)
}
@ -194,7 +194,7 @@ class NotificationRendererTest {
renderEventsAsNotifications()
notificationDisplayer.verifyInOrder {
mockkNotificationDisplayer.verifyInOrder {
showNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomEventNotificationId(A_SESSION_ID), A_NOTIFICATION)
showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification)
}
@ -221,7 +221,7 @@ class NotificationRendererTest {
useCompleteNotificationFormat: Boolean = USE_COMPLETE_NOTIFICATION_FORMAT,
summaryNotification: SummaryNotification = A_SUMMARY_NOTIFICATION
) {
notificationFactory.givenNotificationsFor(
mockkNotificationFactory.givenNotificationsFor(
groupedEvents = A_PROCESSED_EVENTS,
matrixUser = MatrixUser(A_SESSION_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR_URL),
useCompleteNotificationFormat = useCompleteNotificationFormat,

View file

@ -23,7 +23,7 @@ import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiab
import io.mockk.every
import io.mockk.mockk
class FakeAndroidNotificationFactory {
class MockkNotificationCreator {
val instance = mockk<NotificationCreator>()
fun givenCreateRoomInvitationNotificationFor(event: InviteNotifiableEvent): Notification {
@ -37,4 +37,10 @@ class FakeAndroidNotificationFactory {
every { instance.createSimpleEventNotification(event) } returns mockNotification
return mockNotification
}
fun givenCreateDiagnosticNotification(): Notification {
val mockNotification = mockk<Notification>()
every { instance.createDiagnosticNotification() } returns mockNotification
return mockNotification
}
}

View file

@ -20,13 +20,18 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.push.impl.notifications.NotificationDisplayer
import io.element.android.libraries.push.impl.notifications.NotificationIdProvider
import io.mockk.confirmVerified
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import io.mockk.verifyOrder
class FakeNotificationDisplayer {
class MockkNotificationDisplayer {
val instance = mockk<NotificationDisplayer>(relaxed = true)
fun givenDisplayDiagnosticNotificationResult(result: Boolean) {
every { instance.displayDiagnosticNotification(any()) } returns result
}
fun verifySummaryCancelled() {
verify { instance.cancelNotificationMessage(tag = null, NotificationIdProvider().getSummaryNotificationId(A_SESSION_ID)) }
}

View file

@ -26,7 +26,7 @@ import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
class FakeNotificationFactory {
class MockkNotificationFactory {
val instance = mockk<NotificationFactory>()
fun givenNotificationsFor(

View file

@ -21,7 +21,7 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableEven
import io.mockk.every
import io.mockk.mockk
class FakeOutdatedEventDetector {
class MockkOutdatedEventDetector {
val instance = mockk<OutdatedEventDetector>()
fun givenEventIsOutOfDate(notifiableEvent: NotifiableEvent) {

View file

@ -24,7 +24,7 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableMess
import io.mockk.coEvery
import io.mockk.mockk
class FakeRoomGroupMessageCreator {
class MockkRoomGroupMessageCreator {
val instance = mockk<RoomGroupMessageCreator>()
fun givenCreatesRoomMessageFor(

View file

@ -19,6 +19,6 @@ package io.element.android.libraries.push.impl.notifications.fake
import io.element.android.libraries.push.impl.notifications.SummaryGroupMessageCreator
import io.mockk.mockk
class FakeSummaryGroupMessageCreator {
class MockkSummaryGroupMessageCreator {
val instance = mockk<SummaryGroupMessageCreator>()
}

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2024 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 io.element.android.libraries.push.impl.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.push.test.FakeGetCurrentPushProvider
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Test
class CurrentPushProviderTestTest {
@Test
fun `test CurrentPushProviderTest with a push provider`() = runTest {
val sut = CurrentPushProviderTest(
getCurrentPushProvider = FakeGetCurrentPushProvider("foo"),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
assertThat(lastItem.description).contains("foo")
}
}
@Test
fun `test CurrentPushProviderTest without push provider`() = runTest {
val sut = CurrentPushProviderTest(
getCurrentPushProvider = FakeGetCurrentPushProvider(null),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false))
}
}
}

View file

@ -0,0 +1,90 @@
/*
* Copyright (c) 2024 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 io.element.android.libraries.push.impl.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.push.impl.notifications.fake.MockkNotificationCreator
import io.element.android.libraries.push.impl.notifications.fake.MockkNotificationDisplayer
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Test
class NotificationTestTest {
private val mockkNotificationCreator = MockkNotificationCreator().apply {
givenCreateDiagnosticNotification()
}
private val mockkNotificationDisplayer = MockkNotificationDisplayer().apply {
givenDisplayDiagnosticNotificationResult(true)
}
private val notificationClickHandler = NotificationClickHandler()
@Test
fun `test NotificationTest notification cannot be displayed`() = runTest {
mockkNotificationDisplayer.givenDisplayDiagnosticNotificationResult(false)
val sut = createNotificationTest()
launch {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
assertThat(awaitItem().status).isInstanceOf(NotificationTroubleshootTestState.Status.Failure::class.java)
}
}
@Test
fun `test NotificationTest user does not click on notification`() = runTest {
val sut = createNotificationTest()
launch {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.WaitingForUser)
assertThat(awaitItem().status).isInstanceOf(NotificationTroubleshootTestState.Status.Failure::class.java)
}
}
@Test
fun `test NotificationTest user clicks on notification`() = runTest {
val sut = createNotificationTest()
launch {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.WaitingForUser)
notificationClickHandler.handleNotificationClick()
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
}
}
private fun createNotificationTest(): NotificationTest {
return NotificationTest(
notificationCreator = mockkNotificationCreator.instance,
notificationDisplayer = mockkNotificationDisplayer.instance,
notificationClickHandler = notificationClickHandler,
stringProvider = FakeStringProvider(),
)
}
}

View file

@ -0,0 +1,146 @@
/*
* Copyright (c) 2024 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 io.element.android.libraries.push.impl.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_FAILURE_REASON
import io.element.android.libraries.push.api.gateway.PushGatewayFailure
import io.element.android.libraries.push.test.FakePushService
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Test
class PushLoopbackTestTest {
@Test
fun `test PushLoopbackTest timeout - push is not received`() = runTest {
val diagnosticPushHandler = DiagnosticPushHandler()
val sut = PushLoopbackTest(
pushService = FakePushService(),
diagnosticPushHandler = diagnosticPushHandler,
clock = FakeSystemClock(),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false))
}
}
@Test
fun `test PushLoopbackTest PusherRejected error`() = runTest {
val diagnosticPushHandler = DiagnosticPushHandler()
val sut = PushLoopbackTest(
pushService = FakePushService(
testPushBlock = {
throw PushGatewayFailure.PusherRejected()
}
),
diagnosticPushHandler = diagnosticPushHandler,
clock = FakeSystemClock(),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false))
}
}
@Test
fun `test PushLoopbackTest setup error`() = runTest {
val diagnosticPushHandler = DiagnosticPushHandler()
val sut = PushLoopbackTest(
pushService = FakePushService(
testPushBlock = { false }
),
diagnosticPushHandler = diagnosticPushHandler,
clock = FakeSystemClock(),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false))
}
}
@Test
fun `test PushLoopbackTest other error`() = runTest {
val diagnosticPushHandler = DiagnosticPushHandler()
val sut = PushLoopbackTest(
pushService = FakePushService(
testPushBlock = {
throw AN_EXCEPTION
}
),
diagnosticPushHandler = diagnosticPushHandler,
clock = FakeSystemClock(),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false))
assertThat(lastItem.description).contains(A_FAILURE_REASON)
}
}
@Test
fun `test PushLoopbackTest push is received`() = runTest {
val diagnosticPushHandler = DiagnosticPushHandler()
val sut = PushLoopbackTest(
pushService = FakePushService(testPushBlock = {
diagnosticPushHandler.handlePush()
true
}),
diagnosticPushHandler = diagnosticPushHandler,
clock = FakeSystemClock(),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
}
}
}

View file

@ -0,0 +1,68 @@
/*
* Copyright (c) 2024 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 io.element.android.libraries.push.impl.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.pushproviders.test.FakePushProvider
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Test
class PushProvidersTestTest {
@Test
fun `test PushProvidersTest with empty list`() = runTest {
val sut = PushProvidersTest(
pushProviders = emptySet(),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false))
}
}
@Test
fun `test PushProvidersTest with 2 push providers`() = runTest {
val sut = PushProvidersTest(
pushProviders = setOf(
FakePushProvider(name = "foo"),
FakePushProvider(name = "bar"),
),
stringProvider = FakeStringProvider(),
)
launch {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
assertThat(lastItem.description).contains("2")
assertThat(lastItem.description).contains("foo")
assertThat(lastItem.description).contains("bar")
}
}
}

View file

@ -25,5 +25,6 @@ android {
dependencies {
api(projects.libraries.push.api)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.pushproviders.api)
implementation(projects.tests.testutils)
}

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2024 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 io.element.android.libraries.push.test
import io.element.android.libraries.push.api.GetCurrentPushProvider
class FakeGetCurrentPushProvider(
private val currentPushProvider: String?
) : GetCurrentPushProvider {
override suspend fun getCurrentPushProvider(): String? = currentPushProvider
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2024 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 io.element.android.libraries.push.test
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider
import io.element.android.tests.testutils.simulateLongTask
class FakePushService(
private val testPushBlock: suspend () -> Boolean = { true }
) : PushService {
override fun notificationStyleChanged() {
}
override fun getAvailablePushProviders(): List<PushProvider> {
return emptyList()
}
override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) {
}
override suspend fun testPush(): Boolean = simulateLongTask {
testPushBlock()
}
}

View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2024 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 io.element.android.libraries.pushproviders.api
data class CurrentUserPushConfig(
val url: String,
val pushKey: String,
)

View file

@ -49,8 +49,5 @@ interface PushProvider {
*/
suspend fun unregister(matrixClient: MatrixClient)
/**
* Attempt to troubleshoot the push provider.
*/
suspend fun troubleshoot(): Result<Unit>
suspend fun getCurrentUserPushConfig(): CurrentUserPushConfig?
}

View file

@ -40,6 +40,9 @@ dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.di)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.troubleshoot.api)
implementation(projects.services.toolbox.api)
implementation(projects.libraries.pushstore.api)
implementation(projects.libraries.pushproviders.api)
@ -51,8 +54,11 @@ dependencies {
exclude(group = "com.google.firebase", module = "firebase-measurement-connector")
}
testImplementation(libs.coroutines.test)
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.tests.testutils)
testImplementation(projects.services.toolbox.test)
}

View file

@ -44,7 +44,7 @@ class FirebaseNewTokenHandler @Inject constructor(
sessionStore.getAllSessions().toUserList()
.map { SessionId(it) }
.forEach { userId ->
val userDataStore = userPushStoreFactory.create(userId)
val userDataStore = userPushStoreFactory.getOrCreate(userId)
if (userDataStore.getPushProviderName() == FirebaseConfig.NAME) {
matrixAuthenticationService.restoreSession(userId).getOrNull()?.use { client ->
pusherSubscriber.registerPusher(client, firebaseToken, FirebaseConfig.PUSHER_HTTP_URL)

Some files were not shown because too many files have changed in this diff Show more