From 881ae88b7e8bbd73e2e6bfe66b8fd3fffd85beaf Mon Sep 17 00:00:00 2001 From: Yu Date: Wed, 22 Jul 2026 18:11:27 +0300 Subject: [PATCH] fix(expert mode): don't apply auto start cooldown after a reboot The system bridge auto start has a 5 minute cooldown that prevents it from being auto started again if it was last auto started less than 5 minutes ago. This exists to avoid an infinite restart loop when the system bridge keeps being killed shortly after starting. The cooldown was based solely on the wall clock time of the last auto start (systemBridgeLastAutoStartTime), which persists across reboots. So if the user rebooted, used the device for 1-2 minutes and rebooted again, the system bridge was stopped by the reboot (a correct termination, not a crash) but the cooldown still fired. The user got the "expert mode stopped unexpectedly / not auto restarting because last auto started less than 5 minutes ago" notification and the service did not start. Store the device boot time (unix time - elapsed realtime) alongside the last auto start time, and skip the cooldown when the current boot time differs from the stored one, i.e. the device has rebooted since the last auto start. A crash loop within a single boot session still triggers the cooldown because the boot time does not change between kills. Fixes #2195 --- .../expertmode/SystemBridgeAutoStarter.kt | 34 +++++++++++++++++++ .../expertmode/SystemBridgeAutoStarterTest.kt | 29 ++++++++++++++++ .../io/github/sds100/keymapper/data/Keys.kt | 8 +++++ 3 files changed, 71 insertions(+) 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")