Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 -> {
Expand Down Expand Up @@ -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) {
Expand All @@ -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 }
Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
}
8 changes: 8 additions & 0 deletions data/src/main/java/io/github/sds100/keymapper/data/Keys.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down