Skip to content
Merged
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
159 changes: 81 additions & 78 deletions app/src/main/java/com/chiller3/basicsync/syncthing/DeviceState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
import android.os.SystemClock
import android.util.Log
import androidx.core.app.AlarmManagerCompat
import androidx.core.content.ContextCompat
Expand Down Expand Up @@ -95,7 +94,7 @@ data class DeviceState(
val isBatterySaver: Boolean = false,
val isAutoSyncData: Boolean = false,
val isInTimeWindow: Boolean = false,
val isIdleExpired: Boolean = false,
val idleStart: Long = -1,
val busyFolders: Int = 0,
val connectedDevices: Int = 0,
val connectedOnce: Boolean = false,
Expand Down Expand Up @@ -190,11 +189,11 @@ data class DeviceState(
reasons.add(BlockedReason.AUTO_SYNC_DATA)
}

if (!isInTimeWindow || isIdleExpired) {
if (!isInTimeWindow) {
if (prefs.syncScheduleBatteryOnly && isPluggedIn) {
Log.d(TAG, "Ignoring sync schedule because device is plugged in")
} else {
Log.d(TAG, "Blocked due to execution time window (idleExpired=${isIdleExpired})")
Log.d(TAG, "Blocked due to execution time window")
reasons.add(BlockedReason.TIME_SCHEDULE)
}
}
Expand Down Expand Up @@ -428,91 +427,101 @@ class DeviceStateTracker(private val context: Context) :

private var autoSyncHandle: Any? = null

private val scheduleCycleAction = newAlarmAction()
private val scheduleIdleAction = newAlarmAction()
private val scheduleCyclePendingIntent = alarmPendingIntent(context, scheduleCycleAction)
private val scheduleIdlePendingIntent = alarmPendingIntent(context, scheduleIdleAction)
private val scheduleAction = newAlarmAction()
private val schedulePendingIntent = alarmPendingIntent(context, scheduleAction)

// The OnAlarmListener variant of setExactAndAllowWhileIdle() isn't available until API 37.
private val scheduleReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
scheduleCycleAction -> {
alarmManager.cancel(scheduleCyclePendingIntent)
alarmManager.cancel(scheduleIdlePendingIntent)
// This is idempotent at a given point in time.
scheduleAction -> {
alarmManager.cancel(schedulePendingIntent)

val canRun = if (prefs.syncSchedule) {
val cycleDurationMs = max(prefs.scheduleCycleMs, MINIMUM_CYCLE_MS)
val syncDurationMs = max(prefs.scheduleSyncMs, MINIMUM_SYNC_MS)
val inWindow = !state.isInTimeWindow
val syncIdleMs = max(prefs.scheduleIdleMs, MINIMUM_IDLE_MS)

val now = System.currentTimeMillis()
val windowStart = now / cycleDurationMs * cycleDurationMs
var windowEnd = windowStart + syncDurationMs
var endIsIdle = false
if (state.idleStart >= 0) {
val idleEnd = state.idleStart + syncIdleMs
if (idleEnd in windowStart until windowEnd) {
windowEnd = idleEnd
endIsIdle = true
}
}

val inWindow = now in windowStart until windowEnd

val (label, nextCheckMs) = if (!inWindow) {
"window_start" to windowStart + cycleDurationMs
} else if (!endIsIdle) {
"window_end" to windowEnd
} else {
"idle_end" to windowEnd
}

val (label, durationMs) = if (inWindow) {
"sync" to syncDurationMs
val exact = AlarmManagerCompat.canScheduleExactAlarms(alarmManager)
if (exact) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
nextCheckMs,
schedulePendingIntent,
)
} else {
"off" to (cycleDurationMs - syncDurationMs)
alarmManager.setAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
nextCheckMs,
schedulePendingIntent,
)
}

scheduleAlarm(label, scheduleCyclePendingIntent, durationMs.toLong())
Log.d(TAG, "Scheduled next alarm: for=$label, wait=${nextCheckMs - now}, " +
"inWindow=$inWindow, exact=$exact")

inWindow
} else {
Log.d(TAG, "Sync schedule is disabled")
true
}

if (canRun) {
scheduleIdleTimer()
if (!state.isInTimeWindow && canRun) {
state = state.copy(
isInTimeWindow = true,
idleStart = -1,
connectedOnce = false,
)
} else if (state.isInTimeWindow && !canRun) {
state = state.copy(isInTimeWindow = false)
}

state = state.copy(
isInTimeWindow = canRun,
isIdleExpired = false,
connectedOnce = false,
)
}
scheduleIdleAction -> {
Log.d(TAG, "Idle window expired")

state = state.copy(isIdleExpired = true)
}
}
}
}

private fun scheduleAlarm(type: String, pendingIntent: PendingIntent, sleepMs: Long) {
val now = SystemClock.elapsedRealtime()
val wake = now + sleepMs

val exact = AlarmManagerCompat.canScheduleExactAlarms(alarmManager)
if (exact) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
wake,
pendingIntent,
)
private fun updateIdleState() {
var idleStart = -1L

if (!prefs.syncSchedule || !prefs.syncScheduleIdle) {
Log.d(TAG, "Idle schedule is disabled")
} else if (!state.isInTimeWindow) {
Log.d(TAG, "Outside of window; ignoring change in idle state")
} else if (!state.connectedOnce) {
Log.d(TAG, "Waiting for initial connection")
} else if (state.busyFolders > 0) {
Log.d(TAG, "Became busy")
} else if (state.idleStart < 0) {
Log.d(TAG, "Became idle")
idleStart = System.currentTimeMillis()
} else {
alarmManager.setAndAllowWhileIdle(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
wake,
pendingIntent,
)
Log.d(TAG, "Already idle")
}

Log.d(TAG, "Scheduled alarm: type=$type, now=$now, wake=$wake, exact=$exact")
}

private fun scheduleIdleTimer() {
val enabled = prefs.syncSchedule && prefs.syncScheduleIdle
val isIdle = state.busyFolders == 0
val connectedOnce = state.connectedOnce

if (enabled && isIdle && connectedOnce) {
val syncIdleMs = max(prefs.scheduleIdleMs, MINIMUM_IDLE_MS)

scheduleAlarm("idle", scheduleIdlePendingIntent, syncIdleMs.toLong())
} else {
Log.d(TAG, "Not scheduling idle timer: enabled=$enabled, idle=$isIdle, connectedOnce=$connectedOnce")
}
state = state.copy(idleStart = idleStart)
}

fun updateBusyFolders(folderStates: SyncthingService.FolderStates) {
Expand All @@ -521,12 +530,10 @@ class DeviceStateTracker(private val context: Context) :

handler.post {
if (state.busyFolders != count) {
alarmManager.cancel(scheduleIdlePendingIntent)

Log.d(TAG, "Busy folders updated: count=$count")

state = state.copy(busyFolders = count)
scheduleIdleTimer()
scheduleRefresh.run()
}
}
}
Expand All @@ -548,17 +555,17 @@ class DeviceStateTracker(private val context: Context) :
// After a single device connects, the idle timer is based solely on whether all
// folders are idle. Devices disconnecting do not affect the timer.
if (!connectedBefore && count > 0) {
scheduleIdleTimer()
scheduleRefresh.run()
}
}
}
}

private val scheduleReset = Runnable {
Log.d(TAG, "Resetting sync schedule listener")
private val scheduleRefresh = Runnable {
Log.d(TAG, "Refreshing sync schedule")

state = state.copy(isInTimeWindow = false, isIdleExpired = false)
scheduleReceiver.onReceive(context, Intent(scheduleCycleAction))
updateIdleState()
scheduleReceiver.onReceive(context, Intent(scheduleAction))
}

private val proxyChangeReceiver = object : BroadcastReceiver() {
Expand Down Expand Up @@ -599,8 +606,8 @@ class DeviceStateTracker(private val context: Context) :
Log.d(TAG, "Preference $key changed")

// Try to debounce since these are generally changed at the same time.
handler.removeCallbacks(scheduleReset)
handler.post(scheduleReset)
handler.removeCallbacks(scheduleRefresh)
handler.post(scheduleRefresh)
}
// PREF_ALLOWED_WIFI_NETWORKS is checked in SyncthingService because we need the service
// to request the location foreground permission first.
Expand Down Expand Up @@ -630,13 +637,10 @@ class DeviceStateTracker(private val context: Context) :
ContextCompat.registerReceiver(
context,
scheduleReceiver,
IntentFilter().apply {
addAction(scheduleCycleAction)
addAction(scheduleIdleAction)
},
IntentFilter(scheduleAction),
ContextCompat.RECEIVER_NOT_EXPORTED,
)
scheduleReceiver.onReceive(context, Intent(scheduleCycleAction))
scheduleReceiver.onReceive(context, Intent(scheduleAction))
context.registerReceiver(
proxyChangeReceiver,
IntentFilter(Proxy.PROXY_CHANGE_ACTION),
Expand All @@ -658,9 +662,8 @@ class DeviceStateTracker(private val context: Context) :
context.unregisterReceiver(batterySaverReceiver)
ContentResolver.removeStatusChangeListener(autoSyncHandle)
context.unregisterReceiver(scheduleReceiver)
alarmManager.cancel(scheduleCyclePendingIntent)
alarmManager.cancel(scheduleIdlePendingIntent)
handler.removeCallbacks(scheduleReset)
alarmManager.cancel(schedulePendingIntent)
handler.removeCallbacks(scheduleRefresh)
context.unregisterReceiver(proxyChangeReceiver)

this.listener = null
Expand Down