android: converted Main from java to kotlin

This commit is contained in:
gd 2025-02-05 11:20:39 +02:00
parent 034bcf4f44
commit 0bf77f4eb3
2 changed files with 329 additions and 338 deletions
android/app/src/main/java/org/musicpd

@ -1,338 +0,0 @@
// 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<IMainCallback> mCallbacks = new RemoteCallbackList<IMainCallback>();
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);
}
}

@ -0,0 +1,329 @@
// 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.PowerManager.WakeLock
import android.os.RemoteCallbackList
import android.os.RemoteException
import android.util.Log
import androidx.annotation.OptIn
import androidx.core.app.ServiceCompat
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaSession
import dagger.hilt.android.AndroidEntryPoint
import org.musicpd.Bridge.LogListener
import org.musicpd.data.LoggingRepository
import java.lang.reflect.Constructor
import javax.inject.Inject
@AndroidEntryPoint
class Main : Service(), Runnable {
companion object {
private const val TAG = "Main"
private const val WAKELOCK_TAG = "mpd:wakelockmain"
private const val MAIN_STATUS_ERROR = -1
private const val MAIN_STATUS_STOPPED = 0
private const val MAIN_STATUS_STARTED = 1
private const val MSG_SEND_STATUS = 0
const val SHUTDOWN_ACTION: String = "org.musicpd.action.ShutdownMPD"
/*
* start Main service without any callback
*/
@JvmStatic
fun startService(context: Context, wakelock: Boolean) {
val intent = Intent(context, Main::class.java)
.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)
}
}
private var mThread: Thread? = null
private var mStatus = MAIN_STATUS_STOPPED
private var mAbort = false
private var mError: String? = null
private val mCallbacks = RemoteCallbackList<IMainCallback>()
private val mBinder: IBinder = MainStub(this)
private var mPauseOnHeadphonesDisconnect = false
private var mWakelock: WakeLock? = null
private var mMediaSession: MediaSession? = null
@JvmField
@Inject
var logging: LoggingRepository? = null
internal class MainStub(private val mService: Main) : IMain.Stub() {
override fun start() {
mService.start()
}
override fun stop() {
mService.stop()
}
override fun setPauseOnHeadphonesDisconnect(enabled: Boolean) {
mService.setPauseOnHeadphonesDisconnect(enabled)
}
override fun setWakelockEnabled(enabled: Boolean) {
mService.setWakelockEnabled(enabled)
}
override fun isRunning(): Boolean {
return mService.isRunning
}
override fun registerCallback(cb: IMainCallback) {
mService.registerCallback(cb)
}
override fun unregisterCallback(cb: IMainCallback) {
mService.unregisterCallback(cb)
}
}
@Synchronized
private fun sendMessage(
@Suppress("SameParameterValue") what: Int,
arg1: Int,
arg2: Int,
obj: Any?
) {
var i = mCallbacks.beginBroadcast()
while (i > 0) {
i--
val cb = mCallbacks.getBroadcastItem(i)
try {
when (what) {
MSG_SEND_STATUS -> when (arg1) {
MAIN_STATUS_ERROR -> cb.onError(obj as String?)
MAIN_STATUS_STOPPED -> cb.onStopped()
MAIN_STATUS_STARTED -> cb.onStarted()
}
}
} catch (ignored: RemoteException) {
}
}
mCallbacks.finishBroadcast()
}
private val mLogListener = LogListener { priority, msg ->
logging?.addLogItem(priority, msg)
}
override fun onBind(intent: Intent): IBinder {
return mBinder
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent?.action == SHUTDOWN_ACTION) {
stop()
} else {
start()
if (intent?.getBooleanExtra(
"wakelock",
false
) == true
) setWakelockEnabled(true)
}
return START_REDELIVER_INTENT
}
override fun run() {
if (!Loader.loaded) {
val error = """
Failed to load the native MPD library.
Report this problem to us, and include the following information:
SUPPORTED_ABIS=${java.lang.String.join(", ", *Build.SUPPORTED_ABIS)}
PRODUCT=${Build.PRODUCT}
FINGERPRINT=${Build.FINGERPRINT}
error=${Loader.error}
""".trimIndent()
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)
}
@Synchronized
private fun setStatus(status: Int, error: String?) {
mStatus = status
mError = error
sendMessage(MSG_SEND_STATUS, mStatus, 0, mError)
}
private fun createNotificationBuilderWithChannel(): Notification.Builder? {
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as? NotificationManager
?: return null
val id = "org.musicpd"
val name = "MPD service"
val importance = 3 /* NotificationManager.IMPORTANCE_DEFAULT */
try {
val ncClass = Class.forName("android.app.NotificationChannel")
val ncCtor = ncClass.getConstructor(
String::class.java,
CharSequence::class.java,
Int::class.javaPrimitiveType
)
val nc = ncCtor.newInstance(id, name, importance)
val nmCreateNotificationChannelMethod =
NotificationManager::class.java.getMethod("createNotificationChannel", ncClass)
nmCreateNotificationChannelMethod.invoke(notificationManager, nc)
val nbCtor: Constructor<*> = Notification.Builder::class.java.getConstructor(
Context::class.java, String::class.java
)
return nbCtor.newInstance(this, id) as Notification.Builder
} catch (e: Exception) {
Log.e(TAG, "error creating the NotificationChannel", e)
return null
}
}
@OptIn(markerClass = [UnstableApi::class])
private fun start() {
if (mThread != null) return
val filter = IntentFilter()
filter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
registerReceiver(object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (!mPauseOnHeadphonesDisconnect) return
if (intent.action === AudioManager.ACTION_AUDIO_BECOMING_NOISY) pause()
}
}, filter)
val mainIntent = Intent(this, MainActivity::class.java)
mainIntent.setAction("android.intent.action.MAIN")
mainIntent.addCategory("android.intent.category.LAUNCHER")
val contentIntent = PendingIntent.getActivity(
this, 0,
mainIntent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val nBuilder: Notification.Builder?
if (Build.VERSION.SDK_INT >= 26 /* Build.VERSION_CODES.O */) {
nBuilder = createNotificationBuilderWithChannel()
if (nBuilder == null) return
} else nBuilder = Notification.Builder(this)
val 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 = Thread(this).apply { start() }
val player = MPDPlayer(Looper.getMainLooper())
mMediaSession = MediaSession.Builder(this, player).build()
startForeground(R.string.notification_title_mpd_running, notification)
startService(Intent(this, Main::class.java))
}
private fun stop() {
mMediaSession?.let {
it.release()
mMediaSession = null
}
mThread?.let { thread ->
if (thread.isAlive) {
synchronized(this) {
if (mStatus == MAIN_STATUS_STARTED) Bridge.shutdown()
else mAbort = true
}
}
try {
thread.join()
mThread = null
mAbort = false
} catch (ie: InterruptedException) {
Log.e(TAG, "failed to join", ie)
}
}
setWakelockEnabled(false)
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
stopSelf()
}
private fun pause() {
if (mThread?.isAlive == true) {
synchronized(this) {
if (mStatus == MAIN_STATUS_STARTED) Bridge.pause()
}
}
}
private fun setPauseOnHeadphonesDisconnect(enabled: Boolean) {
mPauseOnHeadphonesDisconnect = enabled
}
private fun setWakelockEnabled(enabled: Boolean) {
if (enabled) {
val wakeLock =
mWakelock ?: run {
val pm = getSystemService(POWER_SERVICE) as PowerManager
pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG).also {
mWakelock = it
}
}
wakeLock.acquire(10 * 60 * 1000L /*10 minutes*/)
Log.d(TAG, "Wakelock acquired")
} else {
mWakelock?.let {
it.release()
mWakelock = null
}
Log.d(TAG, "Wakelock released")
}
}
private val isRunning: Boolean
get() = mThread?.isAlive == true
private fun registerCallback(cb: IMainCallback?) {
if (cb != null) {
mCallbacks.register(cb)
sendMessage(MSG_SEND_STATUS, mStatus, 0, mError)
}
}
private fun unregisterCallback(cb: IMainCallback?) {
if (cb != null) {
mCallbacks.unregister(cb)
}
}
}