diff --git a/echolot-prober/app/src/main/java/app/echo_lot/prober/MainActivity.kt b/echolot-prober/app/src/main/java/app/echo_lot/prober/MainActivity.kt index 3dbb510..c746a98 100644 --- a/echolot-prober/app/src/main/java/app/echo_lot/prober/MainActivity.kt +++ b/echolot-prober/app/src/main/java/app/echo_lot/prober/MainActivity.kt @@ -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() - 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")) } diff --git a/echolot-prober/app/src/main/java/app/echo_lot/prober/ProberViewModel.kt b/echolot-prober/app/src/main/java/app/echo_lot/prober/ProberViewModel.kt new file mode 100644 index 0000000..46ce1d1 --- /dev/null +++ b/echolot-prober/app/src/main/java/app/echo_lot/prober/ProberViewModel.kt @@ -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() + 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) + } + } +}