OXYGENHUB
# dfbc3e972872 oxygenhub-android / app / src / main / java / io / oxygenhub / android / runner / RunnerService.kt
5.2 KiB · last changed The phone as an OxygenFlow runner, first part (refs #8) 10 days ago source blame history raw
1
package io.oxygenhub.android.runner
2
3
import android.app.Notification
4
import android.app.NotificationChannel
5
import android.app.NotificationManager
6
import android.app.PendingIntent
7
import android.app.Service
8
import android.content.BroadcastReceiver
9
import android.content.Context
10
import android.content.Intent
11
import android.content.IntentFilter
12
import android.content.pm.ServiceInfo
13
import android.net.ConnectivityManager
14
import android.net.Network
15
import android.net.NetworkCapabilities
16
import android.os.Build
17
import android.os.IBinder
18
import io.oxygenhub.android.MainActivity
19
import io.oxygenhub.android.R
20
import uniffi.rust_core.runnerServe
21
import uniffi.rust_core.runnerStop
22
23
/**
24
 * Runs the phone's OxygenFlow runner (#8) in the foreground, with a
25
 * notification saying so, while the phone is on charge and on Wi-Fi.
26
 * Leaving the charger or the network drains it: it claims nothing more,
27
 * finishes and reports the job it holds, and the service stops.
28
 */
29
class RunnerService : Service() {
30
    private var serving: Thread? = null
31
    private var connectivity: ConnectivityManager.NetworkCallback? = null
32
33
    private val power = object : BroadcastReceiver() {
34
        override fun onReceive(context: Context, intent: Intent) {
35
            PhoneRunner.drain("taken off charge")
36
            notify("Draining: taken off charge")
37
        }
38
    }
39
40
    override fun onBind(intent: Intent?): IBinder? = null
41
42
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
43
        // The special-use type is Android 14's; before it, the manifest's type stands.
44
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
45
            startForeground(NOTIFICATION, notification("Starting"), ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
46
        } else {
47
            startForeground(NOTIFICATION, notification("Starting"))
48
        }
49
        if (serving != null) return START_NOT_STICKY
50
        val blocked = PhoneRunner.blockedBy(this)
51
        if (blocked != null) {
52
            PhoneRunner.set(PhoneRunner.State.Waiting(blocked))
53
            stopSelf()
54
            return START_NOT_STICKY
55
        }
56
        registerReceiver(power, IntentFilter(Intent.ACTION_POWER_DISCONNECTED))
57
        watchNetwork()
58
        val home = PhoneRunner.home(this).path
59
        PhoneRunner.set(PhoneRunner.State.Running)
60
        notify("Waiting for WASM jobs, on charge and Wi-Fi")
61
        serving = Thread {
62
            try {
63
                runnerServe(home)
64
                if (PhoneRunner.state.value !is PhoneRunner.State.Waiting) PhoneRunner.set(PhoneRunner.State.Stopped)
65
            } catch (e: Exception) {
66
                PhoneRunner.set(PhoneRunner.State.Failed(e.message ?: "the runner stopped"))
67
            } finally {
68
                serving = null
69
                stopSelf()
70
            }
71
        }.apply {
72
            name = "oxygenflow-runner"
73
            start()
74
        }
75
        return START_NOT_STICKY
76
    }
77
78
    /** Off Wi-Fi (or onto a metered network): drain. */
79
    private fun watchNetwork() {
80
        val net = getSystemService(ConnectivityManager::class.java) ?: return
81
        val callback = object : ConnectivityManager.NetworkCallback() {
82
            override fun onLost(network: Network) {
83
                if (PhoneRunner.blockedBy(this@RunnerService) != null) {
84
                    PhoneRunner.drain("off Wi-Fi")
85
                    notify("Draining: off Wi-Fi")
86
                }
87
            }
88
89
            override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) {
90
                if (!caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) {
91
                    PhoneRunner.drain("on a metered network")
92
                    notify("Draining: on a metered network")
93
                }
94
            }
95
        }
96
        net.registerDefaultNetworkCallback(callback)
97
        connectivity = callback
98
    }
99
100
    override fun onDestroy() {
101
        runCatching { unregisterReceiver(power) }
102
        connectivity?.let { cb -> runCatching { getSystemService(ConnectivityManager::class.java)?.unregisterNetworkCallback(cb) } }
103
        // Stopped from outside (the app, the system): drain rather than leave a job behind.
104
        if (serving != null) runnerStop()
105
        super.onDestroy()
106
    }
107
108
    private fun notify(text: String) {
109
        getSystemService(NotificationManager::class.java)?.notify(NOTIFICATION, notification(text))
110
    }
111
112
    private fun notification(text: String): Notification {
113
        val manager = getSystemService(NotificationManager::class.java)
114
        manager?.createNotificationChannel(
115
            NotificationChannel(CHANNEL, "OxygenFlow runner", NotificationManager.IMPORTANCE_LOW).apply {
116
                description = "While this phone runs OxygenFlow jobs"
117
            },
118
        )
119
        val open = PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java), PendingIntent.FLAG_IMMUTABLE)
120
        return Notification.Builder(this, CHANNEL)
121
            .setSmallIcon(R.drawable.ic_launcher_monochrome)
122
            .setContentTitle("OxygenFlow runner")
123
            .setContentText(text)
124
            .setContentIntent(open)
125
            .setOngoing(true)
126
            .build()
127
    }
128
129
    private companion object {
130
        const val CHANNEL = "runner"
131
        const val NOTIFICATION = 8
132
    }
133
}