Push: improve Push history screen, log and stored data (#4601)
* Add adb tools to help with doze mode and app standby * Add info about the device state when an error occurs in push. * Keep more events in the DB. * Push history: add confirmation dialog when resetting the data * Push history: add a filter to see only the errors * Update screenshots * Push history: print out invalid/ignored data received. * Increase log level for push, to make such log more visible. It also appears that sometimes Timber.d are not present in the rageshakes. * Log priority * Do not include device state for invalid/ignored event. * Fix tests. * Fix format issue. * Fix mistake in code blocks and do not filter when not necessary. * Improve formatting and add missing unit test. * Reduce nesting of blocks. --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
parent
505d0b411d
commit
f916e4e3d4
30 changed files with 388 additions and 58 deletions
|
|
@ -7,8 +7,13 @@
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.history
|
package io.element.android.libraries.push.impl.history
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.PowerManager
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
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.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
|
@ -21,15 +26,38 @@ import javax.inject.Inject
|
||||||
class DefaultPushHistoryService @Inject constructor(
|
class DefaultPushHistoryService @Inject constructor(
|
||||||
private val pushDatabase: PushDatabase,
|
private val pushDatabase: PushDatabase,
|
||||||
private val systemClock: SystemClock,
|
private val systemClock: SystemClock,
|
||||||
|
@ApplicationContext context: Context,
|
||||||
) : PushHistoryService {
|
) : PushHistoryService {
|
||||||
|
private val powerManager = context.getSystemService<PowerManager>()
|
||||||
|
private val packageName = context.packageName
|
||||||
|
|
||||||
override fun onPushReceived(
|
override fun onPushReceived(
|
||||||
providerInfo: String,
|
providerInfo: String,
|
||||||
eventId: EventId?,
|
eventId: EventId?,
|
||||||
roomId: RoomId?,
|
roomId: RoomId?,
|
||||||
sessionId: SessionId?,
|
sessionId: SessionId?,
|
||||||
hasBeenResolved: Boolean,
|
hasBeenResolved: Boolean,
|
||||||
|
includeDeviceState: Boolean,
|
||||||
comment: String?,
|
comment: String?,
|
||||||
) {
|
) {
|
||||||
|
val finalComment = buildString {
|
||||||
|
append(comment.orEmpty())
|
||||||
|
if (includeDeviceState && powerManager != null) {
|
||||||
|
// Add info about device state
|
||||||
|
append("\n")
|
||||||
|
append(" - Idle: ${powerManager.isDeviceIdleMode}\n")
|
||||||
|
append(" - Power Save Mode: ${powerManager.isPowerSaveMode}\n")
|
||||||
|
append(" - Ignoring Battery Optimizations: ${powerManager.isIgnoringBatteryOptimizations(packageName)}\n")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
append(" - Device Light Idle Mode: ${powerManager.isDeviceLightIdleMode}\n")
|
||||||
|
append(" - Low Power Standby Enabled: ${powerManager.isLowPowerStandbyEnabled}\n")
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
append(" - Exempt from Low Power Standby: ${powerManager.isExemptFromLowPowerStandby}\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.takeIf { it.isNotEmpty() }
|
||||||
|
|
||||||
pushDatabase.pushHistoryQueries.insertPushHistory(
|
pushDatabase.pushHistoryQueries.insertPushHistory(
|
||||||
PushHistory(
|
PushHistory(
|
||||||
pushDate = systemClock.epochMillis(),
|
pushDate = systemClock.epochMillis(),
|
||||||
|
|
@ -38,11 +66,11 @@ class DefaultPushHistoryService @Inject constructor(
|
||||||
roomId = roomId?.value,
|
roomId = roomId?.value,
|
||||||
sessionId = sessionId?.value,
|
sessionId = sessionId?.value,
|
||||||
hasBeenResolved = if (hasBeenResolved) 1 else 0,
|
hasBeenResolved = if (hasBeenResolved) 1 else 0,
|
||||||
comment = comment,
|
comment = finalComment,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Keep only the last 100 events
|
// Keep only the last 1_000 events
|
||||||
pushDatabase.pushHistoryQueries.removeOldest(100)
|
pushDatabase.pushHistoryQueries.removeOldest(1_000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,19 +22,22 @@ interface PushHistoryService {
|
||||||
roomId: RoomId?,
|
roomId: RoomId?,
|
||||||
sessionId: SessionId?,
|
sessionId: SessionId?,
|
||||||
hasBeenResolved: Boolean,
|
hasBeenResolved: Boolean,
|
||||||
|
includeDeviceState: Boolean,
|
||||||
comment: String?,
|
comment: String?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun PushHistoryService.onInvalidPushReceived(
|
fun PushHistoryService.onInvalidPushReceived(
|
||||||
providerInfo: String,
|
providerInfo: String,
|
||||||
|
data: String,
|
||||||
) = onPushReceived(
|
) = onPushReceived(
|
||||||
providerInfo = providerInfo,
|
providerInfo = providerInfo,
|
||||||
eventId = null,
|
eventId = null,
|
||||||
roomId = null,
|
roomId = null,
|
||||||
sessionId = null,
|
sessionId = null,
|
||||||
hasBeenResolved = false,
|
hasBeenResolved = false,
|
||||||
comment = "Invalid push data",
|
includeDeviceState = false,
|
||||||
|
comment = "Invalid or ignored push data:\n$data",
|
||||||
)
|
)
|
||||||
|
|
||||||
fun PushHistoryService.onUnableToRetrieveSession(
|
fun PushHistoryService.onUnableToRetrieveSession(
|
||||||
|
|
@ -48,6 +51,7 @@ fun PushHistoryService.onUnableToRetrieveSession(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
sessionId = null,
|
sessionId = null,
|
||||||
hasBeenResolved = false,
|
hasBeenResolved = false,
|
||||||
|
includeDeviceState = true,
|
||||||
comment = "Unable to retrieve session: $reason",
|
comment = "Unable to retrieve session: $reason",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -63,6 +67,7 @@ fun PushHistoryService.onUnableToResolveEvent(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
hasBeenResolved = false,
|
hasBeenResolved = false,
|
||||||
|
includeDeviceState = true,
|
||||||
comment = "Unable to resolve event: $reason",
|
comment = "Unable to resolve event: $reason",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -78,6 +83,7 @@ fun PushHistoryService.onSuccess(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
hasBeenResolved = true,
|
hasBeenResolved = true,
|
||||||
|
includeDeviceState = false,
|
||||||
comment = buildString {
|
comment = buildString {
|
||||||
append("Success")
|
append("Success")
|
||||||
if (comment.isNullOrBlank().not()) {
|
if (comment.isNullOrBlank().not()) {
|
||||||
|
|
@ -94,5 +100,6 @@ fun PushHistoryService.onDiagnosticPush(
|
||||||
roomId = null,
|
roomId = null,
|
||||||
sessionId = null,
|
sessionId = null,
|
||||||
hasBeenResolved = true,
|
hasBeenResolved = true,
|
||||||
|
includeDeviceState = false,
|
||||||
comment = "Diagnostic push",
|
comment = "Diagnostic push",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -72,9 +72,9 @@ class DefaultPushHandler @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun handleInvalid(providerInfo: String) {
|
override suspend fun handleInvalid(providerInfo: String, data: String) {
|
||||||
incrementPushDataStore.incrementPushCounter()
|
incrementPushDataStore.incrementPushCounter()
|
||||||
pushHistoryService.onInvalidPushReceived(providerInfo)
|
pushHistoryService.onInvalidPushReceived(providerInfo, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,9 @@ class FakePushHistoryService(
|
||||||
RoomId?,
|
RoomId?,
|
||||||
SessionId?,
|
SessionId?,
|
||||||
Boolean,
|
Boolean,
|
||||||
|
Boolean,
|
||||||
String?
|
String?
|
||||||
) -> Unit = { _, _, _, _, _, _ -> lambdaError() }
|
) -> Unit = { _, _, _, _, _, _, _ -> lambdaError() }
|
||||||
) : PushHistoryService {
|
) : PushHistoryService {
|
||||||
override fun onPushReceived(
|
override fun onPushReceived(
|
||||||
providerInfo: String,
|
providerInfo: String,
|
||||||
|
|
@ -28,6 +29,7 @@ class FakePushHistoryService(
|
||||||
roomId: RoomId?,
|
roomId: RoomId?,
|
||||||
sessionId: SessionId?,
|
sessionId: SessionId?,
|
||||||
hasBeenResolved: Boolean,
|
hasBeenResolved: Boolean,
|
||||||
|
includeDeviceState: Boolean,
|
||||||
comment: String?,
|
comment: String?,
|
||||||
) {
|
) {
|
||||||
onPushReceivedResult(
|
onPushReceivedResult(
|
||||||
|
|
@ -36,6 +38,7 @@ class FakePushHistoryService(
|
||||||
roomId,
|
roomId,
|
||||||
sessionId,
|
sessionId,
|
||||||
hasBeenResolved,
|
hasBeenResolved,
|
||||||
|
includeDeviceState,
|
||||||
comment
|
comment
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ class DefaultPushHandlerTest {
|
||||||
@Test
|
@Test
|
||||||
fun `check handleInvalid behavior`() = runTest {
|
fun `check handleInvalid behavior`() = runTest {
|
||||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> }
|
||||||
val pushHistoryService = FakePushHistoryService(
|
val pushHistoryService = FakePushHistoryService(
|
||||||
onPushReceivedResult = onPushReceivedResult,
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
)
|
)
|
||||||
|
|
@ -68,12 +68,12 @@ class DefaultPushHandlerTest {
|
||||||
incrementPushCounterResult = incrementPushCounterResult,
|
incrementPushCounterResult = incrementPushCounterResult,
|
||||||
pushHistoryService = pushHistoryService,
|
pushHistoryService = pushHistoryService,
|
||||||
)
|
)
|
||||||
defaultPushHandler.handleInvalid(A_PUSHER_INFO)
|
defaultPushHandler.handleInvalid(A_PUSHER_INFO, "data")
|
||||||
incrementPushCounterResult.assertions()
|
incrementPushCounterResult.assertions()
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
onPushReceivedResult.assertions()
|
onPushReceivedResult.assertions()
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(value(A_PUSHER_INFO), value(null), value(null), value(null), value(false), value("Invalid push data"))
|
.with(value(A_PUSHER_INFO), value(null), value(null), value(null), value(false), value(false), value("Invalid or ignored push data:\ndata"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -85,7 +85,7 @@ class DefaultPushHandlerTest {
|
||||||
}
|
}
|
||||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> }
|
||||||
val pushHistoryService = FakePushHistoryService(
|
val pushHistoryService = FakePushHistoryService(
|
||||||
onPushReceivedResult = onPushReceivedResult,
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
)
|
)
|
||||||
|
|
@ -133,7 +133,7 @@ class DefaultPushHandlerTest {
|
||||||
unread = 0,
|
unread = 0,
|
||||||
clientSecret = A_SECRET,
|
clientSecret = A_SECRET,
|
||||||
)
|
)
|
||||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> }
|
||||||
val pushHistoryService = FakePushHistoryService(
|
val pushHistoryService = FakePushHistoryService(
|
||||||
onPushReceivedResult = onPushReceivedResult,
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
)
|
)
|
||||||
|
|
@ -176,7 +176,7 @@ class DefaultPushHandlerTest {
|
||||||
unread = 0,
|
unread = 0,
|
||||||
clientSecret = A_SECRET,
|
clientSecret = A_SECRET,
|
||||||
)
|
)
|
||||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> }
|
||||||
val pushHistoryService = FakePushHistoryService(
|
val pushHistoryService = FakePushHistoryService(
|
||||||
onPushReceivedResult = onPushReceivedResult,
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
)
|
)
|
||||||
|
|
@ -221,7 +221,7 @@ class DefaultPushHandlerTest {
|
||||||
unread = 0,
|
unread = 0,
|
||||||
clientSecret = A_SECRET,
|
clientSecret = A_SECRET,
|
||||||
)
|
)
|
||||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> }
|
||||||
val pushHistoryService = FakePushHistoryService(
|
val pushHistoryService = FakePushHistoryService(
|
||||||
onPushReceivedResult = onPushReceivedResult,
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
)
|
)
|
||||||
|
|
@ -263,7 +263,7 @@ class DefaultPushHandlerTest {
|
||||||
unread = 0,
|
unread = 0,
|
||||||
clientSecret = A_SECRET,
|
clientSecret = A_SECRET,
|
||||||
)
|
)
|
||||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> }
|
||||||
val pushHistoryService = FakePushHistoryService(
|
val pushHistoryService = FakePushHistoryService(
|
||||||
onPushReceivedResult = onPushReceivedResult,
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
)
|
)
|
||||||
|
|
@ -290,7 +290,7 @@ class DefaultPushHandlerTest {
|
||||||
.isNeverCalled()
|
.isNeverCalled()
|
||||||
onPushReceivedResult.assertions()
|
onPushReceivedResult.assertions()
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(any(), value(AN_EVENT_ID), value(A_ROOM_ID), value(A_USER_ID), value(false), any())
|
.with(any(), value(AN_EVENT_ID), value(A_ROOM_ID), value(A_USER_ID), value(false), value(true), any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -314,7 +314,7 @@ class DefaultPushHandlerTest {
|
||||||
> { _, _, _, _, _, _, _, _ -> }
|
> { _, _, _, _, _, _, _, _ -> }
|
||||||
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
|
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
|
||||||
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
|
||||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> }
|
||||||
val pushHistoryService = FakePushHistoryService(
|
val pushHistoryService = FakePushHistoryService(
|
||||||
onPushReceivedResult = onPushReceivedResult,
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
)
|
)
|
||||||
|
|
@ -359,7 +359,7 @@ class DefaultPushHandlerTest {
|
||||||
Unit,
|
Unit,
|
||||||
> { _, _, _, _, _, _, _, _ -> }
|
> { _, _, _, _, _, _, _, _ -> }
|
||||||
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
|
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
|
||||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> }
|
||||||
val pushHistoryService = FakePushHistoryService(
|
val pushHistoryService = FakePushHistoryService(
|
||||||
onPushReceivedResult = onPushReceivedResult,
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
)
|
)
|
||||||
|
|
@ -403,7 +403,7 @@ class DefaultPushHandlerTest {
|
||||||
Unit,
|
Unit,
|
||||||
> { _, _, _, _, _, _, _, _ -> }
|
> { _, _, _, _, _, _, _, _ -> }
|
||||||
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
|
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
|
||||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> }
|
||||||
val pushHistoryService = FakePushHistoryService(
|
val pushHistoryService = FakePushHistoryService(
|
||||||
onPushReceivedResult = onPushReceivedResult,
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
)
|
)
|
||||||
|
|
@ -444,7 +444,7 @@ class DefaultPushHandlerTest {
|
||||||
)
|
)
|
||||||
val onRedactedEventReceived = lambdaRecorder<ResolvedPushEvent.Redaction, Unit> { }
|
val onRedactedEventReceived = lambdaRecorder<ResolvedPushEvent.Redaction, Unit> { }
|
||||||
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
val incrementPushCounterResult = lambdaRecorder<Unit> {}
|
||||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> }
|
||||||
val pushHistoryService = FakePushHistoryService(
|
val pushHistoryService = FakePushHistoryService(
|
||||||
onPushReceivedResult = onPushReceivedResult,
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
)
|
)
|
||||||
|
|
@ -476,7 +476,7 @@ class DefaultPushHandlerTest {
|
||||||
clientSecret = A_SECRET,
|
clientSecret = A_SECRET,
|
||||||
)
|
)
|
||||||
val diagnosticPushHandler = DiagnosticPushHandler()
|
val diagnosticPushHandler = DiagnosticPushHandler()
|
||||||
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, String?, Unit> { _, _, _, _, _, _ -> }
|
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> }
|
||||||
val pushHistoryService = FakePushHistoryService(
|
val pushHistoryService = FakePushHistoryService(
|
||||||
onPushReceivedResult = onPushReceivedResult,
|
onPushReceivedResult = onPushReceivedResult,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ class FakePushService(
|
||||||
pushCounterFlow.value = counter
|
pushCounterFlow.value = counter
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun resetPushHistory() {
|
override suspend fun resetPushHistory() = simulateLongTask {
|
||||||
resetPushHistoryResult()
|
resetPushHistoryResult()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,13 @@ import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
class FakePushHandler(
|
class FakePushHandler(
|
||||||
private val handleResult: (PushData, String) -> Unit = { _, _ -> lambdaError() },
|
private val handleResult: (PushData, String) -> Unit = { _, _ -> lambdaError() },
|
||||||
private val handleInvalidResult: (String) -> Unit = { lambdaError() },
|
private val handleInvalidResult: (String, String) -> Unit = { _, _ -> lambdaError() },
|
||||||
) : PushHandler {
|
) : PushHandler {
|
||||||
override suspend fun handle(pushData: PushData, providerInfo: String) {
|
override suspend fun handle(pushData: PushData, providerInfo: String) {
|
||||||
handleResult(pushData, providerInfo)
|
handleResult(pushData, providerInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun handleInvalid(providerInfo: String) {
|
override suspend fun handleInvalid(providerInfo: String, data: String) {
|
||||||
handleInvalidResult(providerInfo)
|
handleInvalidResult(providerInfo, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,5 +15,6 @@ interface PushHandler {
|
||||||
|
|
||||||
suspend fun handleInvalid(
|
suspend fun handleInvalid(
|
||||||
providerInfo: String,
|
providerInfo: String,
|
||||||
|
data: String,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,20 +31,23 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewToken(token: String) {
|
override fun onNewToken(token: String) {
|
||||||
Timber.tag(loggerTag.value).d("New Firebase token")
|
Timber.tag(loggerTag.value).w("New Firebase token")
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
firebaseNewTokenHandler.handle(token)
|
firebaseNewTokenHandler.handle(token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMessageReceived(message: RemoteMessage) {
|
override fun onMessageReceived(message: RemoteMessage) {
|
||||||
Timber.tag(loggerTag.value).d("New Firebase message")
|
Timber.tag(loggerTag.value).w("New Firebase message. Priority: ${message.priority}/${message.originalPriority}")
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val pushData = pushParser.parse(message.data)
|
val pushData = pushParser.parse(message.data)
|
||||||
if (pushData == null) {
|
if (pushData == null) {
|
||||||
Timber.tag(loggerTag.value).w("Invalid data received from Firebase")
|
Timber.tag(loggerTag.value).w("Invalid data received from Firebase")
|
||||||
pushHandler.handleInvalid(
|
pushHandler.handleInvalid(
|
||||||
providerInfo = FirebaseConfig.NAME,
|
providerInfo = FirebaseConfig.NAME,
|
||||||
|
data = message.data.keys.joinToString("\n") {
|
||||||
|
"$it: ${message.data[it]}"
|
||||||
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
pushHandler.handle(
|
pushHandler.handle(
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,24 @@ import org.robolectric.RobolectricTestRunner
|
||||||
class VectorFirebaseMessagingServiceTest {
|
class VectorFirebaseMessagingServiceTest {
|
||||||
@Test
|
@Test
|
||||||
fun `test receiving invalid data`() = runTest {
|
fun `test receiving invalid data`() = runTest {
|
||||||
val lambda = lambdaRecorder<String, Unit> {}
|
val lambda = lambdaRecorder<String, String, Unit> { _, _ -> }
|
||||||
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
|
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
|
||||||
pushHandler = FakePushHandler(handleInvalidResult = lambda)
|
pushHandler = FakePushHandler(handleInvalidResult = lambda)
|
||||||
)
|
)
|
||||||
vectorFirebaseMessagingService.onMessageReceived(RemoteMessage(Bundle()))
|
vectorFirebaseMessagingService.onMessageReceived(
|
||||||
|
message = RemoteMessage(
|
||||||
|
Bundle().apply {
|
||||||
|
putString("a", "A")
|
||||||
|
putString("b", "B")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
runCurrent()
|
runCurrent()
|
||||||
lambda.assertions().isCalledOnce()
|
lambda.assertions().isCalledOnce()
|
||||||
|
.with(
|
||||||
|
value(FirebaseConfig.NAME),
|
||||||
|
value("a: A\nb: B"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -46,13 +46,14 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
||||||
* @param instance connection, for multi-account
|
* @param instance connection, for multi-account
|
||||||
*/
|
*/
|
||||||
override fun onMessage(context: Context, message: ByteArray, instance: String) {
|
override fun onMessage(context: Context, message: ByteArray, instance: String) {
|
||||||
Timber.tag(loggerTag.value).d("New message")
|
Timber.tag(loggerTag.value).w("New message")
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val pushData = pushParser.parse(message, instance)
|
val pushData = pushParser.parse(message, instance)
|
||||||
if (pushData == null) {
|
if (pushData == null) {
|
||||||
Timber.tag(loggerTag.value).w("Invalid data received from UnifiedPush")
|
Timber.tag(loggerTag.value).w("Invalid data received from UnifiedPush")
|
||||||
pushHandler.handleInvalid(
|
pushHandler.handleInvalid(
|
||||||
providerInfo = "${UnifiedPushConfig.NAME} - $instance",
|
providerInfo = "${UnifiedPushConfig.NAME} - $instance",
|
||||||
|
data = String(message),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
pushHandler.handle(
|
pushHandler.handle(
|
||||||
|
|
@ -68,7 +69,7 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
||||||
* You should send the endpoint to your application server and sync for missing notifications.
|
* You should send the endpoint to your application server and sync for missing notifications.
|
||||||
*/
|
*/
|
||||||
override fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
|
override fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
|
||||||
Timber.tag(loggerTag.value).i("onNewEndpoint: $endpoint")
|
Timber.tag(loggerTag.value).w("onNewEndpoint: $endpoint")
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val gateway = unifiedPushGatewayResolver.getGateway(endpoint)
|
val gateway = unifiedPushGatewayResolver.getGateway(endpoint)
|
||||||
.let { gatewayResult ->
|
.let { gatewayResult ->
|
||||||
|
|
@ -109,7 +110,7 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
||||||
* Called when this application is unregistered from receiving push messages.
|
* Called when this application is unregistered from receiving push messages.
|
||||||
*/
|
*/
|
||||||
override fun onUnregistered(context: Context, instance: String) {
|
override fun onUnregistered(context: Context, instance: String) {
|
||||||
Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered")
|
Timber.tag(loggerTag.value).w("Unifiedpush: Unregistered")
|
||||||
/*
|
/*
|
||||||
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
|
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
|
||||||
pushDataStore.setFdroidSyncBackgroundMode(mode)
|
pushDataStore.setFdroidSyncBackgroundMode(mode)
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ class VectorUnifiedPushMessagingReceiverTest {
|
||||||
@Test
|
@Test
|
||||||
fun `onMessage invalid invokes the push handler invalid method`() = runTest {
|
fun `onMessage invalid invokes the push handler invalid method`() = runTest {
|
||||||
val context = InstrumentationRegistry.getInstrumentation().context
|
val context = InstrumentationRegistry.getInstrumentation().context
|
||||||
val handleInvalidResult = lambdaRecorder<String, Unit> { }
|
val handleInvalidResult = lambdaRecorder<String, String, Unit> { _, _ -> }
|
||||||
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver(
|
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver(
|
||||||
pushHandler = FakePushHandler(
|
pushHandler = FakePushHandler(
|
||||||
handleInvalidResult = handleInvalidResult,
|
handleInvalidResult = handleInvalidResult,
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,7 @@
|
||||||
package io.element.android.libraries.troubleshoot.impl.history
|
package io.element.android.libraries.troubleshoot.impl.history
|
||||||
|
|
||||||
sealed interface PushHistoryEvents {
|
sealed interface PushHistoryEvents {
|
||||||
data object Reset : PushHistoryEvents
|
data class SetShowOnlyErrors(val showOnlyErrors: Boolean) : PushHistoryEvents
|
||||||
|
data class Reset(val requiresConfirmation: Boolean) : PushHistoryEvents
|
||||||
|
data object ClearDialog : PushHistoryEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,15 @@ package io.element.android.libraries.troubleshoot.impl.history
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.push.api.PushService
|
import io.element.android.libraries.push.api.PushService
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -25,14 +29,36 @@ class PushHistoryPresenter @Inject constructor(
|
||||||
override fun present(): PushHistoryState {
|
override fun present(): PushHistoryState {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val pushCounter by pushService.pushCounter.collectAsState(0)
|
val pushCounter by pushService.pushCounter.collectAsState(0)
|
||||||
val pushHistory by remember {
|
var showOnlyErrors: Boolean by remember { mutableStateOf(false) }
|
||||||
pushService.getPushHistoryItemsFlow()
|
val pushHistory by remember(showOnlyErrors) {
|
||||||
|
pushService.getPushHistoryItemsFlow().map {
|
||||||
|
if (showOnlyErrors) {
|
||||||
|
it.filter { item -> item.hasBeenResolved.not() }
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
}.collectAsState(emptyList())
|
}.collectAsState(emptyList())
|
||||||
|
var resetAction: AsyncAction<Unit> by remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
|
|
||||||
fun handleEvents(event: PushHistoryEvents) {
|
fun handleEvents(event: PushHistoryEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
PushHistoryEvents.Reset -> coroutineScope.launch {
|
is PushHistoryEvents.SetShowOnlyErrors -> {
|
||||||
pushService.resetPushHistory()
|
showOnlyErrors = event.showOnlyErrors
|
||||||
|
}
|
||||||
|
is PushHistoryEvents.Reset -> {
|
||||||
|
if (event.requiresConfirmation) {
|
||||||
|
resetAction = AsyncAction.ConfirmingNoParams
|
||||||
|
} else {
|
||||||
|
resetAction = AsyncAction.Loading
|
||||||
|
coroutineScope.launch {
|
||||||
|
pushService.resetPushHistory()
|
||||||
|
resetAction = AsyncAction.Uninitialized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PushHistoryEvents.ClearDialog -> {
|
||||||
|
resetAction = AsyncAction.Uninitialized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -40,6 +66,8 @@ class PushHistoryPresenter @Inject constructor(
|
||||||
return PushHistoryState(
|
return PushHistoryState(
|
||||||
pushCounter = pushCounter,
|
pushCounter = pushCounter,
|
||||||
pushHistoryItems = pushHistory.toImmutableList(),
|
pushHistoryItems = pushHistory.toImmutableList(),
|
||||||
|
showOnlyErrors = showOnlyErrors,
|
||||||
|
resetAction = resetAction,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,14 @@
|
||||||
|
|
||||||
package io.element.android.libraries.troubleshoot.impl.history
|
package io.element.android.libraries.troubleshoot.impl.history
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.push.api.history.PushHistoryItem
|
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class PushHistoryState(
|
data class PushHistoryState(
|
||||||
val pushCounter: Int,
|
val pushCounter: Int,
|
||||||
val pushHistoryItems: ImmutableList<PushHistoryItem>,
|
val pushHistoryItems: ImmutableList<PushHistoryItem>,
|
||||||
|
val showOnlyErrors: Boolean,
|
||||||
|
val resetAction: AsyncAction<Unit>,
|
||||||
val eventSink: (PushHistoryEvents) -> Unit,
|
val eventSink: (PushHistoryEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
package io.element.android.libraries.troubleshoot.impl.history
|
package io.element.android.libraries.troubleshoot.impl.history
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
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.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
|
@ -36,16 +37,23 @@ open class PushHistoryStateProvider : PreviewParameterProvider<PushHistoryState>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
aPushHistoryState(
|
||||||
|
resetAction = AsyncAction.ConfirmingNoParams,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aPushHistoryState(
|
fun aPushHistoryState(
|
||||||
pushCounter: Int = 0,
|
pushCounter: Int = 0,
|
||||||
pushHistoryItems: List<PushHistoryItem> = emptyList(),
|
pushHistoryItems: List<PushHistoryItem> = emptyList(),
|
||||||
|
showOnlyErrors: Boolean = false,
|
||||||
|
resetAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
eventSink: (PushHistoryEvents) -> Unit = {},
|
eventSink: (PushHistoryEvents) -> Unit = {},
|
||||||
) = PushHistoryState(
|
) = PushHistoryState(
|
||||||
pushCounter = pushCounter,
|
pushCounter = pushCounter,
|
||||||
pushHistoryItems = pushHistoryItems.toImmutableList(),
|
pushHistoryItems = pushHistoryItems.toImmutableList(),
|
||||||
|
showOnlyErrors = showOnlyErrors,
|
||||||
|
resetAction = resetAction,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,10 @@ import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
|
@ -31,16 +35,20 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
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.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
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.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
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.RoomId
|
||||||
|
|
@ -57,6 +65,8 @@ fun PushHistoryView(
|
||||||
onItemClick: (SessionId, RoomId, EventId) -> Unit,
|
onItemClick: (SessionId, RoomId, EventId) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -77,12 +87,42 @@ fun PushHistoryView(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
TextButton(
|
IconButton(onClick = { showMenu = !showMenu }) {
|
||||||
text = stringResource(CommonStrings.action_reset),
|
Icon(
|
||||||
onClick = {
|
imageVector = CompoundIcons.OverflowVertical(),
|
||||||
state.eventSink(PushHistoryEvents.Reset)
|
contentDescription = stringResource(id = CommonStrings.a11y_user_menu),
|
||||||
},
|
)
|
||||||
)
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = showMenu,
|
||||||
|
onDismissRequest = { showMenu = false },
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Show only errors") },
|
||||||
|
trailingIcon = if (state.showOnlyErrors) {
|
||||||
|
{
|
||||||
|
Icon(
|
||||||
|
imageVector = CompoundIcons.CheckCircleSolid(),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
showMenu = false
|
||||||
|
state.eventSink(PushHistoryEvents.SetShowOnlyErrors(state.showOnlyErrors.not()))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(id = CommonStrings.action_reset)) },
|
||||||
|
onClick = {
|
||||||
|
showMenu = false
|
||||||
|
state.eventSink(PushHistoryEvents.Reset(requiresConfirmation = true))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -95,6 +135,22 @@ fun PushHistoryView(
|
||||||
onItemClick = onItemClick,
|
onItemClick = onItemClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AsyncActionView(
|
||||||
|
async = state.resetAction,
|
||||||
|
onSuccess = {},
|
||||||
|
confirmationDialog = {
|
||||||
|
ConfirmationDialog(
|
||||||
|
content = "",
|
||||||
|
title = stringResource(CommonStrings.dialog_title_confirmation),
|
||||||
|
submitText = stringResource(CommonStrings.action_reset),
|
||||||
|
cancelText = stringResource(CommonStrings.action_cancel),
|
||||||
|
onSubmitClick = { state.eventSink(PushHistoryEvents.Reset(requiresConfirmation = false)) },
|
||||||
|
onDismiss = { state.eventSink(PushHistoryEvents.ClearDialog) },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onErrorDismiss = {},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,12 @@
|
||||||
package io.element.android.libraries.troubleshoot.impl.history
|
package io.element.android.libraries.troubleshoot.impl.history
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.push.api.PushService
|
import io.element.android.libraries.push.api.PushService
|
||||||
import io.element.android.libraries.push.test.FakePushService
|
import io.element.android.libraries.push.test.FakePushService
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runCurrent
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -27,6 +27,8 @@ class PushHistoryPresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.pushCounter).isEqualTo(0)
|
assertThat(initialState.pushCounter).isEqualTo(0)
|
||||||
assertThat(initialState.pushHistoryItems).isEmpty()
|
assertThat(initialState.pushHistoryItems).isEmpty()
|
||||||
|
assertThat(initialState.showOnlyErrors).isFalse()
|
||||||
|
assertThat(initialState.resetAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,7 +51,7 @@ class PushHistoryPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - reset`() = runTest {
|
fun `present - reset and cancel`() = runTest {
|
||||||
val resetPushHistoryResult = lambdaRecorder<Unit> { }
|
val resetPushHistoryResult = lambdaRecorder<Unit> { }
|
||||||
val pushService = FakePushService(
|
val pushService = FakePushService(
|
||||||
resetPushHistoryResult = resetPushHistoryResult,
|
resetPushHistoryResult = resetPushHistoryResult,
|
||||||
|
|
@ -59,12 +61,64 @@ class PushHistoryPresenterTest {
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(PushHistoryEvents.Reset)
|
initialState.eventSink(PushHistoryEvents.Reset(requiresConfirmation = true))
|
||||||
runCurrent()
|
assertThat(awaitItem().resetAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
|
initialState.eventSink(PushHistoryEvents.ClearDialog)
|
||||||
|
assertThat(awaitItem().resetAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
|
resetPushHistoryResult.assertions().isNeverCalled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - reset and confirm`() = runTest {
|
||||||
|
val resetPushHistoryResult = lambdaRecorder<Unit> { }
|
||||||
|
val pushService = FakePushService(
|
||||||
|
resetPushHistoryResult = resetPushHistoryResult,
|
||||||
|
)
|
||||||
|
val presenter = createPushHistoryPresenter(
|
||||||
|
pushService = pushService,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(PushHistoryEvents.Reset(requiresConfirmation = true))
|
||||||
|
assertThat(awaitItem().resetAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
|
initialState.eventSink(PushHistoryEvents.Reset(requiresConfirmation = false))
|
||||||
|
assertThat(awaitItem().resetAction).isEqualTo(AsyncAction.Loading)
|
||||||
|
assertThat(awaitItem().resetAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
resetPushHistoryResult.assertions().isCalledOnce()
|
resetPushHistoryResult.assertions().isCalledOnce()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - set show only errors`() = runTest {
|
||||||
|
val pushService = FakePushService()
|
||||||
|
val presenter = createPushHistoryPresenter(
|
||||||
|
pushService = pushService,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.showOnlyErrors).isFalse()
|
||||||
|
val item = aPushHistoryItem(hasBeenResolved = true)
|
||||||
|
val itemError = aPushHistoryItem(hasBeenResolved = false)
|
||||||
|
pushService.emitPushHistoryItems(listOf(item, itemError))
|
||||||
|
awaitItem().let { state ->
|
||||||
|
assertThat(state.pushHistoryItems).containsExactly(item, itemError)
|
||||||
|
state.eventSink(PushHistoryEvents.SetShowOnlyErrors(showOnlyErrors = true))
|
||||||
|
}
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().let { state ->
|
||||||
|
assertThat(state.showOnlyErrors).isTrue()
|
||||||
|
assertThat(state.pushHistoryItems).containsExactly(itemError)
|
||||||
|
state.eventSink(PushHistoryEvents.SetShowOnlyErrors(showOnlyErrors = false))
|
||||||
|
}
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().let { state ->
|
||||||
|
assertThat(state.showOnlyErrors).isFalse()
|
||||||
|
assertThat(state.pushHistoryItems).containsExactly(item, itemError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createPushHistoryPresenter(
|
private fun createPushHistoryPresenter(
|
||||||
pushService: PushService = FakePushService(),
|
pushService: PushService = FakePushService(),
|
||||||
): PushHistoryPresenter {
|
): PushHistoryPresenter {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ package io.element.android.libraries.troubleshoot.impl.history
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
|
@ -46,12 +47,44 @@ class PushHistoryViewTest {
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
val menuContentDescription = rule.activity.getString(CommonStrings.a11y_user_menu)
|
||||||
|
rule.onNodeWithContentDescription(menuContentDescription).performClick()
|
||||||
rule.clickOn(CommonStrings.action_reset)
|
rule.clickOn(CommonStrings.action_reset)
|
||||||
eventsRecorder.assertSingle(PushHistoryEvents.Reset)
|
eventsRecorder.assertSingle(PushHistoryEvents.Reset(requiresConfirmation = true))
|
||||||
// Also check that the push counter is rendered
|
// Also check that the push counter is rendered
|
||||||
rule.onNodeWithText("123").assertExists()
|
rule.onNodeWithText("123").assertExists()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on show only errors sends a PushHistoryEvents(true)`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PushHistoryEvents>()
|
||||||
|
rule.setPushHistoryView(
|
||||||
|
aPushHistoryState(
|
||||||
|
showOnlyErrors = false,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val menuContentDescription = rule.activity.getString(CommonStrings.a11y_user_menu)
|
||||||
|
rule.onNodeWithContentDescription(menuContentDescription).performClick()
|
||||||
|
rule.onNodeWithText("Show only errors").performClick()
|
||||||
|
eventsRecorder.assertSingle(PushHistoryEvents.SetShowOnlyErrors(showOnlyErrors = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on show only errors sends a PushHistoryEvents(false)`() {
|
||||||
|
val eventsRecorder = EventsRecorder<PushHistoryEvents>()
|
||||||
|
rule.setPushHistoryView(
|
||||||
|
aPushHistoryState(
|
||||||
|
showOnlyErrors = true,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val menuContentDescription = rule.activity.getString(CommonStrings.a11y_user_menu)
|
||||||
|
rule.onNodeWithContentDescription(menuContentDescription).performClick()
|
||||||
|
rule.onNodeWithText("Show only errors").performClick()
|
||||||
|
eventsRecorder.assertSingle(PushHistoryEvents.SetShowOnlyErrors(showOnlyErrors = false))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on an invalid event has no effect`() {
|
fun `clicking on an invalid event has no effect`() {
|
||||||
val eventsRecorder = EventsRecorder<PushHistoryEvents>(expectEvents = false)
|
val eventsRecorder = EventsRecorder<PushHistoryEvents>(expectEvents = false)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:04cc3a21bceeee5727df5728d34eb7618a5a90949352cd02f4eaa2be77c47492
|
oid sha256:673fc1787251cbd1f0d6ea6971c1c7a35b4a990e47adbf5b3e2ac442cf74f809
|
||||||
size 13565
|
size 12656
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7a8c0a12277b4335b1d01d3f5f206704b37c4cfb3693ec7d9a26ce15215ba800
|
oid sha256:5a778dc2f98f1ad4ee901e4ca0d9b593cb78f44ab67337350349869f4b211a51
|
||||||
size 45566
|
size 44773
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b32c39383aef9ed41ba65109d4ee5e97f057221474aeeb90522c161d3b0c2409
|
||||||
|
size 21014
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9d877e4d537fa0990dd0f2a5e14f2d00cf8dcd45a4c27fac6c0188ef2b441756
|
oid sha256:97c2af47763d65926e407c94901e1b0a691218b1cf152203fc4fd2f396c17e43
|
||||||
size 13163
|
size 12270
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1f110d1684003cdd1d6e47f765bbf4f55e2b0f3ba732926df08d06297b36bb7b
|
oid sha256:bfb8a1f9bd060141b0f20490d4066724f34d1560296235f5e45f98de9d3d7aad
|
||||||
size 44157
|
size 43310
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:af4e8fb83c87caccd084c0321a5e47451c1a8bba3eed1e948e95d6184667f710
|
||||||
|
size 19339
|
||||||
18
tools/adb/disable_app_standby.sh
Executable file
18
tools/adb/disable_app_standby.sh
Executable file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_your_app_with_app_standby
|
||||||
|
|
||||||
|
echo " => Standby OFF"
|
||||||
|
|
||||||
|
set -x
|
||||||
|
package_name="io.element.android.x.debug"
|
||||||
|
adb shell dumpsys battery reset
|
||||||
|
adb shell am set-inactive "${package_name}" false
|
||||||
|
adb shell am get-inactive "${package_name}"
|
||||||
|
|
||||||
|
tools/adb/print_device_state.sh
|
||||||
16
tools/adb/disable_doze_mode.sh
Executable file
16
tools/adb/disable_doze_mode.sh
Executable file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_doze
|
||||||
|
|
||||||
|
echo " => Disable doze mode"
|
||||||
|
|
||||||
|
set -x
|
||||||
|
adb shell dumpsys deviceidle unforce
|
||||||
|
adb shell dumpsys battery reset
|
||||||
|
|
||||||
|
tools/adb/print_device_state.sh
|
||||||
18
tools/adb/enable_app_standby.sh
Executable file
18
tools/adb/enable_app_standby.sh
Executable file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_your_app_with_app_standby
|
||||||
|
|
||||||
|
echo " => Standby ON"
|
||||||
|
|
||||||
|
set -x
|
||||||
|
package_name="io.element.android.x.debug"
|
||||||
|
adb shell dumpsys battery unplug
|
||||||
|
adb shell am set-inactive "${package_name}" true
|
||||||
|
adb shell am get-inactive "${package_name}"
|
||||||
|
|
||||||
|
tools/adb/print_device_state.sh
|
||||||
16
tools/adb/enable_doze_mode.sh
Executable file
16
tools/adb/enable_doze_mode.sh
Executable file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_doze
|
||||||
|
|
||||||
|
echo " => Enable doze mode"
|
||||||
|
|
||||||
|
set -x
|
||||||
|
adb shell dumpsys battery unplug
|
||||||
|
adb shell dumpsys deviceidle force-idle
|
||||||
|
|
||||||
|
tools/adb/print_device_state.sh
|
||||||
18
tools/adb/print_device_state.sh
Executable file
18
tools/adb/print_device_state.sh
Executable file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_doze
|
||||||
|
|
||||||
|
echo " => Device state"
|
||||||
|
|
||||||
|
set -x
|
||||||
|
adb shell dumpsys deviceidle get light
|
||||||
|
adb shell dumpsys deviceidle get deep
|
||||||
|
adb shell dumpsys deviceidle get force
|
||||||
|
adb shell dumpsys deviceidle get screen
|
||||||
|
adb shell dumpsys deviceidle get charging
|
||||||
|
adb shell dumpsys deviceidle get network
|
||||||
Loading…
Add table
Add a link
Reference in a new issue