diff --git a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt index c54298cffa..0cdd5a1611 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarter.kt @@ -284,6 +284,7 @@ class SystemBridgeAutoStarter @Inject constructor( // Otherwise, it may not autostart on reboot if it started earlier than when it last auto // started relative to the last boot. preferences.set(Keys.systemBridgeLastAutoStartTime, clock.unixTimestamp()) + preferences.set(Keys.systemBridgeLastAutoStartBootTime, bootTimestamp()) when (type) { AutoStartType.ADB -> { @@ -349,6 +350,8 @@ class SystemBridgeAutoStarter @Inject constructor( private suspend fun isWithinAutoStartCooldown(): Boolean { val lastAutoStartTime = preferences.get(Keys.systemBridgeLastAutoStartTime).first() val lastManualStartTime = preferences.get(Keys.systemBridgeLastManualStartTime).first() + val lastAutoStartBootTime = + preferences.get(Keys.systemBridgeLastAutoStartBootTime).first() val currentTime = clock.unixTimestamp() if (lastAutoStartTime == null) { @@ -362,10 +365,31 @@ class SystemBridgeAutoStarter @Inject constructor( return false } + // If the device has rebooted since the last auto start then the system bridge stopped + // because of the reboot and not because it crashed. The cooldown only exists to prevent + // an infinite restart loop within a single boot session, so ignore it after a reboot. + // Otherwise rebooting twice within 5 minutes wrongly prevents the system bridge from + // auto starting again. + if (lastAutoStartBootTime != null && + bootTimestamp() - lastAutoStartBootTime > REBOOT_DETECTION_TOLERANCE_SEC + ) { + return false + } + return currentTime >= lastAutoStartTime && currentTime - lastAutoStartTime < (5 * 60) } + /** + * The approximate unix time in seconds at which the device last booted, calculated from the + * current wall clock time minus the time elapsed since boot. This value is stable within a + * boot session (aside from small clock adjustments) and jumps forward when the device + * reboots, so it can be used to detect that a reboot has happened. + */ + private fun bootTimestamp(): Long { + return clock.unixTimestamp() - (clock.elapsedRealtime() / 1000) + } + private suspend fun isAutoStartEnabled(): Boolean { return preferences.get(Keys.isSystemBridgeKeepAliveEnabled) .map { it ?: PreferenceDefaults.EXPERT_MODE_KEEP_ALIVE } @@ -451,4 +475,14 @@ class SystemBridgeAutoStarter @Inject constructor( notificationAdapter.showNotification(model) } + + companion object { + /** + * The boot timestamp is only accurate to within a few seconds because the wall clock and + * elapsed realtime are not sampled at exactly the same instant and the clock may be + * adjusted by the system. This tolerance avoids mistaking that jitter for a reboot while + * still reliably detecting a real reboot, which shifts the boot timestamp far more. + */ + private const val REBOOT_DETECTION_TOLERANCE_SEC = 60L + } } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarterTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarterTest.kt index 39f34d3b4a..9f0df80f2e 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarterTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/expertmode/SystemBridgeAutoStarterTest.kt @@ -783,4 +783,33 @@ class SystemBridgeAutoStarterTest { verify(mockConnectionManager, never()).startWithRoot() } } + + @Test + fun `auto start within 5 minutes of the last auto start if the device rebooted in between`() = + runTest(testDispatcher) { + fakePreferences.set(Keys.isSystemBridgeKeepAliveEnabled, true) + fakePreferences.set(Keys.isSystemBridgeUsed, true) + fakePreferences.set(Keys.handledUpgradeToExpertMode, true) + + // The system bridge was last auto started only 2 minutes ago, which is within the + // cooldown, but that was during a previous boot because the device has rebooted since. + fakePreferences.set( + Keys.systemBridgeLastAutoStartTime, + testScopeClock.unixTimestamp() - 120, + ) + fakePreferences.set( + Keys.systemBridgeLastAutoStartBootTime, + // Booted an hour before the current boot, i.e. the device has rebooted since. + testScopeClock.unixTimestamp() - testScopeClock.elapsedRealtime() / 1000 - 3600, + ) + isRootGrantedFlow.value = true + + inOrder(mockConnectionManager) { + systemBridgeAutoStarter.init() + advanceTimeBy(6000) + + // The cooldown is skipped because the device rebooted, so it auto starts. + verify(mockConnectionManager).startWithRoot() + } + } } diff --git a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt index 2e0ec15787..afab7ef6e6 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt @@ -150,6 +150,14 @@ object Keys { longPreferencesKey("key_system_bridge_last_manual_start_time") val systemBridgeLastAutoStartTime = longPreferencesKey("key_system_bridge_last_auto_start_time") + /** + * The unix time in seconds of the last device boot at which the system bridge was auto + * started. Used to detect whether the device has rebooted since the last auto start so that + * the auto start cooldown is not applied after a reboot. + */ + val systemBridgeLastAutoStartBootTime = + longPreferencesKey("key_system_bridge_last_auto_start_boot_time") + val keyEventActionsUseSystemBridge = booleanPreferencesKey("key_key_event_actions_use_system_bridge")