From ca9037ccf3030df287e52b1fa92ab7eb0871eec2 Mon Sep 17 00:00:00 2001 From: Lena Date: Sun, 23 Aug 2026 00:00:00 +0000 Subject: app: read the dashboard off the main thread Every value the dashboard draws comes from an app-private file, and it read all of them during layout, on every resume and at the end of every sync. Scheduler.apply did the same at process start, where it also opens WorkManager's database. Read on a worker thread and draw the result; overlapping refreshes are settled by generation, the same way the log viewer already settles its own. --- app/src/main/java/invalid/lena/rsend/App.kt | 7 ++- .../main/java/invalid/lena/rsend/MainActivity.kt | 50 +++++++++++++++++----- 2 files changed, 44 insertions(+), 13 deletions(-) (limited to 'app/src/main/java') diff --git a/app/src/main/java/invalid/lena/rsend/App.kt b/app/src/main/java/invalid/lena/rsend/App.kt index 0251d7d..443229d 100644 --- a/app/src/main/java/invalid/lena/rsend/App.kt +++ b/app/src/main/java/invalid/lena/rsend/App.kt @@ -3,6 +3,7 @@ package invalid.lena.rsend import android.app.Application import android.app.NotificationChannel import android.app.NotificationManager +import kotlin.concurrent.thread // App creates the notification channel used by the foreground sync service. class App : Application() { @@ -13,8 +14,10 @@ class App : Application() { getSystemService(NotificationManager::class.java).createNotificationChannel(ch) // Apply on every process start, including one launched by an old // WorkManager request after an app upgrade. This retires stale unique - // names without waiting for the dashboard to be opened. - Scheduler.apply(this) + // names without waiting for the dashboard to be opened. Off the main + // thread: it reads the config and opens WorkManager's database, and + // nothing here has to finish before the first screen draws. + thread { Scheduler.apply(this) } } companion object { diff --git a/app/src/main/java/invalid/lena/rsend/MainActivity.kt b/app/src/main/java/invalid/lena/rsend/MainActivity.kt index 875afdf..06270ca 100644 --- a/app/src/main/java/invalid/lena/rsend/MainActivity.kt +++ b/app/src/main/java/invalid/lena/rsend/MainActivity.kt @@ -62,6 +62,10 @@ class MainActivity : AppCompatActivity() { private var jobState: String = "none" private var recoveryShown = false + // Settles overlapping refreshes: a resume and a finishing sync can start + // one each, and only the newest read may draw. + private var generation = 0 + private val notifPerm = registerForActivityResult(ActivityResultContracts.RequestPermission()) { refresh() } @@ -221,15 +225,11 @@ class MainActivity : AppCompatActivity() { override fun onResume() { super.onResume() - Scheduler.apply(this) refresh() - showRecoveryNotice() } - private fun showRecoveryNotice() { - if (recoveryShown) return - val message = Config.recoveryMessage(this).trim() - if (message.isEmpty()) return + private fun showRecoveryNotice(message: String) { + if (recoveryShown || message.isEmpty()) return recoveryShown = true AlertDialog.Builder(this) .setTitle("Configuration recovered") @@ -277,12 +277,40 @@ class MainActivity : AppCompatActivity() { scheduleValue.text = "$sched, job $jobState" } + // One read of everything the dashboard draws. + private data class Dashboard( + val cfg: Config, + val lastSync: String, + val keyExists: Boolean, + val recovery: String, + ) + + // Every field above comes from an app-private file, so read them on a worker + // thread and draw the result. Reading them during layout was the same + // main-thread stall the WorkManager query was moved off. private fun refresh() { - val cfg = Config.load(this) + val current = ++generation + thread { + Scheduler.apply(this) + val d = Dashboard( + cfg = Config.load(this), + lastSync = LastSync.get(this), + keyExists = Keys.exists(this), + recovery = Config.recoveryMessage(this).trim(), + ) + runOnUiThread { + if (isFinishing || isDestroyed || current != generation) return@runOnUiThread + draw(d) + } + } + } - lastSync.text = "Last sync: ${LastSync.get(this)}" + private fun draw(d: Dashboard) { + val cfg = d.cfg - keyValue.text = if (Keys.exists(this)) "Generated, tap to view" else "Not created, tap to create" + lastSync.text = "Last sync: ${d.lastSync}" + + keyValue.text = if (d.keyExists) "Generated, tap to view" else "Not created, tap to create" schedule = cfg.schedule renderSchedule(cfg.schedule) @@ -324,6 +352,8 @@ class MainActivity : AppCompatActivity() { } } } + + showRecoveryNotice(d.recovery) } // addRow inflates one tappable list row: icon, title, subtitle, chip. @@ -451,7 +481,6 @@ class MainActivity : AppCompatActivity() { runOnUiThread { if (isFinishing || isDestroyed) return@runOnUiThread if (error == null) { - Scheduler.apply(this) refresh() Toast.makeText(this, "New key generated.", Toast.LENGTH_SHORT).show() showKey() @@ -497,7 +526,6 @@ class MainActivity : AppCompatActivity() { runOnUiThread { if (isFinishing || isDestroyed) return@runOnUiThread if (error == null) { - Scheduler.apply(this) refresh() Toast.makeText(this, "Key imported.", Toast.LENGTH_SHORT).show() showKey() -- cgit v1.2.3