Introduce PushHistoryService to store data about the received push (#4573)
* Introduce PushHistoryService to store data about the received push Add a push database. * Update screenshots * Improve preview. * Update screenshots * Add missing test. * Add test for PushHistoryView * Fix configuration issue. Was: w: /libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryPresenterTest.kt:35:27 Cannot access class 'PushProvider' in the expression type. While it may work, this case indicates a configuration mistake and can lead to avoidable compilation errors, so it may be forbidden soon. Check your module classpath for missing or conflicting dependencies. --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
parent
2b1a66ff37
commit
c7f0566dc1
60 changed files with 1656 additions and 214 deletions
|
|
@ -70,6 +70,7 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
|
|
@ -378,6 +379,14 @@ class LoggedInFlowNode @AssistedInject constructor(
|
|||
override fun onOpenRoomNotificationSettings(roomId: RoomId) {
|
||||
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.NotificationSettings))
|
||||
}
|
||||
|
||||
override fun navigateTo(sessionId: SessionId, roomId: RoomId, eventId: EventId) {
|
||||
// We do not check the sessionId, but it will have to be done at some point (multi account)
|
||||
if (sessionId != matrixClient.sessionId) {
|
||||
Timber.e("SessionId mismatch, expected ${matrixClient.sessionId} but got $sessionId")
|
||||
}
|
||||
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Messages(eventId)))
|
||||
}
|
||||
}
|
||||
val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
|
||||
preferencesEntryPoint.nodeBuilder(this, buildContext)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ import com.bumble.appyx.core.node.Node
|
|||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
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 kotlinx.parcelize.Parcelize
|
||||
|
||||
interface PreferencesEntryPoint : FeatureEntryPoint {
|
||||
|
|
@ -29,6 +31,7 @@ interface PreferencesEntryPoint : FeatureEntryPoint {
|
|||
}
|
||||
|
||||
data class Params(val initialElement: InitialTarget) : NodeInputs
|
||||
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
|
||||
interface NodeBuilder {
|
||||
|
|
@ -41,5 +44,6 @@ interface PreferencesEntryPoint : FeatureEntryPoint {
|
|||
fun onOpenBugReport()
|
||||
fun onSecureBackupClick()
|
||||
fun onOpenRoomNotificationSettings(roomId: RoomId)
|
||||
fun navigateTo(sessionId: SessionId, roomId: RoomId, eventId: EventId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,9 +39,12 @@ import io.element.android.libraries.architecture.BaseFlowNode
|
|||
import io.element.android.libraries.architecture.appyx.canPop
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
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.user.MatrixUser
|
||||
import io.element.android.libraries.troubleshoot.api.NotificationTroubleShootEntryPoint
|
||||
import io.element.android.libraries.troubleshoot.api.PushHistoryEntryPoint
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
|
|
@ -50,6 +53,7 @@ class PreferencesFlowNode @AssistedInject constructor(
|
|||
@Assisted plugins: List<Plugin>,
|
||||
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
||||
private val notificationTroubleShootEntryPoint: NotificationTroubleShootEntryPoint,
|
||||
private val pushHistoryEntryPoint: PushHistoryEntryPoint,
|
||||
private val logoutEntryPoint: LogoutEntryPoint,
|
||||
private val openSourceLicensesEntryPoint: OpenSourceLicensesEntryPoint,
|
||||
private val accountDeactivationEntryPoint: AccountDeactivationEntryPoint,
|
||||
|
|
@ -83,6 +87,9 @@ class PreferencesFlowNode @AssistedInject constructor(
|
|||
@Parcelize
|
||||
data object TroubleshootNotifications : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object PushHistory : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object LockScreenSettings : NavTarget
|
||||
|
||||
|
|
@ -182,6 +189,10 @@ class PreferencesFlowNode @AssistedInject constructor(
|
|||
override fun onTroubleshootNotificationsClick() {
|
||||
backstack.push(NavTarget.TroubleshootNotifications)
|
||||
}
|
||||
|
||||
override fun onPushHistoryClick() {
|
||||
backstack.push(NavTarget.PushHistory)
|
||||
}
|
||||
}
|
||||
createNode<NotificationSettingsNode>(buildContext, listOf(notificationSettingsCallback))
|
||||
}
|
||||
|
|
@ -198,6 +209,23 @@ class PreferencesFlowNode @AssistedInject constructor(
|
|||
})
|
||||
.build()
|
||||
}
|
||||
NavTarget.PushHistory -> {
|
||||
pushHistoryEntryPoint.nodeBuilder(this, buildContext)
|
||||
.callback(object : PushHistoryEntryPoint.Callback {
|
||||
override fun onDone() {
|
||||
if (backstack.canPop()) {
|
||||
backstack.pop()
|
||||
} else {
|
||||
navigateUp()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(sessionId: SessionId, roomId: RoomId, eventId: EventId) {
|
||||
plugins<PreferencesEntryPoint.Callback>().forEach { it.navigateTo(sessionId, roomId, eventId) }
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
is NavTarget.EditDefaultNotificationSetting -> {
|
||||
val callback = object : EditDefaultNotificationSettingNode.Callback {
|
||||
override fun openRoomNotificationSettings(roomId: RoomId) {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ class NotificationSettingsNode @AssistedInject constructor(
|
|||
interface Callback : Plugin {
|
||||
fun editDefaultNotificationMode(isOneToOne: Boolean)
|
||||
fun onTroubleshootNotificationsClick()
|
||||
fun onPushHistoryClick()
|
||||
}
|
||||
|
||||
private val callbacks = plugins<Callback>()
|
||||
|
|
@ -39,6 +40,10 @@ class NotificationSettingsNode @AssistedInject constructor(
|
|||
callbacks.forEach { it.onTroubleshootNotificationsClick() }
|
||||
}
|
||||
|
||||
private fun onPushHistoryClick() {
|
||||
callbacks.forEach { it.onPushHistoryClick() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
|
|
@ -47,6 +52,7 @@ class NotificationSettingsNode @AssistedInject constructor(
|
|||
onOpenEditDefault = { openEditDefault(isOneToOne = it) },
|
||||
onBackClick = ::navigateUp,
|
||||
onTroubleshootNotificationsClick = ::onTroubleshootNotificationsClick,
|
||||
onPushHistoryClick = ::onPushHistoryClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ fun NotificationSettingsView(
|
|||
state: NotificationSettingsState,
|
||||
onOpenEditDefault: (isOneToOne: Boolean) -> Unit,
|
||||
onTroubleshootNotificationsClick: () -> Unit,
|
||||
onPushHistoryClick: () -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -82,6 +83,7 @@ fun NotificationSettingsView(
|
|||
// onCallsNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetCallNotificationsEnabled(it)) },
|
||||
onInviteForMeNotificationsChange = { state.eventSink(NotificationSettingsEvents.SetInviteForMeNotificationsEnabled(it)) },
|
||||
onTroubleshootNotificationsClick = onTroubleshootNotificationsClick,
|
||||
onPushHistoryClick = onPushHistoryClick,
|
||||
)
|
||||
}
|
||||
AsyncActionView(
|
||||
|
|
@ -105,6 +107,7 @@ private fun NotificationSettingsContentView(
|
|||
// onCallsNotificationsChanged: (Boolean) -> Unit,
|
||||
onInviteForMeNotificationsChange: (Boolean) -> Unit,
|
||||
onTroubleshootNotificationsClick: () -> Unit,
|
||||
onPushHistoryClick: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val systemSettings: NotificationSettingsState.AppSettings = state.appSettings
|
||||
|
|
@ -203,6 +206,12 @@ private fun NotificationSettingsContentView(
|
|||
},
|
||||
onClick = onTroubleshootNotificationsClick
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(stringResource(R.string.troubleshoot_notifications_entry_point_push_history_title))
|
||||
},
|
||||
onClick = onPushHistoryClick
|
||||
)
|
||||
}
|
||||
if (state.showAdvancedSettings) {
|
||||
PreferenceCategory(title = stringResource(id = CommonStrings.common_advanced_settings)) {
|
||||
|
|
@ -303,5 +312,6 @@ internal fun NotificationSettingsViewPreview(@PreviewParameter(NotificationSetti
|
|||
onBackClick = {},
|
||||
onOpenEditDefault = {},
|
||||
onTroubleshootNotificationsClick = {},
|
||||
onPushHistoryClick = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ 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_push_history_title">"Push history"</string>
|
||||
<string name="troubleshoot_notifications_entry_point_section">"Troubleshoot"</string>
|
||||
<string name="troubleshoot_notifications_entry_point_title">"Troubleshoot notifications"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -66,6 +66,22 @@ class NotificationSettingsViewTest {
|
|||
eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled)
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `clicking on push history notification invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<NotificationSettingsEvents>()
|
||||
ensureCalledOnce {
|
||||
rule.setNotificationSettingsView(
|
||||
state = aValidNotificationSettingsState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onPushHistoryClick = it
|
||||
)
|
||||
rule.clickOn(R.string.troubleshoot_notifications_entry_point_push_history_title)
|
||||
}
|
||||
eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled)
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `clicking on group chats invokes the expected callback`() {
|
||||
|
|
@ -284,6 +300,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setNotif
|
|||
state: NotificationSettingsState,
|
||||
onOpenEditDefault: (isOneToOne: Boolean) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onTroubleshootNotificationsClick: () -> Unit = EnsureNeverCalled(),
|
||||
onPushHistoryClick: () -> Unit = EnsureNeverCalled(),
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
|
|
@ -291,6 +308,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setNotif
|
|||
state = state,
|
||||
onOpenEditDefault = onOpenEditDefault,
|
||||
onTroubleshootNotificationsClick = onTroubleshootNotificationsClick,
|
||||
onPushHistoryClick = onPushHistoryClick,
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,3 +87,4 @@ const val A_RECOVERY_KEY = "1234 5678"
|
|||
val A_SERVER_LIST = listOf("server1", "server2")
|
||||
|
||||
const val A_TIMESTAMP = 567L
|
||||
const val A_FORMATTED_DATE = "April 6, 1980 at 6:35 PM"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ package io.element.android.libraries.push.api
|
|||
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
import io.element.android.libraries.pushproviders.api.PushProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -51,4 +52,19 @@ interface PushService {
|
|||
* Return false in case of early error.
|
||||
*/
|
||||
suspend fun testPush(): Boolean
|
||||
|
||||
/**
|
||||
* Get a flow of total number of received Push.
|
||||
*/
|
||||
val pushCounter: Flow<Int>
|
||||
|
||||
/**
|
||||
* Get a flow of list of [PushHistoryItem].
|
||||
*/
|
||||
fun getPushHistoryItemsFlow(): Flow<List<PushHistoryItem>>
|
||||
|
||||
/**
|
||||
* Reset the push history, including the push counter.
|
||||
*/
|
||||
suspend fun resetPushHistory()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.api.history
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
* Data class representing a push history item.
|
||||
* @property pushDate Date (timestamp).
|
||||
* @property formattedDate Formatted date.
|
||||
* @property providerInfo Push provider name / info
|
||||
* @property eventId EventId from the push, can be null if the received data are not correct.
|
||||
* @property roomId RoomId from the push, can be null if the received data are not correct.
|
||||
* @property sessionId The session Id, can be null if the session cannot be retrieved
|
||||
* @property hasBeenResolved Result of resolving the event
|
||||
* @property comment Comment. Can contains an error message if the event could not be resolved, or other any information.
|
||||
*/
|
||||
data class PushHistoryItem(
|
||||
val pushDate: Long,
|
||||
val formattedDate: String,
|
||||
val providerInfo: String,
|
||||
val eventId: EventId?,
|
||||
val roomId: RoomId?,
|
||||
val sessionId: SessionId?,
|
||||
val hasBeenResolved: Boolean,
|
||||
val comment: String?,
|
||||
)
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.api.store
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface PushDataStore {
|
||||
val pushCounterFlow: Flow<Int>
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import extension.setupAnvil
|
|||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.sqldelight)
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
@ -32,9 +33,16 @@ dependencies {
|
|||
implementation(libs.serialization.json)
|
||||
implementation(libs.coil)
|
||||
|
||||
implementation(libs.sqldelight.driver.android)
|
||||
implementation(libs.sqlcipher)
|
||||
implementation(libs.sqlite)
|
||||
implementation(libs.sqldelight.coroutines)
|
||||
implementation(projects.libraries.encryptedDb)
|
||||
|
||||
implementation(projects.appconfig)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.dateformatter.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(projects.libraries.androidutils)
|
||||
|
|
@ -76,3 +84,11 @@ dependencies {
|
|||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(libs.kotlinx.collections.immutable)
|
||||
}
|
||||
|
||||
sqldelight {
|
||||
databases {
|
||||
create("PushDatabase") {
|
||||
schemaOutputDirectory = File("src/main/sqldelight/databases")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
|||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.push.api.GetCurrentPushProvider
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||
import io.element.android.libraries.push.impl.store.PushDataStore
|
||||
import io.element.android.libraries.push.impl.test.TestPush
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
import io.element.android.libraries.pushproviders.api.PushProvider
|
||||
|
|
@ -34,6 +36,7 @@ class DefaultPushService @Inject constructor(
|
|||
private val getCurrentPushProvider: GetCurrentPushProvider,
|
||||
private val sessionObserver: SessionObserver,
|
||||
private val pushClientSecretStore: PushClientSecretStore,
|
||||
private val pushDataStore: PushDataStore,
|
||||
) : PushService, SessionListener {
|
||||
init {
|
||||
observeSessions()
|
||||
|
|
@ -125,4 +128,14 @@ class DefaultPushService @Inject constructor(
|
|||
pushClientSecretStore.resetSecret(sessionId)
|
||||
userPushStore.reset()
|
||||
}
|
||||
|
||||
override val pushCounter: Flow<Int> = pushDataStore.pushCounterFlow
|
||||
|
||||
override fun getPushHistoryItemsFlow(): Flow<List<PushHistoryItem>> {
|
||||
return pushDataStore.getPushHistoryItemsFlow()
|
||||
}
|
||||
|
||||
override suspend fun resetPushHistory() {
|
||||
pushDataStore.reset()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.history
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
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.push.impl.PushDatabase
|
||||
import io.element.android.libraries.push.impl.db.PushHistory
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultPushHistoryService @Inject constructor(
|
||||
private val pushDatabase: PushDatabase,
|
||||
private val systemClock: SystemClock,
|
||||
) : PushHistoryService {
|
||||
override fun onPushReceived(
|
||||
providerInfo: String,
|
||||
eventId: EventId?,
|
||||
roomId: RoomId?,
|
||||
sessionId: SessionId?,
|
||||
hasBeenResolved: Boolean,
|
||||
comment: String?,
|
||||
) {
|
||||
pushDatabase.pushHistoryQueries.insertPushHistory(
|
||||
PushHistory(
|
||||
pushDate = systemClock.epochMillis(),
|
||||
providerInfo = providerInfo,
|
||||
eventId = eventId?.value,
|
||||
roomId = roomId?.value,
|
||||
sessionId = sessionId?.value,
|
||||
hasBeenResolved = if (hasBeenResolved) 1 else 0,
|
||||
comment = comment,
|
||||
)
|
||||
)
|
||||
|
||||
// Keep only the last 100 events
|
||||
pushDatabase.pushHistoryQueries.removeOldest(100)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.history
|
||||
|
||||
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
|
||||
|
||||
interface PushHistoryService {
|
||||
/**
|
||||
* Create a new push history entry.
|
||||
* Do not use directly, prefer using the extension functions.
|
||||
*/
|
||||
fun onPushReceived(
|
||||
providerInfo: String,
|
||||
eventId: EventId?,
|
||||
roomId: RoomId?,
|
||||
sessionId: SessionId?,
|
||||
hasBeenResolved: Boolean,
|
||||
comment: String?,
|
||||
)
|
||||
}
|
||||
|
||||
fun PushHistoryService.onInvalidPushReceived(
|
||||
providerInfo: String,
|
||||
) = onPushReceived(
|
||||
providerInfo = providerInfo,
|
||||
eventId = null,
|
||||
roomId = null,
|
||||
sessionId = null,
|
||||
hasBeenResolved = false,
|
||||
comment = "Invalid push data",
|
||||
)
|
||||
|
||||
fun PushHistoryService.onUnableToRetrieveSession(
|
||||
providerInfo: String,
|
||||
eventId: EventId,
|
||||
roomId: RoomId,
|
||||
reason: String,
|
||||
) = onPushReceived(
|
||||
providerInfo = providerInfo,
|
||||
eventId = eventId,
|
||||
roomId = roomId,
|
||||
sessionId = null,
|
||||
hasBeenResolved = false,
|
||||
comment = "Unable to retrieve session: $reason",
|
||||
)
|
||||
|
||||
fun PushHistoryService.onUnableToResolveEvent(
|
||||
providerInfo: String,
|
||||
eventId: EventId,
|
||||
roomId: RoomId,
|
||||
sessionId: SessionId,
|
||||
reason: String,
|
||||
) = onPushReceived(
|
||||
providerInfo = providerInfo,
|
||||
eventId = eventId,
|
||||
roomId = roomId,
|
||||
sessionId = sessionId,
|
||||
hasBeenResolved = false,
|
||||
comment = "Unable to resolve event: $reason",
|
||||
)
|
||||
|
||||
fun PushHistoryService.onSuccess(
|
||||
providerInfo: String,
|
||||
eventId: EventId,
|
||||
roomId: RoomId,
|
||||
sessionId: SessionId,
|
||||
comment: String?,
|
||||
) = onPushReceived(
|
||||
providerInfo = providerInfo,
|
||||
eventId = eventId,
|
||||
roomId = roomId,
|
||||
sessionId = sessionId,
|
||||
hasBeenResolved = true,
|
||||
comment = buildString {
|
||||
append("Success")
|
||||
if (comment.isNullOrBlank().not()) {
|
||||
append(" - $comment")
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
fun PushHistoryService.onDiagnosticPush(
|
||||
providerInfo: String,
|
||||
) = onPushReceived(
|
||||
providerInfo = providerInfo,
|
||||
eventId = null,
|
||||
roomId = null,
|
||||
sessionId = null,
|
||||
hasBeenResolved = true,
|
||||
comment = "Diagnostic push",
|
||||
)
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.history.di
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.push.impl.PushDatabase
|
||||
import io.element.encrypteddb.SqlCipherDriverFactory
|
||||
import io.element.encrypteddb.passphrase.RandomSecretPassphraseProvider
|
||||
|
||||
@Module
|
||||
@ContributesTo(AppScope::class)
|
||||
object PushHistoryModule {
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
fun providePushDatabase(
|
||||
@ApplicationContext context: Context,
|
||||
): PushDatabase {
|
||||
val name = "push_database"
|
||||
val secretFile = context.getDatabasePath("$name.key")
|
||||
|
||||
// Make sure the parent directory of the key file exists, otherwise it will crash in older Android versions
|
||||
val parentDir = secretFile.parentFile
|
||||
if (parentDir != null && !parentDir.exists()) {
|
||||
parentDir.mkdirs()
|
||||
}
|
||||
|
||||
val passphraseProvider = RandomSecretPassphraseProvider(context, secretFile)
|
||||
val driver = SqlCipherDriverFactory(passphraseProvider)
|
||||
.create(PushDatabase.Schema, "$name.db", context)
|
||||
return PushDatabase(driver)
|
||||
}
|
||||
}
|
||||
|
|
@ -30,16 +30,26 @@ interface CallNotificationEventResolver {
|
|||
* @param forceNotify `true` to force the notification to be non-ringing, `false` to use the default behaviour. Default is `false`.
|
||||
* @return a [NotifiableEvent] if the notification data is a call notification, null otherwise
|
||||
*/
|
||||
fun resolveEvent(sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean = false): NotifiableEvent?
|
||||
fun resolveEvent(
|
||||
sessionId: SessionId,
|
||||
notificationData: NotificationData,
|
||||
forceNotify: Boolean = false,
|
||||
): Result<NotifiableEvent>
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultCallNotificationEventResolver @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
) : CallNotificationEventResolver {
|
||||
override fun resolveEvent(sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean): NotifiableEvent? {
|
||||
val content = notificationData.content as? NotificationContent.MessageLike.CallNotify ?: return null
|
||||
return notificationData.run {
|
||||
override fun resolveEvent(
|
||||
sessionId: SessionId,
|
||||
notificationData: NotificationData,
|
||||
forceNotify: Boolean
|
||||
): Result<NotifiableEvent> = runCatching {
|
||||
val content = notificationData.content as? NotificationContent.MessageLike.CallNotify
|
||||
?: throw ResolvingException("content is not a call notify")
|
||||
|
||||
notificationData.run {
|
||||
if (NotifiableRingingCallEvent.shouldRing(content.type, timestamp) && !forceNotify) {
|
||||
NotifiableRingingCallEvent(
|
||||
sessionId = sessionId,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import android.content.Context
|
|||
import android.net.Uri
|
||||
import androidx.core.content.FileProvider
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.core.extensions.flatMap
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
|
|
@ -59,7 +60,7 @@ private val loggerTag = LoggerTag("DefaultNotifiableEventResolver", LoggerTag.No
|
|||
* this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk.
|
||||
*/
|
||||
interface NotifiableEventResolver {
|
||||
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): ResolvedPushEvent?
|
||||
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): Result<ResolvedPushEvent>
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
|
|
@ -73,31 +74,39 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
private val callNotificationEventResolver: CallNotificationEventResolver,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : NotifiableEventResolver {
|
||||
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): ResolvedPushEvent? {
|
||||
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): Result<ResolvedPushEvent> {
|
||||
// Restore session
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return Result.failure(
|
||||
ResolvingException("Unable to restore session for $sessionId")
|
||||
)
|
||||
val notificationService = client.notificationService()
|
||||
val notificationData = notificationService.getNotification(
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
).onFailure {
|
||||
Timber.tag(loggerTag.value).e(it, "Unable to resolve event: $eventId.")
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
// TODO this notificationData is not always valid at the moment, sometimes the Rust SDK can't fetch the matching event
|
||||
return notificationData?.asNotifiableEvent(client, sessionId)
|
||||
return notificationData.flatMap {
|
||||
if (it == null) {
|
||||
Timber.tag(loggerTag.value).d("No notification data found for event $eventId")
|
||||
return@flatMap Result.failure(ResolvingException("Unable to resolve event"))
|
||||
} else {
|
||||
it.asNotifiableEvent(client, sessionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun NotificationData.asNotifiableEvent(
|
||||
client: MatrixClient,
|
||||
userId: SessionId,
|
||||
): ResolvedPushEvent? {
|
||||
val content = this.content
|
||||
val notifiableEvent = when (content) {
|
||||
): Result<ResolvedPushEvent> = runCatching {
|
||||
when (val content = this.content) {
|
||||
is NotificationContent.MessageLike.RoomMessage -> {
|
||||
val senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId)
|
||||
val messageBody = descriptionFromMessageContent(content, senderDisambiguatedDisplayName)
|
||||
buildNotifiableMessageEvent(
|
||||
val notifiableMessageEvent = buildNotifiableMessageEvent(
|
||||
sessionId = userId,
|
||||
senderId = content.senderId,
|
||||
roomId = roomId,
|
||||
|
|
@ -115,10 +124,11 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
senderAvatarPath = senderAvatarUrl,
|
||||
hasMentionOrReply = hasMention,
|
||||
)
|
||||
ResolvedPushEvent.Event(notifiableMessageEvent)
|
||||
}
|
||||
is NotificationContent.Invite -> {
|
||||
val senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId)
|
||||
InviteNotifiableEvent(
|
||||
val inviteNotifiableEvent = InviteNotifiableEvent(
|
||||
sessionId = userId,
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
|
|
@ -136,15 +146,16 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
// TODO check if title is needed anymore
|
||||
title = null,
|
||||
)
|
||||
ResolvedPushEvent.Event(inviteNotifiableEvent)
|
||||
}
|
||||
NotificationContent.MessageLike.CallAnswer,
|
||||
NotificationContent.MessageLike.CallCandidates,
|
||||
NotificationContent.MessageLike.CallHangup -> {
|
||||
Timber.tag(loggerTag.value).d("Ignoring notification for call ${content.javaClass.simpleName}")
|
||||
null
|
||||
throw ResolvingException("Ignoring notification for call ${content.javaClass.simpleName}")
|
||||
}
|
||||
is NotificationContent.MessageLike.CallInvite -> {
|
||||
buildNotifiableMessageEvent(
|
||||
val notifiableMessageEvent = buildNotifiableMessageEvent(
|
||||
sessionId = userId,
|
||||
senderId = content.senderId,
|
||||
roomId = roomId,
|
||||
|
|
@ -158,9 +169,11 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
roomAvatarPath = roomAvatarUrl,
|
||||
senderAvatarPath = senderAvatarUrl,
|
||||
)
|
||||
ResolvedPushEvent.Event(notifiableMessageEvent)
|
||||
}
|
||||
is NotificationContent.MessageLike.CallNotify -> {
|
||||
callNotificationEventResolver.resolveEvent(userId, this)
|
||||
val notifiableEvent = callNotificationEventResolver.resolveEvent(userId, this).getOrThrow()
|
||||
ResolvedPushEvent.Event(notifiableEvent)
|
||||
}
|
||||
NotificationContent.MessageLike.KeyVerificationAccept,
|
||||
NotificationContent.MessageLike.KeyVerificationCancel,
|
||||
|
|
@ -168,11 +181,12 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
NotificationContent.MessageLike.KeyVerificationKey,
|
||||
NotificationContent.MessageLike.KeyVerificationMac,
|
||||
NotificationContent.MessageLike.KeyVerificationReady,
|
||||
NotificationContent.MessageLike.KeyVerificationStart -> null.also {
|
||||
NotificationContent.MessageLike.KeyVerificationStart -> {
|
||||
Timber.tag(loggerTag.value).d("Ignoring notification for verification ${content.javaClass.simpleName}")
|
||||
throw ResolvingException("Ignoring notification for verification ${content.javaClass.simpleName}")
|
||||
}
|
||||
is NotificationContent.MessageLike.Poll -> {
|
||||
buildNotifiableMessageEvent(
|
||||
val notifiableEventMessage = buildNotifiableMessageEvent(
|
||||
sessionId = userId,
|
||||
senderId = content.senderId,
|
||||
roomId = roomId,
|
||||
|
|
@ -187,19 +201,35 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
roomAvatarPath = roomAvatarUrl,
|
||||
senderAvatarPath = senderAvatarUrl,
|
||||
)
|
||||
ResolvedPushEvent.Event(notifiableEventMessage)
|
||||
}
|
||||
is NotificationContent.MessageLike.ReactionContent -> null.also {
|
||||
is NotificationContent.MessageLike.ReactionContent -> {
|
||||
Timber.tag(loggerTag.value).d("Ignoring notification for reaction")
|
||||
throw ResolvingException("Ignoring notification for reaction")
|
||||
}
|
||||
NotificationContent.MessageLike.RoomEncrypted -> fallbackNotifiableEvent(userId, roomId, eventId).also {
|
||||
NotificationContent.MessageLike.RoomEncrypted -> {
|
||||
Timber.tag(loggerTag.value).w("Notification with encrypted content -> fallback")
|
||||
val fallbackNotifiableEvent = fallbackNotifiableEvent(userId, roomId, eventId)
|
||||
ResolvedPushEvent.Event(fallbackNotifiableEvent)
|
||||
}
|
||||
is NotificationContent.MessageLike.RoomRedaction -> {
|
||||
// Note: this case will be handled below
|
||||
null
|
||||
val redactedEventId = content.redactedEventId
|
||||
if (redactedEventId == null) {
|
||||
Timber.tag(loggerTag.value).d("redactedEventId is null.")
|
||||
throw ResolvingException("redactedEventId is null")
|
||||
} else {
|
||||
ResolvedPushEvent.Redaction(
|
||||
sessionId = userId,
|
||||
roomId = roomId,
|
||||
redactedEventId = redactedEventId,
|
||||
reason = content.reason,
|
||||
)
|
||||
}
|
||||
}
|
||||
NotificationContent.MessageLike.Sticker -> null.also {
|
||||
NotificationContent.MessageLike.Sticker -> {
|
||||
Timber.tag(loggerTag.value).d("Ignoring notification for sticker")
|
||||
throw ResolvingException("Ignoring notification for reaction")
|
||||
}
|
||||
is NotificationContent.StateEvent.RoomMemberContent,
|
||||
NotificationContent.StateEvent.PolicyRuleRoom,
|
||||
|
|
@ -221,29 +251,11 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
NotificationContent.StateEvent.RoomTombstone,
|
||||
NotificationContent.StateEvent.RoomTopic,
|
||||
NotificationContent.StateEvent.SpaceChild,
|
||||
NotificationContent.StateEvent.SpaceParent -> null.also {
|
||||
NotificationContent.StateEvent.SpaceParent -> {
|
||||
Timber.tag(loggerTag.value).d("Ignoring notification for state event ${content.javaClass.simpleName}")
|
||||
throw ResolvingException("Ignoring notification for state event ${content.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
|
||||
return if (notifiableEvent != null) {
|
||||
ResolvedPushEvent.Event(notifiableEvent)
|
||||
} else if (content is NotificationContent.MessageLike.RoomRedaction) {
|
||||
val redactedEventId = content.redactedEventId
|
||||
if (redactedEventId == null) {
|
||||
Timber.tag(loggerTag.value).d("redactedEventId is null.")
|
||||
null
|
||||
} else {
|
||||
ResolvedPushEvent.Redaction(
|
||||
sessionId = userId,
|
||||
roomId = roomId,
|
||||
redactedEventId = redactedEventId,
|
||||
reason = content.reason,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun fallbackNotifiableEvent(
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class DefaultOnMissedCallNotificationHandler @Inject constructor(
|
|||
notificationData = notificationData,
|
||||
// Make sure the notifiable event is not a ringing one
|
||||
forceNotify = true,
|
||||
)
|
||||
).getOrNull()
|
||||
notifiableEvent?.let { defaultNotificationDrawerManager.onNotifiableEventReceived(it) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
class ResolvingException(message: String) : Exception(message)
|
||||
|
|
@ -14,6 +14,12 @@ import io.element.android.libraries.core.log.logger.LoggerTag
|
|||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.push.impl.history.PushHistoryService
|
||||
import io.element.android.libraries.push.impl.history.onDiagnosticPush
|
||||
import io.element.android.libraries.push.impl.history.onInvalidPushReceived
|
||||
import io.element.android.libraries.push.impl.history.onSuccess
|
||||
import io.element.android.libraries.push.impl.history.onUnableToResolveEvent
|
||||
import io.element.android.libraries.push.impl.history.onUnableToRetrieveSession
|
||||
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
|
||||
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
||||
|
|
@ -43,13 +49,15 @@ class DefaultPushHandler @Inject constructor(
|
|||
private val diagnosticPushHandler: DiagnosticPushHandler,
|
||||
private val elementCallEntryPoint: ElementCallEntryPoint,
|
||||
private val notificationChannels: NotificationChannels,
|
||||
private val pushHistoryService: PushHistoryService,
|
||||
) : PushHandler {
|
||||
/**
|
||||
* Called when message is received.
|
||||
*
|
||||
* @param pushData the data received in the push.
|
||||
* @param providerInfo the provider info.
|
||||
*/
|
||||
override suspend fun handle(pushData: PushData) {
|
||||
override suspend fun handle(pushData: PushData, providerInfo: String) {
|
||||
Timber.tag(loggerTag.value).d("## handling pushData: ${pushData.roomId}/${pushData.eventId}")
|
||||
if (buildMeta.lowPrivacyLoggingEnabled) {
|
||||
Timber.tag(loggerTag.value).d("## pushData: $pushData")
|
||||
|
|
@ -57,18 +65,25 @@ class DefaultPushHandler @Inject constructor(
|
|||
incrementPushDataStore.incrementPushCounter()
|
||||
// Diagnostic Push
|
||||
if (pushData.eventId == DefaultTestPush.TEST_EVENT_ID) {
|
||||
pushHistoryService.onDiagnosticPush(providerInfo)
|
||||
diagnosticPushHandler.handlePush()
|
||||
} else {
|
||||
handleInternal(pushData)
|
||||
handleInternal(pushData, providerInfo)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun handleInvalid(providerInfo: String) {
|
||||
incrementPushDataStore.incrementPushCounter()
|
||||
pushHistoryService.onInvalidPushReceived(providerInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal receive method.
|
||||
*
|
||||
* @param pushData Object containing message data.
|
||||
* @param providerInfo the provider info.
|
||||
*/
|
||||
private suspend fun handleInternal(pushData: PushData) {
|
||||
private suspend fun handleInternal(pushData: PushData, providerInfo: String) {
|
||||
try {
|
||||
if (buildMeta.lowPrivacyLoggingEnabled) {
|
||||
Timber.tag(loggerTag.value).d("## handleInternal() : $pushData")
|
||||
|
|
@ -77,42 +92,77 @@ class DefaultPushHandler @Inject constructor(
|
|||
}
|
||||
val clientSecret = pushData.clientSecret
|
||||
// clientSecret should not be null. If this happens, restore default session
|
||||
val userId = clientSecret
|
||||
?.let {
|
||||
// Get userId from client secret
|
||||
pushClientSecret.getUserIdFromSecret(clientSecret)
|
||||
var reason = if (clientSecret == null) "No client secret" else ""
|
||||
val userId = clientSecret?.let {
|
||||
// Get userId from client secret
|
||||
pushClientSecret.getUserIdFromSecret(clientSecret).also {
|
||||
if (it == null) {
|
||||
reason = "Unable to get userId from client secret"
|
||||
}
|
||||
}
|
||||
?: run {
|
||||
matrixAuthenticationService.getLatestSessionId()
|
||||
}
|
||||
if (userId == null) {
|
||||
Timber.w("Unable to get a session")
|
||||
return
|
||||
}
|
||||
val resolvedPushEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId)
|
||||
when (resolvedPushEvent) {
|
||||
null -> Timber.tag(loggerTag.value).w("Unable to get a notification data")
|
||||
is ResolvedPushEvent.Event -> {
|
||||
when (val notifiableEvent = resolvedPushEvent.notifiableEvent) {
|
||||
is NotifiableRingingCallEvent -> {
|
||||
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
|
||||
handleRingingCallEvent(notifiableEvent)
|
||||
}
|
||||
else -> {
|
||||
val userPushStore = userPushStoreFactory.getOrCreate(userId)
|
||||
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
|
||||
if (areNotificationsEnabled) {
|
||||
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
|
||||
}
|
||||
?: run {
|
||||
matrixAuthenticationService.getLatestSessionId().also {
|
||||
if (it == null) {
|
||||
if (reason.isNotEmpty()) reason += " - "
|
||||
reason += "Unable to get latest sessionId"
|
||||
}
|
||||
}
|
||||
}
|
||||
is ResolvedPushEvent.Redaction -> {
|
||||
onRedactedEventReceived.onRedactedEventReceived(resolvedPushEvent)
|
||||
}
|
||||
if (userId == null) {
|
||||
Timber.w("Unable to get a session")
|
||||
pushHistoryService.onUnableToRetrieveSession(
|
||||
providerInfo = providerInfo,
|
||||
eventId = pushData.eventId,
|
||||
roomId = pushData.roomId,
|
||||
reason = reason,
|
||||
)
|
||||
return
|
||||
}
|
||||
notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId).fold(
|
||||
onSuccess = { resolvedPushEvent ->
|
||||
pushHistoryService.onSuccess(
|
||||
providerInfo = providerInfo,
|
||||
eventId = pushData.eventId,
|
||||
roomId = pushData.roomId,
|
||||
sessionId = userId,
|
||||
comment = resolvedPushEvent.javaClass.simpleName,
|
||||
)
|
||||
|
||||
when (resolvedPushEvent) {
|
||||
is ResolvedPushEvent.Event -> {
|
||||
when (val notifiableEvent = resolvedPushEvent.notifiableEvent) {
|
||||
is NotifiableRingingCallEvent -> {
|
||||
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
|
||||
handleRingingCallEvent(notifiableEvent)
|
||||
}
|
||||
else -> {
|
||||
val userPushStore = userPushStoreFactory.getOrCreate(userId)
|
||||
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
|
||||
if (areNotificationsEnabled) {
|
||||
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is ResolvedPushEvent.Redaction -> {
|
||||
onRedactedEventReceived.onRedactedEventReceived(resolvedPushEvent)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = { failure ->
|
||||
Timber.tag(loggerTag.value).w(failure, "Unable to get a notification data")
|
||||
pushHistoryService.onUnableToResolveEvent(
|
||||
providerInfo = providerInfo,
|
||||
eventId = pushData.eventId,
|
||||
roomId = pushData.roomId,
|
||||
sessionId = userId,
|
||||
reason = failure.message ?: failure.javaClass.simpleName,
|
||||
)
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,11 +13,20 @@ import androidx.datastore.preferences.core.Preferences
|
|||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import app.cash.sqldelight.coroutines.asFlow
|
||||
import app.cash.sqldelight.coroutines.mapToList
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.push.api.store.PushDataStore
|
||||
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.push.api.history.PushHistoryItem
|
||||
import io.element.android.libraries.push.impl.PushDatabase
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
|
@ -28,6 +37,9 @@ private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(na
|
|||
@ContributesBinding(AppScope::class)
|
||||
class DefaultPushDataStore @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val pushDatabase: PushDatabase,
|
||||
private val dateFormatter: DateFormatter,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : PushDataStore {
|
||||
private val pushCounter = intPreferencesKey("push_counter")
|
||||
|
||||
|
|
@ -41,4 +53,35 @@ class DefaultPushDataStore @Inject constructor(
|
|||
settings[pushCounter] = currentCounterValue + 1
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPushHistoryItemsFlow(): Flow<List<PushHistoryItem>> {
|
||||
return pushDatabase.pushHistoryQueries.selectAll()
|
||||
.asFlow()
|
||||
.mapToList(dispatchers.io)
|
||||
.map { items ->
|
||||
items.map { pushHistory ->
|
||||
PushHistoryItem(
|
||||
pushDate = pushHistory.pushDate,
|
||||
formattedDate = dateFormatter.format(
|
||||
timestamp = pushHistory.pushDate,
|
||||
mode = DateFormatterMode.Full,
|
||||
useRelative = false,
|
||||
),
|
||||
providerInfo = pushHistory.providerInfo,
|
||||
eventId = pushHistory.eventId?.let { EventId(it) },
|
||||
roomId = pushHistory.roomId?.let { RoomId(it) },
|
||||
sessionId = pushHistory.sessionId?.let { SessionId(it) },
|
||||
hasBeenResolved = pushHistory.hasBeenResolved == 1L,
|
||||
comment = pushHistory.comment,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun reset() {
|
||||
pushDatabase.pushHistoryQueries.removeAll()
|
||||
context.dataStore.edit {
|
||||
it.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.store
|
||||
|
||||
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface PushDataStore {
|
||||
val pushCounterFlow: Flow<Int>
|
||||
|
||||
/**
|
||||
* Get a flow of list of [PushHistoryItem].
|
||||
*/
|
||||
fun getPushHistoryItemsFlow(): Flow<List<PushHistoryItem>>
|
||||
|
||||
/**
|
||||
* Reset the push counter to 0, and clear the database.
|
||||
*/
|
||||
suspend fun reset()
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
CREATE TABLE PushHistory (
|
||||
pushDate INTEGER NOT NULL,
|
||||
providerInfo TEXT NOT NULL,
|
||||
eventId TEXT,
|
||||
roomId TEXT,
|
||||
sessionId TEXT,
|
||||
hasBeenResolved INTEGER NOT NULL,
|
||||
comment TEXT
|
||||
);
|
||||
|
||||
selectAll:
|
||||
SELECT * FROM PushHistory ORDER BY pushDate DESC;
|
||||
|
||||
insertPushHistory:
|
||||
INSERT INTO PushHistory VALUES ?;
|
||||
|
||||
removeAll:
|
||||
DELETE FROM PushHistory;
|
||||
|
||||
-- add query to keep only the last x entries
|
||||
removeOldest:
|
||||
DELETE FROM PushHistory WHERE rowid NOT IN (SELECT rowid FROM PushHistory ORDER BY pushDate DESC LIMIT ?);
|
||||
|
|
@ -14,6 +14,8 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
|||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.push.api.GetCurrentPushProvider
|
||||
import io.element.android.libraries.push.impl.store.InMemoryPushDataStore
|
||||
import io.element.android.libraries.push.impl.store.PushDataStore
|
||||
import io.element.android.libraries.push.impl.test.FakeTestPush
|
||||
import io.element.android.libraries.push.impl.test.TestPush
|
||||
import io.element.android.libraries.push.test.FakeGetCurrentPushProvider
|
||||
|
|
@ -288,6 +290,7 @@ class DefaultPushServiceTest {
|
|||
getCurrentPushProvider: GetCurrentPushProvider = FakeGetCurrentPushProvider(currentPushProvider = null),
|
||||
sessionObserver: SessionObserver = NoOpSessionObserver(),
|
||||
pushClientSecretStore: PushClientSecretStore = InMemoryPushClientSecretStore(),
|
||||
pushDataStore: PushDataStore = InMemoryPushDataStore(),
|
||||
): DefaultPushService {
|
||||
return DefaultPushService(
|
||||
testPush = testPush,
|
||||
|
|
@ -296,6 +299,7 @@ class DefaultPushServiceTest {
|
|||
getCurrentPushProvider = getCurrentPushProvider,
|
||||
sessionObserver = sessionObserver,
|
||||
pushClientSecretStore = pushClientSecretStore,
|
||||
pushDataStore = pushDataStore,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.history
|
||||
|
||||
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.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakePushHistoryService(
|
||||
private val onPushReceivedResult: (
|
||||
String,
|
||||
EventId?,
|
||||
RoomId?,
|
||||
SessionId?,
|
||||
Boolean,
|
||||
String?
|
||||
) -> Unit = { _, _, _, _, _, _ -> lambdaError() }
|
||||
) : PushHistoryService {
|
||||
override fun onPushReceived(
|
||||
providerInfo: String,
|
||||
eventId: EventId?,
|
||||
roomId: RoomId?,
|
||||
sessionId: SessionId?,
|
||||
hasBeenResolved: Boolean,
|
||||
comment: String?,
|
||||
) {
|
||||
onPushReceivedResult(
|
||||
providerInfo,
|
||||
eventId,
|
||||
roomId,
|
||||
sessionId,
|
||||
hasBeenResolved,
|
||||
comment
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -69,7 +69,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
fun `resolve event no session`() = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(notificationService = null)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result).isNull()
|
||||
assertThat(result.isFailure).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -78,7 +78,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
notificationResult = Result.failure(AN_EXCEPTION)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result).isNull()
|
||||
assertThat(result.isFailure).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -87,7 +87,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
notificationResult = Result.success(null)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result).isNull()
|
||||
assertThat(result.isFailure).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -106,7 +106,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Hello world")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -127,7 +127,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Hello world", hasMentionOrReply = true)
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -152,7 +152,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Hello world")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -177,7 +177,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Hello world")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -196,7 +196,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Audio")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -215,7 +215,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Video")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -234,7 +234,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Voice message")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -253,7 +253,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Image")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -272,7 +272,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Sticker")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -291,7 +291,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "File")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -310,7 +310,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Location")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -329,7 +329,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Notice")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -348,7 +348,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "* Bob is happy")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -367,7 +367,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
val expectedResult = ResolvedPushEvent.Event(
|
||||
aNotifiableMessageEvent(body = "Poll: A question")
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -384,7 +384,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result).isNull()
|
||||
assertThat(result.getOrNull()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -418,7 +418,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
isUpdated = false,
|
||||
)
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -452,7 +452,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
isUpdated = false,
|
||||
)
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -487,7 +487,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
isUpdated = false,
|
||||
)
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -522,7 +522,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
isUpdated = false,
|
||||
)
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -538,7 +538,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result).isNull()
|
||||
assertThat(result.getOrNull()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -564,7 +564,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
timestamp = A_FAKE_TIMESTAMP,
|
||||
)
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -602,7 +602,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
isUpdated = false
|
||||
)
|
||||
)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -638,7 +638,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -675,7 +675,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -711,7 +711,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -733,7 +733,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
reason = A_REDACTION_REASON,
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result).isEqualTo(expectedResult)
|
||||
assertThat(result.getOrNull()).isEqualTo(expectedResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -749,46 +749,46 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result).isNull()
|
||||
assertThat(result.isFailure).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve null cases`() {
|
||||
testNull(NotificationContent.MessageLike.CallAnswer)
|
||||
testNull(NotificationContent.MessageLike.CallHangup)
|
||||
testNull(NotificationContent.MessageLike.CallCandidates)
|
||||
testNull(NotificationContent.MessageLike.KeyVerificationReady)
|
||||
testNull(NotificationContent.MessageLike.KeyVerificationStart)
|
||||
testNull(NotificationContent.MessageLike.KeyVerificationCancel)
|
||||
testNull(NotificationContent.MessageLike.KeyVerificationAccept)
|
||||
testNull(NotificationContent.MessageLike.KeyVerificationKey)
|
||||
testNull(NotificationContent.MessageLike.KeyVerificationMac)
|
||||
testNull(NotificationContent.MessageLike.KeyVerificationDone)
|
||||
testNull(NotificationContent.MessageLike.ReactionContent(relatedEventId = AN_EVENT_ID_2.value))
|
||||
testNull(NotificationContent.MessageLike.Sticker)
|
||||
testNull(NotificationContent.StateEvent.PolicyRuleRoom)
|
||||
testNull(NotificationContent.StateEvent.PolicyRuleServer)
|
||||
testNull(NotificationContent.StateEvent.PolicyRuleUser)
|
||||
testNull(NotificationContent.StateEvent.RoomAliases)
|
||||
testNull(NotificationContent.StateEvent.RoomAvatar)
|
||||
testNull(NotificationContent.StateEvent.RoomCanonicalAlias)
|
||||
testNull(NotificationContent.StateEvent.RoomCreate)
|
||||
testNull(NotificationContent.StateEvent.RoomEncryption)
|
||||
testNull(NotificationContent.StateEvent.RoomGuestAccess)
|
||||
testNull(NotificationContent.StateEvent.RoomHistoryVisibility)
|
||||
testNull(NotificationContent.StateEvent.RoomJoinRules)
|
||||
testNull(NotificationContent.StateEvent.RoomName)
|
||||
testNull(NotificationContent.StateEvent.RoomPinnedEvents)
|
||||
testNull(NotificationContent.StateEvent.RoomPowerLevels)
|
||||
testNull(NotificationContent.StateEvent.RoomServerAcl)
|
||||
testNull(NotificationContent.StateEvent.RoomThirdPartyInvite)
|
||||
testNull(NotificationContent.StateEvent.RoomTombstone)
|
||||
testNull(NotificationContent.StateEvent.RoomTopic)
|
||||
testNull(NotificationContent.StateEvent.SpaceChild)
|
||||
testNull(NotificationContent.StateEvent.SpaceParent)
|
||||
testFailure(NotificationContent.MessageLike.CallAnswer)
|
||||
testFailure(NotificationContent.MessageLike.CallHangup)
|
||||
testFailure(NotificationContent.MessageLike.CallCandidates)
|
||||
testFailure(NotificationContent.MessageLike.KeyVerificationReady)
|
||||
testFailure(NotificationContent.MessageLike.KeyVerificationStart)
|
||||
testFailure(NotificationContent.MessageLike.KeyVerificationCancel)
|
||||
testFailure(NotificationContent.MessageLike.KeyVerificationAccept)
|
||||
testFailure(NotificationContent.MessageLike.KeyVerificationKey)
|
||||
testFailure(NotificationContent.MessageLike.KeyVerificationMac)
|
||||
testFailure(NotificationContent.MessageLike.KeyVerificationDone)
|
||||
testFailure(NotificationContent.MessageLike.ReactionContent(relatedEventId = AN_EVENT_ID_2.value))
|
||||
testFailure(NotificationContent.MessageLike.Sticker)
|
||||
testFailure(NotificationContent.StateEvent.PolicyRuleRoom)
|
||||
testFailure(NotificationContent.StateEvent.PolicyRuleServer)
|
||||
testFailure(NotificationContent.StateEvent.PolicyRuleUser)
|
||||
testFailure(NotificationContent.StateEvent.RoomAliases)
|
||||
testFailure(NotificationContent.StateEvent.RoomAvatar)
|
||||
testFailure(NotificationContent.StateEvent.RoomCanonicalAlias)
|
||||
testFailure(NotificationContent.StateEvent.RoomCreate)
|
||||
testFailure(NotificationContent.StateEvent.RoomEncryption)
|
||||
testFailure(NotificationContent.StateEvent.RoomGuestAccess)
|
||||
testFailure(NotificationContent.StateEvent.RoomHistoryVisibility)
|
||||
testFailure(NotificationContent.StateEvent.RoomJoinRules)
|
||||
testFailure(NotificationContent.StateEvent.RoomName)
|
||||
testFailure(NotificationContent.StateEvent.RoomPinnedEvents)
|
||||
testFailure(NotificationContent.StateEvent.RoomPowerLevels)
|
||||
testFailure(NotificationContent.StateEvent.RoomServerAcl)
|
||||
testFailure(NotificationContent.StateEvent.RoomThirdPartyInvite)
|
||||
testFailure(NotificationContent.StateEvent.RoomTombstone)
|
||||
testFailure(NotificationContent.StateEvent.RoomTopic)
|
||||
testFailure(NotificationContent.StateEvent.SpaceChild)
|
||||
testFailure(NotificationContent.StateEvent.SpaceParent)
|
||||
}
|
||||
|
||||
private fun testNull(content: NotificationContent) = runTest {
|
||||
private fun testFailure(content: NotificationContent) = runTest {
|
||||
val sut = createDefaultNotifiableEventResolver(
|
||||
notificationResult = Result.success(
|
||||
aNotificationData(
|
||||
|
|
@ -797,7 +797,7 @@ class DefaultNotifiableEventResolverTest {
|
|||
)
|
||||
)
|
||||
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
|
||||
assertThat(result).isNull()
|
||||
assertThat(result.isFailure).isTrue()
|
||||
}
|
||||
|
||||
private fun createDefaultNotifiableEventResolver(
|
||||
|
|
|
|||
|
|
@ -60,7 +60,9 @@ class DefaultOnMissedCallNotificationHandlerTest {
|
|||
imageLoaderHolder = FakeImageLoaderHolder(),
|
||||
activeNotificationsProvider = FakeActiveNotificationsProvider(),
|
||||
),
|
||||
callNotificationEventResolver = FakeCallNotificationEventResolver(resolveEventLambda = { _, _, _ -> aNotifiableMessageEvent() }),
|
||||
callNotificationEventResolver = FakeCallNotificationEventResolver(resolveEventLambda = { _, _, _ ->
|
||||
Result.success(aNotifiableMessageEvent())
|
||||
}),
|
||||
)
|
||||
|
||||
defaultOnMissedCallNotificationHandler.addMissedCallNotification(
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEv
|
|||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeNotifiableEventResolver(
|
||||
private val notifiableEventResult: (SessionId, RoomId, EventId) -> ResolvedPushEvent? = { _, _, _ -> lambdaError() }
|
||||
private val notifiableEventResult: (SessionId, RoomId, EventId) -> Result<ResolvedPushEvent> = { _, _, _ -> lambdaError() }
|
||||
) : NotifiableEventResolver {
|
||||
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): ResolvedPushEvent? {
|
||||
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): Result<ResolvedPushEvent> {
|
||||
return notifiableEventResult(sessionId, roomId, eventId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,10 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
|
|||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.push.impl.history.FakePushHistoryService
|
||||
import io.element.android.libraries.push.impl.history.PushHistoryService
|
||||
import io.element.android.libraries.push.impl.notifications.FakeNotifiableEventResolver
|
||||
import io.element.android.libraries.push.impl.notifications.ResolvingException
|
||||
import io.element.android.libraries.push.impl.notifications.channels.FakeNotificationChannels
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableCallEvent
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
||||
|
|
@ -42,6 +45,7 @@ import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
|||
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStore
|
||||
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory
|
||||
import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.FakePushClientSecret
|
||||
import io.element.android.tests.testutils.lambda.any
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
|
|
@ -50,16 +54,41 @@ import kotlinx.coroutines.test.runTest
|
|||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
|
||||
private const val A_PUSHER_INFO = "info"
|
||||
|
||||
class DefaultPushHandlerTest {
|
||||
@Test
|
||||
fun `check handleInvalid behavior`() = runTest {
|
||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
||||
val pushHistoryService = FakePushHistoryService(
|
||||
onPushReceivedResult = onPushReceivedResult,
|
||||
)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
incrementPushCounterResult = incrementPushCounterResult,
|
||||
pushHistoryService = pushHistoryService,
|
||||
)
|
||||
defaultPushHandler.handleInvalid(A_PUSHER_INFO)
|
||||
incrementPushCounterResult.assertions()
|
||||
.isCalledOnce()
|
||||
onPushReceivedResult.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_PUSHER_INFO), value(null), value(null), value(null), value(false), value("Invalid push data"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when classical PushData is received, the notification drawer is informed`() = runTest {
|
||||
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
||||
val notifiableEventResult =
|
||||
lambdaRecorder<SessionId, RoomId, EventId, ResolvedPushEvent> { _, _, _ ->
|
||||
ResolvedPushEvent.Event(aNotifiableMessageEvent)
|
||||
lambdaRecorder<SessionId, RoomId, EventId, Result<ResolvedPushEvent>> { _, _, _ ->
|
||||
Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))
|
||||
}
|
||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
||||
val pushHistoryService = FakePushHistoryService(
|
||||
onPushReceivedResult = onPushReceivedResult,
|
||||
)
|
||||
val aPushData = PushData(
|
||||
eventId = AN_EVENT_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
|
|
@ -72,9 +101,10 @@ class DefaultPushHandlerTest {
|
|||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_USER_ID }
|
||||
),
|
||||
incrementPushCounterResult = incrementPushCounterResult
|
||||
incrementPushCounterResult = incrementPushCounterResult,
|
||||
pushHistoryService = pushHistoryService,
|
||||
)
|
||||
defaultPushHandler.handle(aPushData)
|
||||
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||
incrementPushCounterResult.assertions()
|
||||
.isCalledOnce()
|
||||
notifiableEventResult.assertions()
|
||||
|
|
@ -83,6 +113,8 @@ class DefaultPushHandlerTest {
|
|||
onNotifiableEventReceived.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(aNotifiableMessageEvent))
|
||||
onPushReceivedResult.assertions()
|
||||
.isCalledOnce()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -90,8 +122,8 @@ class DefaultPushHandlerTest {
|
|||
runTest {
|
||||
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
||||
val notifiableEventResult =
|
||||
lambdaRecorder<SessionId, RoomId, EventId, ResolvedPushEvent.Event> { _, _, _ ->
|
||||
ResolvedPushEvent.Event(aNotifiableMessageEvent)
|
||||
lambdaRecorder<SessionId, RoomId, EventId, Result<ResolvedPushEvent.Event>> { _, _, _ ->
|
||||
Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))
|
||||
}
|
||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||
|
|
@ -101,6 +133,10 @@ class DefaultPushHandlerTest {
|
|||
unread = 0,
|
||||
clientSecret = A_SECRET,
|
||||
)
|
||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
||||
val pushHistoryService = FakePushHistoryService(
|
||||
onPushReceivedResult = onPushReceivedResult,
|
||||
)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
notifiableEventResult = notifiableEventResult,
|
||||
|
|
@ -110,15 +146,18 @@ class DefaultPushHandlerTest {
|
|||
userPushStore = FakeUserPushStore().apply {
|
||||
setNotificationEnabledForDevice(false)
|
||||
},
|
||||
incrementPushCounterResult = incrementPushCounterResult
|
||||
incrementPushCounterResult = incrementPushCounterResult,
|
||||
pushHistoryService = pushHistoryService,
|
||||
)
|
||||
defaultPushHandler.handle(aPushData)
|
||||
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||
incrementPushCounterResult.assertions()
|
||||
.isCalledOnce()
|
||||
notifiableEventResult.assertions()
|
||||
.isCalledOnce()
|
||||
onNotifiableEventReceived.assertions()
|
||||
.isNeverCalled()
|
||||
onPushReceivedResult.assertions()
|
||||
.isCalledOnce()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -126,8 +165,8 @@ class DefaultPushHandlerTest {
|
|||
runTest {
|
||||
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
||||
val notifiableEventResult =
|
||||
lambdaRecorder<SessionId, RoomId, EventId, ResolvedPushEvent.Event> { _, _, _ ->
|
||||
ResolvedPushEvent.Event(aNotifiableMessageEvent)
|
||||
lambdaRecorder<SessionId, RoomId, EventId, Result<ResolvedPushEvent.Event>> { _, _, _ ->
|
||||
Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))
|
||||
}
|
||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||
|
|
@ -137,6 +176,10 @@ class DefaultPushHandlerTest {
|
|||
unread = 0,
|
||||
clientSecret = A_SECRET,
|
||||
)
|
||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
||||
val pushHistoryService = FakePushHistoryService(
|
||||
onPushReceivedResult = onPushReceivedResult,
|
||||
)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
notifiableEventResult = notifiableEventResult,
|
||||
|
|
@ -146,9 +189,10 @@ class DefaultPushHandlerTest {
|
|||
matrixAuthenticationService = FakeMatrixAuthenticationService().apply {
|
||||
getLatestSessionIdLambda = { A_USER_ID }
|
||||
},
|
||||
incrementPushCounterResult = incrementPushCounterResult
|
||||
incrementPushCounterResult = incrementPushCounterResult,
|
||||
pushHistoryService = pushHistoryService,
|
||||
)
|
||||
defaultPushHandler.handle(aPushData)
|
||||
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||
incrementPushCounterResult.assertions()
|
||||
.isCalledOnce()
|
||||
notifiableEventResult.assertions()
|
||||
|
|
@ -157,6 +201,8 @@ class DefaultPushHandlerTest {
|
|||
onNotifiableEventReceived.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(aNotifiableMessageEvent))
|
||||
onPushReceivedResult.assertions()
|
||||
.isCalledOnce()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -164,8 +210,8 @@ class DefaultPushHandlerTest {
|
|||
runTest {
|
||||
val aNotifiableMessageEvent = aNotifiableMessageEvent()
|
||||
val notifiableEventResult =
|
||||
lambdaRecorder<SessionId, RoomId, EventId, ResolvedPushEvent.Event> { _, _, _ ->
|
||||
ResolvedPushEvent.Event(aNotifiableMessageEvent)
|
||||
lambdaRecorder<SessionId, RoomId, EventId, Result<ResolvedPushEvent.Event>> { _, _, _ ->
|
||||
Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))
|
||||
}
|
||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||
|
|
@ -175,6 +221,10 @@ class DefaultPushHandlerTest {
|
|||
unread = 0,
|
||||
clientSecret = A_SECRET,
|
||||
)
|
||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
||||
val pushHistoryService = FakePushHistoryService(
|
||||
onPushReceivedResult = onPushReceivedResult,
|
||||
)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
notifiableEventResult = notifiableEventResult,
|
||||
|
|
@ -184,22 +234,27 @@ class DefaultPushHandlerTest {
|
|||
matrixAuthenticationService = FakeMatrixAuthenticationService().apply {
|
||||
getLatestSessionIdLambda = { null }
|
||||
},
|
||||
incrementPushCounterResult = incrementPushCounterResult
|
||||
incrementPushCounterResult = incrementPushCounterResult,
|
||||
pushHistoryService = pushHistoryService,
|
||||
)
|
||||
defaultPushHandler.handle(aPushData)
|
||||
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||
incrementPushCounterResult.assertions()
|
||||
.isCalledOnce()
|
||||
notifiableEventResult.assertions()
|
||||
.isNeverCalled()
|
||||
onNotifiableEventReceived.assertions()
|
||||
.isNeverCalled()
|
||||
onPushReceivedResult.assertions()
|
||||
.isCalledOnce()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when classical PushData is received, but not able to resolve the event, nothing happen`() =
|
||||
runTest {
|
||||
val notifiableEventResult =
|
||||
lambdaRecorder<SessionId, RoomId, EventId, ResolvedPushEvent.Event?> { _, _, _ -> null }
|
||||
lambdaRecorder<SessionId, RoomId, EventId, Result<ResolvedPushEvent.Event>> { _, _, _ ->
|
||||
Result.failure(ResolvingException("Unable to resolve"))
|
||||
}
|
||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||
val aPushData = PushData(
|
||||
|
|
@ -208,6 +263,10 @@ class DefaultPushHandlerTest {
|
|||
unread = 0,
|
||||
clientSecret = A_SECRET,
|
||||
)
|
||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
||||
val pushHistoryService = FakePushHistoryService(
|
||||
onPushReceivedResult = onPushReceivedResult,
|
||||
)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
notifiableEventResult = notifiableEventResult,
|
||||
|
|
@ -218,9 +277,10 @@ class DefaultPushHandlerTest {
|
|||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_USER_ID }
|
||||
),
|
||||
incrementPushCounterResult = incrementPushCounterResult
|
||||
incrementPushCounterResult = incrementPushCounterResult,
|
||||
pushHistoryService = pushHistoryService,
|
||||
)
|
||||
defaultPushHandler.handle(aPushData)
|
||||
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||
incrementPushCounterResult.assertions()
|
||||
.isCalledOnce()
|
||||
notifiableEventResult.assertions()
|
||||
|
|
@ -228,6 +288,9 @@ class DefaultPushHandlerTest {
|
|||
.with(value(A_USER_ID), value(A_ROOM_ID), value(AN_EVENT_ID))
|
||||
onNotifiableEventReceived.assertions()
|
||||
.isNeverCalled()
|
||||
onPushReceivedResult.assertions()
|
||||
.isCalledOnce()
|
||||
.with(any(), value(AN_EVENT_ID), value(A_ROOM_ID), value(A_USER_ID), value(false), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -251,20 +314,28 @@ class DefaultPushHandlerTest {
|
|||
> { _, _, _, _, _, _, _, _ -> }
|
||||
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
|
||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
||||
val pushHistoryService = FakePushHistoryService(
|
||||
onPushReceivedResult = onPushReceivedResult,
|
||||
)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
elementCallEntryPoint = elementCallEntryPoint,
|
||||
notifiableEventResult = { _, _, _ ->
|
||||
ResolvedPushEvent.Event(aNotifiableCallEvent(callNotifyType = CallNotifyType.RING, timestamp = Instant.now().toEpochMilli()))
|
||||
Result.success(
|
||||
ResolvedPushEvent.Event(aNotifiableCallEvent(callNotifyType = CallNotifyType.RING, timestamp = Instant.now().toEpochMilli()))
|
||||
)
|
||||
},
|
||||
incrementPushCounterResult = {},
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_USER_ID }
|
||||
),
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
pushHistoryService = pushHistoryService,
|
||||
)
|
||||
defaultPushHandler.handle(aPushData)
|
||||
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||
handleIncomingCallLambda.assertions().isCalledOnce()
|
||||
onNotifiableEventReceived.assertions().isCalledOnce()
|
||||
onPushReceivedResult.assertions().isCalledOnce()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -288,21 +359,27 @@ class DefaultPushHandlerTest {
|
|||
Unit,
|
||||
> { _, _, _, _, _, _, _, _ -> }
|
||||
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
|
||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
||||
val pushHistoryService = FakePushHistoryService(
|
||||
onPushReceivedResult = onPushReceivedResult,
|
||||
)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
elementCallEntryPoint = elementCallEntryPoint,
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
notifiableEventResult = { _, _, _ ->
|
||||
ResolvedPushEvent.Event(aNotifiableMessageEvent(type = EventType.CALL_NOTIFY))
|
||||
Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent(type = EventType.CALL_NOTIFY)))
|
||||
},
|
||||
incrementPushCounterResult = {},
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_USER_ID }
|
||||
),
|
||||
pushHistoryService = pushHistoryService,
|
||||
)
|
||||
defaultPushHandler.handle(aPushData)
|
||||
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||
|
||||
handleIncomingCallLambda.assertions().isNeverCalled()
|
||||
onNotifiableEventReceived.assertions().isCalledOnce()
|
||||
onPushReceivedResult.assertions().isCalledOnce()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -326,11 +403,15 @@ class DefaultPushHandlerTest {
|
|||
Unit,
|
||||
> { _, _, _, _, _, _, _, _ -> }
|
||||
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
|
||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
||||
val pushHistoryService = FakePushHistoryService(
|
||||
onPushReceivedResult = onPushReceivedResult,
|
||||
)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
elementCallEntryPoint = elementCallEntryPoint,
|
||||
onNotifiableEventReceived = onNotifiableEventReceived,
|
||||
notifiableEventResult = { _, _, _ ->
|
||||
ResolvedPushEvent.Event(aNotifiableCallEvent())
|
||||
Result.success(ResolvedPushEvent.Event(aNotifiableCallEvent()))
|
||||
},
|
||||
incrementPushCounterResult = {},
|
||||
userPushStore = FakeUserPushStore().apply {
|
||||
|
|
@ -339,10 +420,12 @@ class DefaultPushHandlerTest {
|
|||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_USER_ID }
|
||||
),
|
||||
pushHistoryService = pushHistoryService,
|
||||
)
|
||||
defaultPushHandler.handle(aPushData)
|
||||
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||
handleIncomingCallLambda.assertions().isCalledOnce()
|
||||
onNotifiableEventReceived.assertions().isCalledOnce()
|
||||
onPushReceivedResult.assertions().isCalledOnce()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -361,19 +444,26 @@ class DefaultPushHandlerTest {
|
|||
)
|
||||
val onRedactedEventReceived = lambdaRecorder<ResolvedPushEvent.Redaction, Unit> { }
|
||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
||||
val pushHistoryService = FakePushHistoryService(
|
||||
onPushReceivedResult = onPushReceivedResult,
|
||||
)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
onRedactedEventReceived = onRedactedEventReceived,
|
||||
incrementPushCounterResult = incrementPushCounterResult,
|
||||
notifiableEventResult = { _, _, _ -> aRedaction },
|
||||
notifiableEventResult = { _, _, _ -> Result.success(aRedaction) },
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_USER_ID }
|
||||
),
|
||||
pushHistoryService = pushHistoryService,
|
||||
)
|
||||
defaultPushHandler.handle(aPushData)
|
||||
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||
incrementPushCounterResult.assertions()
|
||||
.isCalledOnce()
|
||||
onRedactedEventReceived.assertions().isCalledOnce()
|
||||
.with(value(aRedaction))
|
||||
onPushReceivedResult.assertions()
|
||||
.isCalledOnce()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -386,20 +476,27 @@ class DefaultPushHandlerTest {
|
|||
clientSecret = A_SECRET,
|
||||
)
|
||||
val diagnosticPushHandler = DiagnosticPushHandler()
|
||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
||||
val pushHistoryService = FakePushHistoryService(
|
||||
onPushReceivedResult = onPushReceivedResult,
|
||||
)
|
||||
val defaultPushHandler = createDefaultPushHandler(
|
||||
diagnosticPushHandler = diagnosticPushHandler,
|
||||
incrementPushCounterResult = { }
|
||||
incrementPushCounterResult = { },
|
||||
pushHistoryService = pushHistoryService,
|
||||
)
|
||||
diagnosticPushHandler.state.test {
|
||||
defaultPushHandler.handle(aPushData)
|
||||
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
|
||||
awaitItem()
|
||||
}
|
||||
onPushReceivedResult.assertions()
|
||||
.isCalledOnce()
|
||||
}
|
||||
|
||||
private fun createDefaultPushHandler(
|
||||
onNotifiableEventReceived: (NotifiableEvent) -> Unit = { lambdaError() },
|
||||
onRedactedEventReceived: (ResolvedPushEvent.Redaction) -> Unit = { lambdaError() },
|
||||
notifiableEventResult: (SessionId, RoomId, EventId) -> ResolvedPushEvent? = { _, _, _ -> lambdaError() },
|
||||
notifiableEventResult: (SessionId, RoomId, EventId) -> Result<ResolvedPushEvent> = { _, _, _ -> lambdaError() },
|
||||
incrementPushCounterResult: () -> Unit = { lambdaError() },
|
||||
userPushStore: UserPushStore = FakeUserPushStore(),
|
||||
pushClientSecret: PushClientSecret = FakePushClientSecret(),
|
||||
|
|
@ -408,6 +505,7 @@ class DefaultPushHandlerTest {
|
|||
diagnosticPushHandler: DiagnosticPushHandler = DiagnosticPushHandler(),
|
||||
elementCallEntryPoint: FakeElementCallEntryPoint = FakeElementCallEntryPoint(),
|
||||
notificationChannels: FakeNotificationChannels = FakeNotificationChannels(),
|
||||
pushHistoryService: PushHistoryService = FakePushHistoryService(),
|
||||
): DefaultPushHandler {
|
||||
return DefaultPushHandler(
|
||||
onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceived),
|
||||
|
|
@ -425,6 +523,7 @@ class DefaultPushHandlerTest {
|
|||
diagnosticPushHandler = diagnosticPushHandler,
|
||||
elementCallEntryPoint = elementCallEntryPoint,
|
||||
notificationChannels = notificationChannels,
|
||||
pushHistoryService = pushHistoryService,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.store
|
||||
|
||||
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class InMemoryPushDataStore(
|
||||
initialPushCounter: Int = 0,
|
||||
initialPushHistoryItems: List<PushHistoryItem> = emptyList(),
|
||||
private val resetResult: () -> Unit = { lambdaError() }
|
||||
) : PushDataStore {
|
||||
private val mutablePushCounterFlow = MutableStateFlow(initialPushCounter)
|
||||
override val pushCounterFlow: Flow<Int> = mutablePushCounterFlow.asStateFlow()
|
||||
|
||||
private val mutablePushHistoryItemsFlow = MutableStateFlow(initialPushHistoryItems)
|
||||
|
||||
override fun getPushHistoryItemsFlow(): Flow<List<PushHistoryItem>> {
|
||||
return mutablePushHistoryItemsFlow.asStateFlow()
|
||||
}
|
||||
|
||||
override suspend fun reset() {
|
||||
resetResult()
|
||||
}
|
||||
}
|
||||
|
|
@ -15,10 +15,10 @@ android {
|
|||
|
||||
dependencies {
|
||||
api(projects.libraries.push.api)
|
||||
api(projects.libraries.pushproviders.api)
|
||||
implementation(projects.libraries.push.impl)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.pushproviders.api)
|
||||
implementation(projects.tests.testutils)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.coil.compose)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ package io.element.android.libraries.push.test
|
|||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
import io.element.android.libraries.pushproviders.api.PushProvider
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
|
@ -26,6 +27,7 @@ class FakePushService(
|
|||
private val currentPushProvider: () -> PushProvider? = { availablePushProviders.firstOrNull() },
|
||||
private val selectPushProviderLambda: suspend (SessionId, PushProvider) -> Unit = { _, _ -> lambdaError() },
|
||||
private val setIgnoreRegistrationErrorLambda: (SessionId, Boolean) -> Unit = { _, _ -> lambdaError() },
|
||||
private val resetPushHistoryResult: () -> Unit = { lambdaError() },
|
||||
) : PushService {
|
||||
override suspend fun getCurrentPushProvider(): PushProvider? {
|
||||
return registeredPushProvider ?: currentPushProvider()
|
||||
|
|
@ -68,4 +70,26 @@ class FakePushService(
|
|||
override suspend fun testPush(): Boolean = simulateLongTask {
|
||||
testPushBlock()
|
||||
}
|
||||
|
||||
private val pushHistoryItemsFlow = MutableStateFlow<List<PushHistoryItem>>(emptyList())
|
||||
|
||||
override fun getPushHistoryItemsFlow(): Flow<List<PushHistoryItem>> {
|
||||
return pushHistoryItemsFlow
|
||||
}
|
||||
|
||||
fun emitPushHistoryItems(items: List<PushHistoryItem>) {
|
||||
pushHistoryItemsFlow.value = items
|
||||
}
|
||||
|
||||
private val pushCounterFlow = MutableStateFlow(0)
|
||||
|
||||
override val pushCounter: Flow<Int> = pushCounterFlow
|
||||
|
||||
fun emitPushCounter(counter: Int) {
|
||||
pushCounterFlow.value = counter
|
||||
}
|
||||
|
||||
override suspend fun resetPushHistory() {
|
||||
resetPushHistoryResult()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,14 @@ import io.element.android.libraries.matrix.api.core.SessionId
|
|||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||
import io.element.android.libraries.push.impl.notifications.CallNotificationEventResolver
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeCallNotificationEventResolver(
|
||||
var resolveEventLambda: (sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean) -> NotifiableEvent? = { _, _, _ -> null },
|
||||
var resolveEventLambda: (sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean) -> Result<NotifiableEvent> = { _, _, _ ->
|
||||
lambdaError()
|
||||
},
|
||||
) : CallNotificationEventResolver {
|
||||
override fun resolveEvent(sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean): NotifiableEvent? {
|
||||
override fun resolveEvent(sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean): Result<NotifiableEvent> {
|
||||
return resolveEventLambda(sessionId, notificationData, forceNotify)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,14 @@ import io.element.android.libraries.pushproviders.api.PushHandler
|
|||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakePushHandler(
|
||||
private val handleResult: (PushData) -> Unit = { lambdaError() }
|
||||
private val handleResult: (PushData, String) -> Unit = { _, _ -> lambdaError() },
|
||||
private val handleInvalidResult: (String) -> Unit = { lambdaError() },
|
||||
) : PushHandler {
|
||||
override suspend fun handle(pushData: PushData) {
|
||||
handleResult(pushData)
|
||||
override suspend fun handle(pushData: PushData, providerInfo: String) {
|
||||
handleResult(pushData, providerInfo)
|
||||
}
|
||||
|
||||
override suspend fun handleInvalid(providerInfo: String) {
|
||||
handleInvalidResult(providerInfo)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,5 +8,12 @@
|
|||
package io.element.android.libraries.pushproviders.api
|
||||
|
||||
interface PushHandler {
|
||||
suspend fun handle(pushData: PushData)
|
||||
suspend fun handle(
|
||||
pushData: PushData,
|
||||
providerInfo: String,
|
||||
)
|
||||
|
||||
suspend fun handleInvalid(
|
||||
providerInfo: String,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,8 +43,14 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
|||
val pushData = pushParser.parse(message.data)
|
||||
if (pushData == null) {
|
||||
Timber.tag(loggerTag.value).w("Invalid data received from Firebase")
|
||||
pushHandler.handleInvalid(
|
||||
providerInfo = FirebaseConfig.NAME,
|
||||
)
|
||||
} else {
|
||||
pushHandler.handle(pushData)
|
||||
pushHandler.handle(
|
||||
pushData = pushData,
|
||||
providerInfo = FirebaseConfig.NAME,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import io.element.android.tests.testutils.lambda.value
|
|||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
|
@ -31,16 +32,18 @@ import org.robolectric.RobolectricTestRunner
|
|||
class VectorFirebaseMessagingServiceTest {
|
||||
@Test
|
||||
fun `test receiving invalid data`() = runTest {
|
||||
val lambda = lambdaRecorder<PushData, Unit>(ensureNeverCalled = true) { }
|
||||
val lambda = lambdaRecorder<String, Unit> {}
|
||||
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
|
||||
pushHandler = FakePushHandler(handleResult = lambda)
|
||||
pushHandler = FakePushHandler(handleInvalidResult = lambda)
|
||||
)
|
||||
vectorFirebaseMessagingService.onMessageReceived(RemoteMessage(Bundle()))
|
||||
runCurrent()
|
||||
lambda.assertions().isCalledOnce()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test receiving valid data`() = runTest {
|
||||
val lambda = lambdaRecorder<PushData, Unit> { }
|
||||
val lambda = lambdaRecorder<PushData, String, Unit> { _, _ -> }
|
||||
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
|
||||
pushHandler = FakePushHandler(handleResult = lambda)
|
||||
)
|
||||
|
|
@ -56,7 +59,10 @@ class VectorFirebaseMessagingServiceTest {
|
|||
advanceUntilIdle()
|
||||
lambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(PushData(AN_EVENT_ID, A_ROOM_ID, null, A_SECRET)))
|
||||
.with(
|
||||
value(PushData(AN_EVENT_ID, A_ROOM_ID, null, A_SECRET)),
|
||||
value(FirebaseConfig.NAME)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -51,8 +51,14 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
|||
val pushData = pushParser.parse(message, instance)
|
||||
if (pushData == null) {
|
||||
Timber.tag(loggerTag.value).w("Invalid data received from UnifiedPush")
|
||||
pushHandler.handleInvalid(
|
||||
providerInfo = "${UnifiedPushConfig.NAME} - $instance",
|
||||
)
|
||||
} else {
|
||||
pushHandler.handle(pushData)
|
||||
pushHandler.handle(
|
||||
pushData = pushData,
|
||||
providerInfo = "${UnifiedPushConfig.NAME} - $instance",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,9 +60,9 @@ class VectorUnifiedPushMessagingReceiverTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `onMessage valid invoke the push handler`() = runTest {
|
||||
fun `onMessage valid invokes the push handler`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val pushHandlerResult = lambdaRecorder<PushData, Unit> {}
|
||||
val pushHandlerResult = lambdaRecorder<PushData, String, Unit> { _, _ -> }
|
||||
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver(
|
||||
pushHandler = FakePushHandler(
|
||||
handleResult = pushHandlerResult
|
||||
|
|
@ -80,23 +80,25 @@ class VectorUnifiedPushMessagingReceiverTest {
|
|||
unread = 1,
|
||||
clientSecret = A_SECRET
|
||||
)
|
||||
),
|
||||
value(
|
||||
UnifiedPushConfig.NAME + " - " + A_SECRET
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onMessage invalid does not invoke the push handler`() = runTest {
|
||||
fun `onMessage invalid invokes the push handler invalid method`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val pushHandlerResult = lambdaRecorder<PushData, Unit> {}
|
||||
val handleInvalidResult = lambdaRecorder<String, Unit> { }
|
||||
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver(
|
||||
pushHandler = FakePushHandler(
|
||||
handleResult = pushHandlerResult
|
||||
handleInvalidResult = handleInvalidResult,
|
||||
),
|
||||
)
|
||||
vectorUnifiedPushMessagingReceiver.onMessage(context, "".toByteArray(), A_SECRET)
|
||||
advanceUntilIdle()
|
||||
pushHandlerResult.assertions()
|
||||
.isNeverCalled()
|
||||
handleInvalidResult.assertions().isCalledOnce()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ android {
|
|||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(libs.coroutines.core)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.troubleshoot.api
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
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
|
||||
|
||||
interface PushHistoryEntryPoint : FeatureEntryPoint {
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
|
||||
interface NodeBuilder {
|
||||
fun callback(callback: Callback): NodeBuilder
|
||||
fun build(): Node
|
||||
}
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onDone()
|
||||
fun onItemClick(sessionId: SessionId, roomId: RoomId, eventId: EventId)
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,8 @@ dependencies {
|
|||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
api(projects.libraries.troubleshoot.api)
|
||||
api(projects.libraries.push.api)
|
||||
implementation(projects.services.analytics.api)
|
||||
|
|
@ -40,6 +42,7 @@ dependencies {
|
|||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.troubleshoot.impl.history
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.troubleshoot.api.PushHistoryEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultPushHistoryEntryPoint @Inject constructor() : PushHistoryEntryPoint {
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): PushHistoryEntryPoint.NodeBuilder {
|
||||
val plugins = ArrayList<Plugin>()
|
||||
|
||||
return object : PushHistoryEntryPoint.NodeBuilder {
|
||||
override fun callback(callback: PushHistoryEntryPoint.Callback): PushHistoryEntryPoint.NodeBuilder {
|
||||
plugins += callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<PushHistoryNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.troubleshoot.impl.history
|
||||
|
||||
sealed interface PushHistoryEvents {
|
||||
data object Reset : PushHistoryEvents
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.troubleshoot.impl.history
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
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.troubleshoot.api.PushHistoryEntryPoint
|
||||
import io.element.android.services.analytics.api.ScreenTracker
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class PushHistoryNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: PushHistoryPresenter,
|
||||
private val screenTracker: ScreenTracker,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
private fun onDone() {
|
||||
plugins<PushHistoryEntryPoint.Callback>().forEach {
|
||||
it.onDone()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onItemClick(sessionId: SessionId, roomId: RoomId, eventId: EventId) {
|
||||
plugins<PushHistoryEntryPoint.Callback>().forEach {
|
||||
it.onItemClick(sessionId, roomId, eventId)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
screenTracker.TrackScreen(MobileScreen.ScreenName.NotificationTroubleshoot)
|
||||
val state = presenter.present()
|
||||
PushHistoryView(
|
||||
state = state,
|
||||
onBackClick = ::onDone,
|
||||
onItemClick = ::onItemClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.troubleshoot.impl.history
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class PushHistoryPresenter @Inject constructor(
|
||||
private val pushService: PushService,
|
||||
) : Presenter<PushHistoryState> {
|
||||
@Composable
|
||||
override fun present(): PushHistoryState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val pushCounter by pushService.pushCounter.collectAsState(0)
|
||||
val pushHistory by remember {
|
||||
pushService.getPushHistoryItemsFlow()
|
||||
}.collectAsState(emptyList())
|
||||
|
||||
fun handleEvents(event: PushHistoryEvents) {
|
||||
when (event) {
|
||||
PushHistoryEvents.Reset -> coroutineScope.launch {
|
||||
pushService.resetPushHistory()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return PushHistoryState(
|
||||
pushCounter = pushCounter,
|
||||
pushHistoryItems = pushHistory.toImmutableList(),
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.troubleshoot.impl.history
|
||||
|
||||
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class PushHistoryState(
|
||||
val pushCounter: Int,
|
||||
val pushHistoryItems: ImmutableList<PushHistoryItem>,
|
||||
val eventSink: (PushHistoryEvents) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.troubleshoot.impl.history
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
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.push.api.history.PushHistoryItem
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
open class PushHistoryStateProvider : PreviewParameterProvider<PushHistoryState> {
|
||||
override val values: Sequence<PushHistoryState>
|
||||
get() = sequenceOf(
|
||||
aPushHistoryState(),
|
||||
aPushHistoryState(
|
||||
pushCounter = 123,
|
||||
pushHistoryItems = listOf(
|
||||
aPushHistoryItem(
|
||||
hasBeenResolved = false,
|
||||
comment = "An error description"
|
||||
),
|
||||
aPushHistoryItem(
|
||||
pushDate = 1,
|
||||
providerInfo = "providerInfo2",
|
||||
eventId = EventId("\$anEventId"),
|
||||
roomId = RoomId("!roomId:domain"),
|
||||
sessionId = SessionId("@alice:server.org"),
|
||||
hasBeenResolved = true,
|
||||
comment = "A comment"
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aPushHistoryState(
|
||||
pushCounter: Int = 0,
|
||||
pushHistoryItems: List<PushHistoryItem> = emptyList(),
|
||||
eventSink: (PushHistoryEvents) -> Unit = {},
|
||||
) = PushHistoryState(
|
||||
pushCounter = pushCounter,
|
||||
pushHistoryItems = pushHistoryItems.toImmutableList(),
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
fun aPushHistoryItem(
|
||||
pushDate: Long = 0,
|
||||
formattedDate: String = "formattedDate",
|
||||
providerInfo: String = "providerInfo",
|
||||
eventId: EventId? = null,
|
||||
roomId: RoomId? = null,
|
||||
sessionId: SessionId? = null,
|
||||
hasBeenResolved: Boolean = false,
|
||||
comment: String? = null,
|
||||
): PushHistoryItem {
|
||||
return PushHistoryItem(
|
||||
pushDate = pushDate,
|
||||
formattedDate = formattedDate,
|
||||
providerInfo = providerInfo,
|
||||
eventId = eventId,
|
||||
roomId = roomId,
|
||||
sessionId = sessionId,
|
||||
hasBeenResolved = hasBeenResolved,
|
||||
comment = comment
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.troubleshoot.impl.history
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
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.push.api.history.PushHistoryItem
|
||||
import io.element.android.libraries.troubleshoot.impl.R
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PushHistoryView(
|
||||
state: PushHistoryState,
|
||||
onBackClick: () -> Unit,
|
||||
onItemClick: (SessionId, RoomId, EventId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding()
|
||||
.imePadding(),
|
||||
contentWindowInsets = WindowInsets.statusBars,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
BackButton(onClick = onBackClick)
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.screen_push_history_title),
|
||||
style = ElementTheme.typography.aliasScreenTitle,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
TextButton(
|
||||
text = stringResource(CommonStrings.action_reset),
|
||||
onClick = {
|
||||
state.eventSink(PushHistoryEvents.Reset)
|
||||
},
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
PushHistoryContent(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding),
|
||||
state = state,
|
||||
onItemClick = onItemClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PushHistoryContent(
|
||||
state: PushHistoryState,
|
||||
onItemClick: (SessionId, RoomId, EventId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth()
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text("Total number of received push") },
|
||||
trailingContent = ListItemContent.Text(state.pushCounter.toString()),
|
||||
)
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
items(
|
||||
items = state.pushHistoryItems,
|
||||
key = {
|
||||
it.pushDate.toString() + it.sessionId + it.roomId + it.eventId
|
||||
},
|
||||
) { pushHistory ->
|
||||
PushHistoryItem(
|
||||
pushHistory,
|
||||
onClick = {
|
||||
val sessionId = pushHistory.sessionId
|
||||
val roomId = pushHistory.roomId
|
||||
val eventId = pushHistory.eventId
|
||||
if (sessionId != null && roomId != null && eventId != null) {
|
||||
onItemClick(sessionId, roomId, eventId)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PushHistoryItem(
|
||||
pushHistoryItem: PushHistoryItem,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onClick()
|
||||
},
|
||||
) {
|
||||
HorizontalDivider()
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = pushHistoryItem.formattedDate,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
Text(
|
||||
text = pushHistoryItem.providerInfo,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 8.dp, top = 8.dp),
|
||||
text = pushHistoryItem.sessionId?.value ?: "No sessionId",
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
text = pushHistoryItem.roomId?.value ?: "No roomId",
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
text = pushHistoryItem.eventId?.value ?: "No eventId",
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
)
|
||||
pushHistoryItem.comment?.let {
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
text = it,
|
||||
color = if (pushHistoryItem.hasBeenResolved) {
|
||||
ElementTheme.colors.textSecondary
|
||||
} else {
|
||||
ElementTheme.colors.textCriticalPrimary
|
||||
},
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (pushHistoryItem.hasBeenResolved) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.CheckCircleSolid(),
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = ElementTheme.colors.iconSuccessPrimary,
|
||||
contentDescription = null,
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Error(),
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = ElementTheme.colors.iconCriticalPrimary,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun PushHistoryViewPreview(
|
||||
@PreviewParameter(PushHistoryStateProvider::class) state: PushHistoryState,
|
||||
) = ElementPreview {
|
||||
PushHistoryView(
|
||||
state = state,
|
||||
onBackClick = {},
|
||||
onItemClick = { _, _, _ -> },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_push_history_title">"Push history"</string>
|
||||
<string name="troubleshoot_notifications_screen_action">"Run tests"</string>
|
||||
<string name="troubleshoot_notifications_screen_action_again">"Run tests again"</string>
|
||||
<string name="troubleshoot_notifications_screen_failure">"Some tests failed. Please check the details."</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.libraries.troubleshoot.impl.history
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import io.element.android.libraries.push.test.FakePushService
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class PushHistoryPresenterTest {
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createPushHistoryPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.pushCounter).isEqualTo(0)
|
||||
assertThat(initialState.pushHistoryItems).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - updating state`() = runTest {
|
||||
val pushService = FakePushService()
|
||||
val presenter = createPushHistoryPresenter(
|
||||
pushService = pushService,
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.pushCounter).isEqualTo(0)
|
||||
assertThat(initialState.pushHistoryItems).isEmpty()
|
||||
pushService.emitPushCounter(1)
|
||||
assertThat(awaitItem().pushCounter).isEqualTo(1)
|
||||
val item = aPushHistoryItem()
|
||||
pushService.emitPushHistoryItems(listOf(item))
|
||||
assertThat(awaitItem().pushHistoryItems).containsExactly(item)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - reset`() = runTest {
|
||||
val resetPushHistoryResult = lambdaRecorder<Unit> { }
|
||||
val pushService = FakePushService(
|
||||
resetPushHistoryResult = resetPushHistoryResult,
|
||||
)
|
||||
val presenter = createPushHistoryPresenter(
|
||||
pushService = pushService,
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(PushHistoryEvents.Reset)
|
||||
runCurrent()
|
||||
resetPushHistoryResult.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPushHistoryPresenter(
|
||||
pushService: PushService = FakePushService(),
|
||||
): PushHistoryPresenter {
|
||||
return PushHistoryPresenter(
|
||||
pushService = pushService,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.troubleshoot.impl.history
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
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.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_FORMATTED_DATE
|
||||
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.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithThreeParams
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class PushHistoryViewTest {
|
||||
@get:Rule
|
||||
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on Reset sends a PushHistoryEvents`() {
|
||||
val eventsRecorder = EventsRecorder<PushHistoryEvents>()
|
||||
rule.setPushHistoryView(
|
||||
aPushHistoryState(
|
||||
pushCounter = 123,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_reset)
|
||||
eventsRecorder.assertSingle(PushHistoryEvents.Reset)
|
||||
// Also check that the push counter is rendered
|
||||
rule.onNodeWithText("123").assertExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on an invalid event has no effect`() {
|
||||
val eventsRecorder = EventsRecorder<PushHistoryEvents>(expectEvents = false)
|
||||
rule.setPushHistoryView(
|
||||
aPushHistoryState(
|
||||
pushHistoryItems = listOf(
|
||||
aPushHistoryItem(
|
||||
formattedDate = A_FORMATTED_DATE,
|
||||
)
|
||||
),
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.onNodeWithText(A_FORMATTED_DATE).performClick()
|
||||
// No callback invoked
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on a valid event invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<PushHistoryEvents>(expectEvents = false)
|
||||
val onItemClick = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
|
||||
rule.setPushHistoryView(
|
||||
aPushHistoryState(
|
||||
pushHistoryItems = listOf(
|
||||
aPushHistoryItem(
|
||||
formattedDate = A_FORMATTED_DATE,
|
||||
eventId = AN_EVENT_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
sessionId = A_SESSION_ID,
|
||||
)
|
||||
),
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onItemClick = onItemClick,
|
||||
)
|
||||
rule.onNodeWithText(A_FORMATTED_DATE).performClick()
|
||||
onItemClick.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_SESSION_ID), value(A_ROOM_ID), value(AN_EVENT_ID))
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setPushHistoryView(
|
||||
state: PushHistoryState,
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
onItemClick: (SessionId, RoomId, EventId) -> Unit = EnsureNeverCalledWithThreeParams(),
|
||||
) {
|
||||
setContent {
|
||||
PushHistoryView(
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
onItemClick = onItemClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:27587909106650e33d7bc7854c0f2dd7ca6e2dad0aaf6487bf266753288ec6f6
|
||||
size 24898
|
||||
oid sha256:6408d329ea127961b08cb92fcfbbdd93fc2c700191e7c97ce4ebad147647c1c4
|
||||
size 24899
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9e65635400bfda5d242d07ebe31113e3ef2d039f21208421023f099997d5e4f9
|
||||
size 29603
|
||||
oid sha256:c1764cc1d1fc5234ef88a1f1cf9317a234cd7ee7195a190a0e827442c0d75db2
|
||||
size 29601
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:04cc3a21bceeee5727df5728d34eb7618a5a90949352cd02f4eaa2be77c47492
|
||||
size 13565
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7a8c0a12277b4335b1d01d3f5f206704b37c4cfb3693ec7d9a26ce15215ba800
|
||||
size 45566
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9d877e4d537fa0990dd0f2a5e14f2d00cf8dcd45a4c27fac6c0188ef2b441756
|
||||
size 13163
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1f110d1684003cdd1d6e47f765bbf4f55e2b0f3ba732926df08d06297b36bb7b
|
||||
size 44157
|
||||
|
|
@ -273,7 +273,8 @@
|
|||
{
|
||||
"name" : ":libraries:troubleshoot:impl",
|
||||
"includeRegex" : [
|
||||
"troubleshoot_notifications_screen_.*"
|
||||
"troubleshoot_notifications_screen_.*",
|
||||
"screen\\.push_history\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue