OXYGENHUB
# 4aee5293461d oxygenhub-android / app / src / main / java / io / oxygenhub / android / runner / PhoneRunner.kt
3.9 KiB · last changed The phone as an OxygenFlow runner, first part (refs #8) 11 days ago source blame history raw
1
package io.oxygenhub.android.runner
2
3
import android.content.Context
4
import android.content.Intent
5
import android.net.ConnectivityManager
6
import android.net.NetworkCapabilities
7
import android.os.BatteryManager
8
import android.os.Build
9
import androidx.core.content.ContextCompat
10
import io.oxygenhub.android.api.HubApi
11
import kotlinx.coroutines.Dispatchers
12
import kotlinx.coroutines.flow.MutableStateFlow
13
import kotlinx.coroutines.flow.StateFlow
14
import kotlinx.coroutines.flow.asStateFlow
15
import kotlinx.coroutines.withContext
16
import uniffi.rust_core.runnerIsRegistered
17
import uniffi.rust_core.runnerRegister
18
import uniffi.rust_core.runnerStop
19
import java.io.File
20
21
/**
22
 * The phone as an OxygenFlow runner (#8): OxygenFlow's own runner, built
23
 * into the app through rust-core, offering the WASM tier. It runs in
24
 * [RunnerService] only while the phone is on charge and on an unmetered
25
 * network (Wi-Fi), and drains when either stops: it claims nothing more
26
 * and finishes what it holds.
27
 */
28
object PhoneRunner {
29
    sealed interface State {
30
        data object Unregistered : State
31
32
        /** Registered, not running. */
33
        data object Stopped : State
34
35
        /** Not allowed to run now, and why (off charge, off Wi-Fi). */
36
        data class Waiting(val why: String) : State
37
38
        data object Running : State
39
40
        /** Told to stop: finishing what it holds. */
41
        data class Draining(val why: String) : State
42
43
        data class Failed(val message: String) : State
44
    }
45
46
    private val _state = MutableStateFlow<State>(State.Unregistered)
47
    val state: StateFlow<State> = _state.asStateFlow()
48
49
    internal fun set(state: State) {
50
        _state.value = state
51
    }
52
53
    /** Where the runner keeps its credential and its jobs' files. */
54
    fun home(context: Context): File = File(context.filesDir, "flow").also { it.mkdirs() }
55
56
    /** Called when the app starts and the service ends: what the runner is, as far as this process knows. */
57
    fun refresh(context: Context) {
58
        val now = _state.value
59
        if (now is State.Running || now is State.Draining) return
60
        _state.value = if (runnerIsRegistered(home(context).path)) {
61
            if (now is State.Waiting || now is State.Failed) now else State.Stopped
62
        } else {
63
            State.Unregistered
64
        }
65
    }
66
67
    /** The runner's name on the hub: the phone's model, as runner names are written. */
68
    fun name(): String =
69
        "android-" + Build.MODEL.lowercase().replace(Regex("[^a-z0-9]+"), "-").trim('-').ifEmpty { "phone" }
70
71
    /** Ask the hub for a token and register this phone as a runner for the account's repositories. */
72
    suspend fun register(context: Context, api: HubApi, hub: String): String {
73
        val token = api.runnerToken()
74
        val name = withContext(Dispatchers.IO) { runnerRegister(home(context).path, hub, token.token, name()) }
75
        _state.value = State.Stopped
76
        return name
77
    }
78
79
    /** Why the runner may not run now, or null when it may: on charge and on Wi-Fi. */
80
    fun blockedBy(context: Context): String? {
81
        val battery = context.getSystemService(BatteryManager::class.java)
82
        if (battery != null && !battery.isCharging) return "the phone is not on charge"
83
        val net = context.getSystemService(ConnectivityManager::class.java)
84
        val caps = net?.activeNetwork?.let { net.getNetworkCapabilities(it) }
85
        if (caps == null || !caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) {
86
            return "the phone is not on Wi-Fi (or another unmetered network)"
87
        }
88
        return null
89
    }
90
91
    fun start(context: Context) {
92
        ContextCompat.startForegroundService(context, Intent(context, RunnerService::class.java))
93
    }
94
95
    /** Drain: claim nothing more, finish and report what it holds, then stop. */
96
    fun drain(why: String) {
97
        if (_state.value == State.Running) {
98
            _state.value = State.Draining(why)
99
            runnerStop()
100
        }
101
    }
102
}