aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/cast/CastService.kt
blob: 3ea8f923c83ba58f4c6a7cb455a11f9a69186b23 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package org.meshbay.client.cast

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.net.wifi.WifiManager
import android.os.Build
import android.os.IBinder
import android.os.PowerManager

/**
 * Keeps a cast, or music playing on the phone itself, alive with the screen
 * off: a media-playback foreground service with a partial wake lock and a
 * Wi-Fi lock, for as long as either runs.
 *
 * Not sufficient alone — measured (spike S-2a): Chromium freezes a hidden page
 * 60 s after the screen goes off whatever the process importance, and the
 * pipeline (WebRTC → decrypt → relay) lives in the page. The shell also keeps
 * the WebView reported visible while casting (ShellWebView); the two together
 * held a full-rate stream through screen-off and forced Doze.
 */
class CastService : Service() {
    private var wake: PowerManager.WakeLock? = null
    private var wifi: WifiManager.WifiLock? = null

    override fun onBind(intent: Intent?): IBinder? = null

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val nm = getSystemService(NotificationManager::class.java)
        nm.createNotificationChannel(NotificationChannel(CHANNEL, "Playback", NotificationManager.IMPORTANCE_LOW))
        val open = PendingIntent.getActivity(this, 0,
            packageManager.getLaunchIntentForPackage(packageName), PendingIntent.FLAG_IMMUTABLE)
        val n = Notification.Builder(this, CHANNEL)
            .setContentTitle(intent?.getStringExtra(EXTRA_TITLE) ?: "MeshBay")
            .setContentText(intent?.getStringExtra(EXTRA_TEXT) ?: "Casting on this Wi-Fi")
            .setSmallIcon(android.R.drawable.ic_media_play)
            .setContentIntent(open)
            .setOngoing(true)
            .build()
        if (Build.VERSION.SDK_INT >= 29) startForeground(ID, n, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
        else startForeground(ID, n)
        if (wake == null) {
            wake = getSystemService(PowerManager::class.java)
                .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "meshbay:cast").apply { acquire(MAX_HOLD_MS) }
            @Suppress("DEPRECATION")
            val mode = if (Build.VERSION.SDK_INT >= 29) WifiManager.WIFI_MODE_FULL_LOW_LATENCY else WifiManager.WIFI_MODE_FULL_HIGH_PERF
            wifi = (applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager)
                .createWifiLock(mode, "meshbay:cast").apply { acquire() }
        }
        return START_NOT_STICKY
    }

    override fun onDestroy() {
        wake?.let { if (it.isHeld) it.release() }
        wifi?.let { if (it.isHeld) it.release() }
        wake = null; wifi = null
        super.onDestroy()
    }

    companion object {
        private const val CHANNEL = "cast"
        private const val ID = 7
        const val EXTRA_TITLE = "title"
        const val EXTRA_TEXT = "text"
        // A bound on the wake lock, not on the cast: a process that is killed
        // without stopping the service must not hold the CPU for ever.
        private const val MAX_HOLD_MS = 6L * 3600 * 1000
    }
}