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
}
}
|