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.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
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.core.content.ContextCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import app.echo_lot.prober.export.ReportWriter
|
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.ProberScreen
|
||||||
import app.echo_lot.prober.ui.UiState
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
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 =
|
private val permissionLauncher =
|
||||||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { /* proceed regardless */ }
|
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { /* proceed regardless */ }
|
||||||
@@ -40,8 +34,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
Surface {
|
Surface {
|
||||||
ProberScreen(
|
ProberScreen(
|
||||||
state = state,
|
state = vm.state,
|
||||||
onRun = ::runProbes,
|
onRun = vm::runProbes,
|
||||||
onShare = ::shareReport,
|
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() {
|
private fun shareReport() {
|
||||||
if (state.results.isEmpty()) return
|
if (vm.state.results.isEmpty()) return
|
||||||
val intent = ReportWriter.share(this, state.results)
|
val intent = ReportWriter.share(this, vm.state.results)
|
||||||
startActivity(Intent.createChooser(intent, "Export Echolot prober report"))
|
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