OXYGENHUB
◆ v0.1.1 oxygenhub-android / app / src / main / java / io / oxygenhub / android / runner / RunnerCard.kt
5.7 KiB · last changed Panels you can see: the hub's palette by default, clear surface steps and a 1 dp edge on every panel (closes #24) 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
    var busy by remember { mutableStateOf(false) }
46
    var error by remember { mutableStateOf<String?>(null) }
47
    val scope = rememberCoroutineScope()
48
    LaunchedEffect(Unit) { PhoneRunner.refresh(context) }
49
    // The notification that says the runner works; the runner runs without it too.
50
    val ask = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { PhoneRunner.start(context) }
51
    val start = {
52
        val granted = Build.VERSION.SDK_INT < 33 ||
53
            ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
54
        if (granted) PhoneRunner.start(context) else ask.launch(Manifest.permission.POST_NOTIFICATIONS)
55
    }
56
    val status = LocalStatusColors.current
57
    Panel(Modifier.fillMaxWidth()) {
58
        Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
59
            Text("OxygenFlow runner", style = MaterialTheme.typography.titleMedium)
60
            val (line, colour) = when (val s = state) {
61
                PhoneRunner.State.Unregistered -> "Run your repositories' WASM jobs on this phone, while it is charging on Wi-Fi." to MaterialTheme.colorScheme.onSurfaceVariant
62
                PhoneRunner.State.Stopped -> "Registered as ${PhoneRunner.name()}; not running." to MaterialTheme.colorScheme.onSurfaceVariant
63
                is PhoneRunner.State.Waiting -> "Not running: ${s.why}." to MaterialTheme.colorScheme.onSurfaceVariant
64
                PhoneRunner.State.Running -> "Running as ${PhoneRunner.name()}: taking WASM jobs." to status.ok
65
                is PhoneRunner.State.Draining -> "Draining (${s.why}): finishing its job, then stopping." to status.warn
66
                is PhoneRunner.State.Leaving -> if (s.noticeSecs > 0) {
67
                    "Leaving (${s.why}): finishing a job due within ${s.noticeSecs} s, handing back any other, then stopping." to status.warn
68
                } else {
69
                    "Leaving (${s.why}): handing its job back for another runner, then stopping." to status.warn
70
                }
71
                is PhoneRunner.State.Failed -> "Stopped: ${s.message}" to status.bad
72
            }
73
            Text(line, style = MaterialTheme.typography.bodyMedium, color = colour)
74
            error?.let { Text(it, style = MaterialTheme.typography.bodyMedium, color = status.bad) }
75
            Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
76
                when (state) {
77
                    PhoneRunner.State.Unregistered -> Button(
78
                        enabled = canWrite && !busy,
79
                        onClick = {
80
                            busy = true
81
                            error = null
82
                            scope.launch {
83
                                try {
84
                                    PhoneRunner.register(context, api, hub)
85
                                    start()
86
                                } catch (e: CancellationException) {
87
                                    throw e
88
                                } catch (e: Exception) {
89
                                    error = e.userMessage()
90
                                } finally {
91
                                    busy = false
92
                                }
93
                            }
94
                        },
95
                    ) { Text("Offer this phone as a runner") }
96
                    PhoneRunner.State.Running, is PhoneRunner.State.Waiting -> OutlinedButton(onClick = { PhoneRunner.stopByOwner(context) }) { Text("Stop") }
97
                    is PhoneRunner.State.Draining, is PhoneRunner.State.Leaving -> {}
98
                    else -> Button(onClick = { start() }) { Text("Start") }
99
                }
100
            }
101
            if (!canWrite && state == PhoneRunner.State.Unregistered) {
102
                Text("Sign in again to allow it: offering a runner needs repo:write.", style = MaterialTheme.typography.bodySmall)
103
            }
104
        }
105
    }
106
}