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:
Benoit Marty 2025-04-16 16:37:32 +02:00 committed by GitHub
parent 505d0b411d
commit f916e4e3d4
30 changed files with 388 additions and 58 deletions

View file

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

View file

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

View file

@ -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)
} }
/** /**

View file

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

View file

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

View file

@ -89,7 +89,7 @@ class FakePushService(
pushCounterFlow.value = counter pushCounterFlow.value = counter
} }
override suspend fun resetPushHistory() { override suspend fun resetPushHistory() = simulateLongTask {
resetPushHistoryResult() resetPushHistoryResult()
} }
} }

View file

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

View file

@ -15,5 +15,6 @@ interface PushHandler {
suspend fun handleInvalid( suspend fun handleInvalid(
providerInfo: String, providerInfo: String,
data: String,
) )
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b32c39383aef9ed41ba65109d4ee5e97f057221474aeeb90522c161d3b0c2409
size 21014

View file

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

View file

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

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:af4e8fb83c87caccd084c0321a5e47451c1a8bba3eed1e948e95d6184667f710
size 19339

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