OXYGENHUB
# ff65ea456ff1 oxygenhub-android / app / src / main / java / io / oxygenhub / android / runner / RunnerCard.kt
5.3 KiB · last changed Notifications when a proposal can be landed; the runner comes back after an update or restart (closes #20, refs #8) 10 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.ElevatedCard
15
import androidx.compose.material3.MaterialTheme
16
import androidx.compose.material3.OutlinedButton
17
import androidx.compose.material3.Text
18
import androidx.compose.runtime.Composable
19
import androidx.compose.runtime.LaunchedEffect
20
import androidx.compose.runtime.getValue
21
import androidx.compose.runtime.mutableStateOf
22
import androidx.compose.runtime.remember
23
import androidx.compose.runtime.rememberCoroutineScope
24
import androidx.compose.runtime.setValue
25
import androidx.compose.ui.Modifier
26
import androidx.compose.ui.platform.LocalContext
27
import androidx.compose.ui.unit.dp
28
import androidx.core.content.ContextCompat
29
import androidx.lifecycle.compose.collectAsStateWithLifecycle
30
import io.oxygenhub.android.api.HubApi
31
import io.oxygenhub.android.api.userMessage
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
    ElevatedCard(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.Failed -> "Stopped: ${s.message}" to status.bad
67
            }
68
            Text(line, style = MaterialTheme.typography.bodyMedium, color = colour)
69
            error?.let { Text(it, style = MaterialTheme.typography.bodyMedium, color = status.bad) }
70
            Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
71
                when (state) {
72
                    PhoneRunner.State.Unregistered -> Button(
73
                        enabled = canWrite && !busy,
74
                        onClick = {
75
                            busy = true
76
                            error = null
77
                            scope.launch {
78
                                try {
79
                                    PhoneRunner.register(context, api, hub)
80
                                    start()
81
                                } catch (e: CancellationException) {
82
                                    throw e
83
                                } catch (e: Exception) {
84
                                    error = e.userMessage()
85
                                } finally {
86
                                    busy = false
87
                                }
88
                            }
89
                        },
90
                    ) { Text("Offer this phone as a runner") }
91
                    PhoneRunner.State.Running -> OutlinedButton(onClick = { PhoneRunner.stopByOwner(context) }) { Text("Stop") }
92
                    is PhoneRunner.State.Draining -> {}
93
                    else -> Button(onClick = { start() }) { Text("Start") }
94
                }
95
            }
96
            if (!canWrite && state == PhoneRunner.State.Unregistered) {
97
                Text("Sign in again to allow it: offering a runner needs repo:write.", style = MaterialTheme.typography.bodySmall)
98
            }
99
        }
100
    }
101
}