prober: survive configuration changes (rotation wiped a finished run)
Found on the OnePlus 15 (CPH2747): rotating after a run emptied the result list — results lived in a MainActivity field and the run in lifecycleScope, so a config change dropped collected results and cancelled remaining probes. Both now live in ProberViewModel (viewModelScope + application context; no probe needs an Activity). A lost run means a lost export, so this matters for a collection tool. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d3e35ecead
commit
6c29d0c039
@@ -11,24 +11,18 @@ import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import app.echo_lot.prober.export.ReportWriter
|
||||
import app.echo_lot.prober.probe.ProbeRegistry
|
||||
import app.echo_lot.prober.probe.ProbeResult
|
||||
import app.echo_lot.prober.probe.Verdict
|
||||
import app.echo_lot.prober.ui.ProberScreen
|
||||
import app.echo_lot.prober.ui.UiState
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
private var state by mutableStateOf(UiState())
|
||||
// Run state lives in the ViewModel so rotation neither cancels a running
|
||||
// probe sequence nor discards collected results.
|
||||
private val vm: ProberViewModel by viewModels()
|
||||
|
||||
private val permissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { /* proceed regardless */ }
|
||||
@@ -40,8 +34,8 @@ class MainActivity : ComponentActivity() {
|
||||
MaterialTheme {
|
||||
Surface {
|
||||
ProberScreen(
|
||||
state = state,
|
||||
onRun = ::runProbes,
|
||||
state = vm.state,
|
||||
onRun = vm::runProbes,
|
||||
onShare = ::shareReport,
|
||||
)
|
||||
}
|
||||
@@ -49,28 +43,9 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun runProbes() {
|
||||
if (state.running) return
|
||||
state = state.copy(running = true, results = emptyList(), currentTitle = null)
|
||||
lifecycleScope.launch {
|
||||
val acc = mutableListOf<ProbeResult>()
|
||||
for (probe in ProbeRegistry.all) {
|
||||
state = state.copy(currentTitle = probe.title)
|
||||
val result = try {
|
||||
probe.run(this@MainActivity)
|
||||
} catch (t: Throwable) {
|
||||
ProbeResult.of(probe, Verdict.ERROR, "Uncaught: ${t.message ?: t.javaClass.simpleName}")
|
||||
}
|
||||
acc.add(result)
|
||||
state = state.copy(results = acc.toList())
|
||||
}
|
||||
state = state.copy(running = false, currentTitle = null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun shareReport() {
|
||||
if (state.results.isEmpty()) return
|
||||
val intent = ReportWriter.share(this, state.results)
|
||||
if (vm.state.results.isEmpty()) return
|
||||
val intent = ReportWriter.share(this, vm.state.results)
|
||||
startActivity(Intent.createChooser(intent, "Export Echolot prober report"))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// SPDX-FileCopyrightText: 2026 Echolot contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app.echo_lot.prober
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.echo_lot.prober.probe.ProbeRegistry
|
||||
import app.echo_lot.prober.probe.ProbeResult
|
||||
import app.echo_lot.prober.probe.Verdict
|
||||
import app.echo_lot.prober.ui.UiState
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Owns the probe run and its results so both survive configuration changes.
|
||||
* Rotating mid-run must neither cancel remaining probes (viewModelScope, not
|
||||
* lifecycleScope) nor drop collected results (ViewModel, not Activity field) —
|
||||
* a lost run means a lost export.
|
||||
*
|
||||
* Probes get the Application context; none of them need an Activity.
|
||||
*/
|
||||
class ProberViewModel(app: Application) : AndroidViewModel(app) {
|
||||
|
||||
var state by mutableStateOf(UiState())
|
||||
private set
|
||||
|
||||
fun runProbes() {
|
||||
if (state.running) return
|
||||
state = state.copy(running = true, results = emptyList(), currentTitle = null)
|
||||
viewModelScope.launch {
|
||||
val acc = mutableListOf<ProbeResult>()
|
||||
for (probe in ProbeRegistry.all) {
|
||||
state = state.copy(currentTitle = probe.title)
|
||||
val result = try {
|
||||
probe.run(getApplication())
|
||||
} catch (t: Throwable) {
|
||||
ProbeResult.of(probe, Verdict.ERROR, "Uncaught: ${t.message ?: t.javaClass.simpleName}")
|
||||
}
|
||||
acc.add(result)
|
||||
state = state.copy(results = acc.toList())
|
||||
}
|
||||
state = state.copy(running = false, currentTitle = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user