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