Coverage / app/src/main/java/io/oxygenhub/android/runner/RunnerCard.kt 0.0%
0 of 59 lines run, 0 of 18 functions, 0 of 42 branches · run #1662 on main · run missed
| 1 | package io.oxygenhub.android.runner | |
| 2 | ||
| 3 | import android.Manifest | |
| 4 | import android.content.pm.PackageManager | |
| 5 | import android.os.Build | |
| 6 | import androidx.activity.compose.rememberLauncherForActivityResult | |
| 7 | import androidx.activity.result.contract.ActivityResultContracts | |
| 8 | import androidx.compose.foundation.layout.Arrangement | |
| 9 | import androidx.compose.foundation.layout.Column | |
| 10 | import androidx.compose.foundation.layout.Row | |
| 11 | import androidx.compose.foundation.layout.fillMaxWidth | |
| 12 | import androidx.compose.foundation.layout.padding | |
| 13 | import androidx.compose.material3.Button | |
| 14 | import androidx.compose.material3.MaterialTheme | |
| 15 | import androidx.compose.material3.OutlinedButton | |
| 16 | import androidx.compose.material3.Text | |
| 17 | import androidx.compose.runtime.Composable | |
| 18 | import androidx.compose.runtime.LaunchedEffect | |
| 19 | import androidx.compose.runtime.getValue | |
| 20 | import androidx.compose.runtime.mutableStateOf | |
| 21 | import androidx.compose.runtime.remember | |
| 22 | import androidx.compose.runtime.rememberCoroutineScope | |
| 23 | import androidx.compose.runtime.setValue | |
| 24 | import androidx.compose.ui.Modifier | |
| 25 | import androidx.compose.ui.platform.LocalContext | |
| 26 | import androidx.compose.ui.unit.dp | |
| 27 | import androidx.core.content.ContextCompat | |
| 28 | import androidx.lifecycle.compose.collectAsStateWithLifecycle | |
| 29 | import io.oxygenhub.android.api.HubApi | |
| 30 | import io.oxygenhub.android.api.userMessage | |
| 31 | import io.oxygenhub.android.ui.common.Panel | |
| 32 | import io.oxygenhub.android.ui.theme.LocalStatusColors | |
| 33 | import kotlinx.coroutines.CancellationException | |
| 34 | import kotlinx.coroutines.launch | |
| 35 | ||
| 36 | /** | |
| 37 | * The phone as an OxygenFlow runner (#8), on the Account screen: offer it | |
| 38 | * (one tap: a registration token from the hub, then the runner starts), | |
| 39 | * see what it is doing, stop it. | |
| 40 | */ | |
| 41 | @Composable | |
| 42 | fun RunnerCard(api: HubApi, hub: String, canWrite: Boolean) { | |
| 43 | val context = LocalContext.current | |
| 44 | val state by PhoneRunner.state.collectAsStateWithLifecycle() | |
| 45 | val jobs by PhoneRunner.jobs.collectAsStateWithLifecycle() | |
| 46 | var busy by remember { mutableStateOf(false) } | |
| 47 | var error by remember { mutableStateOf<String?>(null) } | |
| 48 | val scope = rememberCoroutineScope() | |
| 49 | LaunchedEffect(Unit) { PhoneRunner.refresh(context) } | |
| 50 | // The notification that says the runner works; the runner runs without it too. | |
| 51 | val ask = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { PhoneRunner.start(context) } | |
| 52 | val start = { | |
| 53 | val granted = Build.VERSION.SDK_INT < 33 || | |
| 54 | ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED | |
| 55 | if (granted) PhoneRunner.start(context) else ask.launch(Manifest.permission.POST_NOTIFICATIONS) | |
| 56 | } | |
| 57 | val status = LocalStatusColors.current | |
| 58 | Panel(Modifier.fillMaxWidth()) { | |
| 59 | Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { | |
| 60 | Text("OxygenFlow runner", style = MaterialTheme.typography.titleMedium) | |
| 61 | val (line, colour) = when (val s = state) { | |
| 62 | PhoneRunner.State.Unregistered -> "Run your repositories' WASM jobs on this phone, while it is charging on Wi-Fi and cool." to MaterialTheme.colorScheme.onSurfaceVariant | |
| 63 | PhoneRunner.State.Stopped -> "Registered as ${PhoneRunner.name()}; not running." to MaterialTheme.colorScheme.onSurfaceVariant | |
| 64 | is PhoneRunner.State.Waiting -> "Not running: ${s.why}." to MaterialTheme.colorScheme.onSurfaceVariant | |
| 65 | PhoneRunner.State.Running -> "Running as ${PhoneRunner.name()}: taking WASM jobs." to status.ok | |
| 66 | is PhoneRunner.State.Draining -> "Draining (${s.why}): finishing its job, then stopping." to status.warn | |
| 67 | is PhoneRunner.State.Leaving -> if (s.noticeSecs > 0) { | |
| 68 | "Leaving (${s.why}): finishing a job due within ${s.noticeSecs} s, handing back any other, then stopping." to status.warn | |
| 69 | } else { | |
| 70 | "Leaving (${s.why}): handing its job back for another runner, then stopping." to status.warn | |
| 71 | } | |
| 72 | is PhoneRunner.State.Failed -> "Stopped: ${s.message}" to status.bad | |
| 73 | } | |
| 74 | Text(line, style = MaterialTheme.typography.bodyMedium, color = colour) | |
| 75 | // What it runs now (#8). | |
| 76 | jobs.forEach { Text("▸ $it", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) } | |
| 77 | error?.let { Text(it, style = MaterialTheme.typography.bodyMedium, color = status.bad) } | |
| 78 | Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { | |
| 79 | when (state) { | |
| 80 | PhoneRunner.State.Unregistered -> Button( | |
| 81 | enabled = canWrite && !busy, | |
| 82 | onClick = { | |
| 83 | busy = true | |
| 84 | error = null | |
| 85 | scope.launch { | |
| 86 | try { | |
| 87 | PhoneRunner.register(context, api, hub) | |
| 88 | start() | |
| 89 | } catch (e: CancellationException) { | |
| 90 | throw e | |
| 91 | } catch (e: Exception) { | |
| 92 | error = e.userMessage() | |
| 93 | } finally { | |
| 94 | busy = false | |
| 95 | } | |
| 96 | } | |
| 97 | }, | |
| 98 | ) { Text("Offer this phone as a runner") } | |
| 99 | PhoneRunner.State.Running, is PhoneRunner.State.Waiting -> OutlinedButton(onClick = { PhoneRunner.stopByOwner(context) }) { Text("Stop") } | |
| 100 | is PhoneRunner.State.Draining, is PhoneRunner.State.Leaving -> {} | |
| 101 | else -> Button(onClick = { start() }) { Text("Start") } | |
| 102 | } | |
| 103 | } | |
| 104 | if (!canWrite && state == PhoneRunner.State.Unregistered) { | |
| 105 | Text("Sign in again to allow it: offering a runner needs repo:write.", style = MaterialTheme.typography.bodySmall) | |
| 106 | } | |
| 107 | } | |
| 108 | } | |
| 109 | } |