OXYGENHUB
# aa627197ec18 oxygenhub-android / app / src / main / java / io / oxygenhub / android / runner / RunnerCard.kt
5.9 KiB · last changed The phone runner says what it runs, on its card and notification (refs #8); pins OxygenFlow main 7 days ago source blame history raw
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
}