package tf.monochrome.music; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Intent; import android.content.pm.ServiceInfo; import android.os.Build; import android.os.IBinder; import android.os.PowerManager; import androidx.core.app.NotificationCompat; /** * Foreground service that keeps the app process alive while audio is playing * in the background. Without this, Android will throttle the WebView and * suspend Web Audio API processing, causing audible skips and dropouts. */ public class AudioPlaybackService extends Service { private static final String CHANNEL_ID = "audio_playback"; private static final int NOTIFICATION_ID = 1; private PowerManager.WakeLock wakeLock; @Override public void onCreate() { super.onCreate(); createNotificationChannel(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null && "STOP".equals(intent.getAction())) { stopSelf(); return START_NOT_STICKY; } Notification notification = buildNotification(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK); } else { startForeground(NOTIFICATION_ID, notification); } acquireWakeLock(); // If the system kills this service, don't restart it automatically — // MainActivity will re-start it when audio resumes. return START_NOT_STICKY; } @Override public void onDestroy() { releaseWakeLock(); super.onDestroy(); } @Override public IBinder onBind(Intent intent) { return null; } private void createNotificationChannel() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { return; } NotificationChannel channel = new NotificationChannel( CHANNEL_ID, "Audio Playback", NotificationManager.IMPORTANCE_LOW ); channel.setDescription("Keeps audio playing in the background"); channel.setSound(null, null); channel.setShowBadge(false); NotificationManager manager = getSystemService(NotificationManager.class); if (manager != null) { manager.createNotificationChannel(channel); } } private Notification buildNotification() { Intent launchIntent = new Intent(this, MainActivity.class); launchIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); PendingIntent pendingIntent = PendingIntent.getActivity( this, 0, launchIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE ); return new NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("Monochrome") .setContentText("Playing audio") .setSmallIcon(android.R.drawable.ic_media_play) .setContentIntent(pendingIntent) .setOngoing(true) .setSilent(true) .build(); } private void acquireWakeLock() { if (wakeLock != null && !wakeLock.isHeld()) { wakeLock = null; } if (wakeLock == null) { PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); if (pm != null) { wakeLock = pm.newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, "monochrome:audio_playback" ); // 4-hour timeout as a safety net to prevent battery drain // if the service is accidentally left running wakeLock.acquire(4 * 60 * 60 * 1000L); } } } private void releaseWakeLock() { if (wakeLock != null && wakeLock.isHeld()) { wakeLock.release(); wakeLock = null; } } }