diff --git a/README.md b/README.md index 72d89f7..22d61dc 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,9 @@ The app is intentionally kept very basic so that the project is easy to maintain * Optionally used for scanning a device's QR code when adding a new device. * `ACCESS_WIFI_STATE`, `ACCESS_COARSE_LOCATION`, `ACCESS_FINE_LOCATION`, `ACCESS_BACKGROUND_LOCATION`, `FOREGROUND_SERVICE_LOCATION` * Optionally used for stopping Syncthing unless connected to specific Wi-Fi networks. +* `SCHEDULE_EXACT_ALARM` + * Optionally used for the time schedule feature. Otherwise, Android may significantly delay both the start and end of the time windows. + * The app will not prompt for this permission because it is only needed when battery optimizations are still enabled, which is strongly discouraged anyway. ## Remote web UI access diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9859815..103b80a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -51,6 +51,9 @@ tools:ignore="BackgroundLocationPolicy" /> + + + diff --git a/app/src/main/java/com/chiller3/basicsync/syncthing/DeviceState.kt b/app/src/main/java/com/chiller3/basicsync/syncthing/DeviceState.kt index 7c44ba7..9c4e0e2 100644 --- a/app/src/main/java/com/chiller3/basicsync/syncthing/DeviceState.kt +++ b/app/src/main/java/com/chiller3/basicsync/syncthing/DeviceState.kt @@ -6,6 +6,7 @@ package com.chiller3.basicsync.syncthing import android.app.AlarmManager +import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.ContentResolver import android.content.Context @@ -26,6 +27,8 @@ 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 import com.chiller3.basicsync.Permissions import com.chiller3.basicsync.Preferences import com.chiller3.basicsync.R @@ -212,6 +215,8 @@ class DeviceStateTracker(private val context: Context) : const val MINIMUM_CYCLE_MS = 2 * MIN_FUTURITY const val MINIMUM_SYNC_MS = MIN_FUTURITY + private var alarmId = 0 + private fun getProxyInfo(): ProxyInfo { val proxyHost = System.getProperty("http.proxyHost") val proxyPort = System.getProperty("http.proxyPort") @@ -401,9 +406,22 @@ class DeviceStateTracker(private val context: Context) : private var autoSyncHandle: Any? = null - private val timeScheduleListener = object : AlarmManager.OnAlarmListener { - override fun onAlarm() { - alarmManager.cancel(this) + private val timeScheduleAction = "${javaClass.canonicalName}.ALARM.$alarmId".apply { + alarmId += 1 + } + private val timeSchedulePendingIntent = PendingIntent.getBroadcast( + context, + 0, + Intent(timeScheduleAction).apply { + setPackage(context.packageName) + }, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + ) + + // The OnAlarmListener variant of setExactAndAllowWhileIdle() isn't available until API 37. + private val timeScheduleReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + alarmManager.cancel(timeSchedulePendingIntent) val canRun = if (prefs.syncSchedule) { val cycleDurationMs = max(prefs.scheduleCycleMs, MINIMUM_CYCLE_MS) @@ -417,15 +435,22 @@ class DeviceStateTracker(private val context: Context) : now + (cycleDurationMs - syncDurationMs) } - alarmManager.set( - AlarmManager.ELAPSED_REALTIME, - wake, - "time_schedule", - this, - handler, - ) + val exact = AlarmManagerCompat.canScheduleExactAlarms(alarmManager) + if (exact) { + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.ELAPSED_REALTIME_WAKEUP, + wake, + timeSchedulePendingIntent, + ) + } else { + alarmManager.setAndAllowWhileIdle( + AlarmManager.ELAPSED_REALTIME_WAKEUP, + wake, + timeSchedulePendingIntent, + ) + } - Log.d(TAG, "Time window updated: now=$now, inWindow=$inWindow, preferredWake=$wake") + Log.d(TAG, "Time window updated: now=$now, inWindow=$inWindow, wake=$wake, exact=$exact") inWindow } else { Log.d(TAG, "Time schedule is disabled") @@ -440,7 +465,7 @@ class DeviceStateTracker(private val context: Context) : Log.d(TAG, "Resetting time schedule listener") state = state.copy(isInTimeWindow = false) - timeScheduleListener.onAlarm() + timeScheduleReceiver.onReceive(context, Intent()) } private val proxyChangeReceiver = object : BroadcastReceiver() { @@ -507,7 +532,13 @@ class DeviceStateTracker(private val context: Context) : autoSyncObserver, ) autoSyncObserver.onStatusChanged(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS) - timeScheduleListener.onAlarm() + ContextCompat.registerReceiver( + context, + timeScheduleReceiver, + IntentFilter(timeScheduleAction), + ContextCompat.RECEIVER_NOT_EXPORTED, + ) + timeScheduleReceiver.onReceive(context, Intent()) context.registerReceiver( proxyChangeReceiver, IntentFilter(Proxy.PROXY_CHANGE_ACTION), @@ -528,7 +559,8 @@ class DeviceStateTracker(private val context: Context) : context.unregisterReceiver(batteryStatusReceiver) context.unregisterReceiver(batterySaverReceiver) ContentResolver.removeStatusChangeListener(autoSyncHandle) - alarmManager.cancel(timeScheduleListener) + context.unregisterReceiver(timeScheduleReceiver) + alarmManager.cancel(timeSchedulePendingIntent) handler.removeCallbacks(timeScheduleReset) context.unregisterReceiver(proxyChangeReceiver)