kv-music/android/app/src/main/java/tf/monochrome/music/AudioPlaybackService.java
tryptz 744984d494 Update android/app/src/main/java/tf/monochrome/music/AudioPlaybackService.java
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-04-05 20:07:39 +03:00

129 lines
4.1 KiB
Java

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