// SPDX-License-Identifier: GPL-2.0-or-later // Copyright The Music Player Daemon Project package org.musicpd; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; import android.os.Build; import android.os.IBinder; import android.os.Looper; import android.os.PowerManager; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.util.Log; import androidx.annotation.OptIn; import androidx.media3.common.util.UnstableApi; import androidx.media3.session.MediaSession; import org.jetbrains.annotations.NotNull; import org.musicpd.data.LoggingRepository; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.Objects; import javax.inject.Inject; import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class Main extends Service implements Runnable { private static final String TAG = "Main"; private static final String WAKELOCK_TAG = "mpd:wakelockmain"; private static final int MAIN_STATUS_ERROR = -1; private static final int MAIN_STATUS_STOPPED = 0; private static final int MAIN_STATUS_STARTED = 1; private static final int MSG_SEND_STATUS = 0; private Thread mThread = null; private int mStatus = MAIN_STATUS_STOPPED; private boolean mAbort = false; private String mError = null; private final RemoteCallbackList mCallbacks = new RemoteCallbackList(); private final IBinder mBinder = new MainStub(this); private boolean mPauseOnHeadphonesDisconnect = false; private PowerManager.WakeLock mWakelock = null; private MediaSession mMediaSession = null; @Inject LoggingRepository logging; @NotNull public static final String SHUTDOWN_ACTION = "org.musicpd.action.ShutdownMPD"; static class MainStub extends IMain.Stub { private Main mService; MainStub(Main service) { mService = service; } public void start() { mService.start(); } public void stop() { mService.stop(); } public void setPauseOnHeadphonesDisconnect(boolean enabled) { mService.setPauseOnHeadphonesDisconnect(enabled); } public void setWakelockEnabled(boolean enabled) { mService.setWakelockEnabled(enabled); } public boolean isRunning() { return mService.isRunning(); } public void registerCallback(IMainCallback cb) { mService.registerCallback(cb); } public void unregisterCallback(IMainCallback cb) { mService.unregisterCallback(cb); } } private synchronized void sendMessage(int what, int arg1, int arg2, Object obj) { int i = mCallbacks.beginBroadcast(); while (i > 0) { i--; final IMainCallback cb = mCallbacks.getBroadcastItem(i); try { switch (what) { case MSG_SEND_STATUS: switch (arg1) { case MAIN_STATUS_ERROR: cb.onError((String)obj); break; case MAIN_STATUS_STOPPED: cb.onStopped(); break; case MAIN_STATUS_STARTED: cb.onStarted(); break; } break; } } catch (RemoteException e) { } } mCallbacks.finishBroadcast(); } private Bridge.LogListener mLogListener = new Bridge.LogListener() { @Override public void onLog(int priority, String msg) { logging.addLogItem(priority, msg); } }; @Override public IBinder onBind(Intent intent) { return mBinder; } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null && Objects.equals(intent.getAction(), SHUTDOWN_ACTION)) { stop(); } else { start(); if (intent != null && intent.getBooleanExtra("wakelock", false)) setWakelockEnabled(true); } return START_STICKY; } @Override public void run() { if (!Loader.loaded) { final String error = "Failed to load the native MPD libary.\n" + "Report this problem to us, and include the following information:\n" + "SUPPORTED_ABIS=" + String.join(", ", Build.SUPPORTED_ABIS) + "\n" + "PRODUCT=" + Build.PRODUCT + "\n" + "FINGERPRINT=" + Build.FINGERPRINT + "\n" + "error=" + Loader.error; setStatus(MAIN_STATUS_ERROR, error); stopSelf(); return; } synchronized (this) { if (mAbort) return; setStatus(MAIN_STATUS_STARTED, null); } Bridge.run(this, mLogListener); setStatus(MAIN_STATUS_STOPPED, null); } private synchronized void setStatus(int status, String error) { mStatus = status; mError = error; sendMessage(MSG_SEND_STATUS, mStatus, 0, mError); } private Notification.Builder createNotificationBuilderWithChannel() { final NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE); if (notificationManager == null) return null; final String id = "org.musicpd"; final String name = "MPD service"; final int importance = 3; /* NotificationManager.IMPORTANCE_DEFAULT */ try { Class ncClass = Class.forName("android.app.NotificationChannel"); Constructor ncCtor = ncClass.getConstructor(String.class, CharSequence.class, int.class); Object nc = ncCtor.newInstance(id, name, importance); Method nmCreateNotificationChannelMethod = NotificationManager.class.getMethod("createNotificationChannel", ncClass); nmCreateNotificationChannelMethod.invoke(notificationManager, nc); Constructor nbCtor = Notification.Builder.class.getConstructor(Context.class, String.class); return (Notification.Builder) nbCtor.newInstance(this, id); } catch (Exception e) { Log.e(TAG, "error creating the NotificationChannel", e); return null; } } @OptIn(markerClass = UnstableApi.class) private void start() { if (mThread != null) return; IntentFilter filter = new IntentFilter(); filter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY); registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (!mPauseOnHeadphonesDisconnect) return; if (intent.getAction() == AudioManager.ACTION_AUDIO_BECOMING_NOISY) pause(); } }, filter); final Intent mainIntent = new Intent(this, MainActivity.class); mainIntent.setAction("android.intent.action.MAIN"); mainIntent.addCategory("android.intent.category.LAUNCHER"); final PendingIntent contentIntent = PendingIntent.getActivity(this, 0, mainIntent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); Notification.Builder nBuilder; if (Build.VERSION.SDK_INT >= 26 /* Build.VERSION_CODES.O */) { nBuilder = createNotificationBuilderWithChannel(); if (nBuilder == null) return; } else nBuilder = new Notification.Builder(this); Notification notification = nBuilder.setContentTitle(getText(R.string.notification_title_mpd_running)) .setContentText(getText(R.string.notification_text_mpd_running)) .setSmallIcon(R.drawable.notification_icon) .setContentIntent(contentIntent) .build(); mThread = new Thread(this); mThread.start(); MPDPlayer player = new MPDPlayer(Looper.getMainLooper()); mMediaSession = new MediaSession.Builder(this, player).build(); startForeground(R.string.notification_title_mpd_running, notification); startService(new Intent(this, Main.class)); } private void stop() { mMediaSession.release(); mMediaSession = null; if (mThread != null) { if (mThread.isAlive()) { synchronized (this) { if (mStatus == MAIN_STATUS_STARTED) Bridge.shutdown(); else mAbort = true; } } try { mThread.join(); mThread = null; mAbort = false; } catch (InterruptedException ie) {} } setWakelockEnabled(false); stopForeground(true); stopSelf(); } private void pause() { if (mThread != null) { if (mThread.isAlive()) { synchronized (this) { if (mStatus == MAIN_STATUS_STARTED) Bridge.pause(); } } } } private void setPauseOnHeadphonesDisconnect(boolean enabled) { mPauseOnHeadphonesDisconnect = enabled; } private void setWakelockEnabled(boolean enabled) { if (enabled && mWakelock == null) { PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); mWakelock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG); mWakelock.acquire(); Log.d(TAG, "Wakelock acquired"); } else if (!enabled && mWakelock != null) { mWakelock.release(); mWakelock = null; Log.d(TAG, "Wakelock released"); } } private boolean isRunning() { return mThread != null && mThread.isAlive(); } private void registerCallback(IMainCallback cb) { if (cb != null) { mCallbacks.register(cb); sendMessage(MSG_SEND_STATUS, mStatus, 0, mError); } } private void unregisterCallback(IMainCallback cb) { if (cb != null) { mCallbacks.unregister(cb); } } /* * start Main service without any callback */ public static void startService(Context context, boolean wakelock) { Intent intent = new Intent(context, Main.class) .putExtra("wakelock", wakelock); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) /* in Android 8+, we need to use this method or else we'll get "IllegalStateException: app is in background" */ context.startForegroundService(intent); else context.startService(intent); } public static void stopService(Context context) { Intent intent = new Intent(context, Main.class); context.stopService(intent); } }