
Loader converted from java to kotlin. Instead of loading libmpd when the service thread is started, the service will not start the the thread if libmpd failed to load. The loader is also accessed by the view data to let the ui adjust if failed to load, by showing the failure reason and disabling the Start MPD button.
327 lines
10 KiB
Kotlin
327 lines
10 KiB
Kotlin
// 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 lateinit var mpdApp: MPDApplication
|
|
private lateinit var mpdLoader: Loader
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
override fun onCreate() {
|
|
super.onCreate()
|
|
mpdLoader = Loader
|
|
}
|
|
|
|
@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() {
|
|
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()
|
|
|
|
if (mpdLoader.isLoaded) {
|
|
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)
|
|
}
|
|
}
|
|
}
|