From 0d868961e5b47442b56482d528dad0962219c96e Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 17 Sep 2026 22:57:35 +0000 Subject: [PATCH 1/5] feat(sync): show next scheduled sync time in Sync Settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surfaces "when will it sync next?" next to the existing "last sync" status, derived from SyncScheduler's SYNC_INTERVAL_MS and the last completed run — no new persisted state. Part of ActivityWatch/aw-android#274 (the per-peer-names half is tracked separately, blocked on aw-server-rust JNI support). Git-Session-Id: 4f6d --- .../activitywatch/android/SyncScheduler.kt | 4 +- .../android/SyncSettingsActivity.kt | 34 ++++++++++++++ .../res/layout/activity_sync_settings.xml | 12 ++++- .../android/SyncSettingsActivityTest.kt | 46 +++++++++++++++++++ 4 files changed, 94 insertions(+), 2 deletions(-) diff --git a/mobile/src/main/java/net/activitywatch/android/SyncScheduler.kt b/mobile/src/main/java/net/activitywatch/android/SyncScheduler.kt index e396203b..922ffbac 100644 --- a/mobile/src/main/java/net/activitywatch/android/SyncScheduler.kt +++ b/mobile/src/main/java/net/activitywatch/android/SyncScheduler.kt @@ -12,7 +12,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch private const val TAG = "SyncScheduler" -private const val SYNC_INTERVAL_MS = 15 * 60 * 1000L +// internal (not private): SyncSettingsActivity reads this to render "next sync at" without +// duplicating the interval or requiring a data-model change. +internal const val SYNC_INTERVAL_MS = 15 * 60 * 1000L private const val ACTION_SYNC_ALARM = "net.activitywatch.android.SYNC_ALARM" class SyncScheduler(private val context: Context) { diff --git a/mobile/src/main/java/net/activitywatch/android/SyncSettingsActivity.kt b/mobile/src/main/java/net/activitywatch/android/SyncSettingsActivity.kt index dace8df5..7c629ed4 100644 --- a/mobile/src/main/java/net/activitywatch/android/SyncSettingsActivity.kt +++ b/mobile/src/main/java/net/activitywatch/android/SyncSettingsActivity.kt @@ -43,6 +43,27 @@ internal fun formatSyncStatus(status: SyncStatus?, dateFormat: DateFormat): Stri return "$headline\n${formatSyncDetail(status)}" } +/** + * "When will it sync next?" — derived from the existing SyncScheduler interval and the + * last completed run, with no new persisted state. The scheduler always reschedules + * SYNC_INTERVAL_MS after the previous run completes (SyncScheduler.performSync), so + * lastCompletedAt + SYNC_INTERVAL_MS is the real next-run time whenever sync is enabled. + */ +internal fun formatNextSyncStatus( + enabled: Boolean, + lastStatus: SyncStatus?, + dateFormat: DateFormat, + now: Long = System.currentTimeMillis(), +): String { + if (!enabled) return "Next sync: sync is disabled" + if (lastStatus == null) { + return "Next sync: shortly (first sync runs about a minute after ActivityWatch starts)" + } + val nextAt = lastStatus.completedAt + SYNC_INTERVAL_MS + if (nextAt <= now) return "Next sync: due now" + return "Next sync: ${dateFormat.format(Date(nextAt))}" +} + /** * The per-run facts line: what moved and which peers it came from. Without * this, a pass that transferred nothing is indistinguishable from one that @@ -69,6 +90,7 @@ class SyncSettingsActivity : AppCompatActivity() { private lateinit var switchSyncEnabled: SwitchCompat private lateinit var tvSyncDirStatus: TextView private lateinit var tvLastSyncStatus: TextView + private lateinit var tvNextSyncStatus: TextView private lateinit var btnChooseDir: Button // Guards against the switch listener firing when we set isChecked programmatically @@ -78,6 +100,7 @@ class SyncSettingsActivity : AppCompatActivity() { override fun onReceive(context: Context, intent: Intent) { if (intent.action == AWPreferences.LAST_SYNC_STATUS_CHANGED_ACTION) { updateLastSyncStatus() + updateNextSyncStatus() } } } @@ -147,6 +170,7 @@ class SyncSettingsActivity : AppCompatActivity() { switchSyncEnabled = findViewById(R.id.switch_sync_enabled) tvSyncDirStatus = findViewById(R.id.tv_sync_dir_status) tvLastSyncStatus = findViewById(R.id.tv_last_sync_status) + tvNextSyncStatus = findViewById(R.id.tv_next_sync_status) btnChooseDir = findViewById(R.id.btn_choose_sync_dir) refreshUI() @@ -160,6 +184,7 @@ class SyncSettingsActivity : AppCompatActivity() { action = BackgroundService.ACTION_SYNC_ENABLED_CHANGED putExtra(BackgroundService.EXTRA_START_ORIGIN, BackgroundService.START_ORIGIN_SETTINGS) }) + updateNextSyncStatus() } btnChooseDir.setOnClickListener { @@ -197,6 +222,7 @@ class SyncSettingsActivity : AppCompatActivity() { isUpdatingSwitch = false updateSyncDirStatus() updateLastSyncStatus() + updateNextSyncStatus() } private fun updateLastSyncStatus() { @@ -206,6 +232,14 @@ class SyncSettingsActivity : AppCompatActivity() { ) } + private fun updateNextSyncStatus() { + tvNextSyncStatus.text = formatNextSyncStatus( + prefs.isSyncEnabled(), + prefs.getLastSyncStatus(), + combinedDateTimeFormat(), + ) + } + private fun combinedDateTimeFormat(): DateFormat { val dateFormat = android.text.format.DateFormat.getMediumDateFormat(this) val timeFormat = android.text.format.DateFormat.getTimeFormat(this) diff --git a/mobile/src/main/res/layout/activity_sync_settings.xml b/mobile/src/main/res/layout/activity_sync_settings.xml index 7cdd1204..4bc95d70 100644 --- a/mobile/src/main/res/layout/activity_sync_settings.xml +++ b/mobile/src/main/res/layout/activity_sync_settings.xml @@ -47,11 +47,21 @@ android:id="@+id/tv_last_sync_status" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="16dp" + android:layout_marginBottom="8dp" android:text="Last sync: never" android:textAppearance="?android:attr/textAppearanceSmall" android:textColor="?android:attr/textColorSecondary" /> + + +