android: Loader - load early (before service thread) both in activity and service.
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.
This commit is contained in:
parent
ae1c5e3424
commit
f1e43cb498
@ -1,23 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Copyright The Music Player Daemon Project
|
||||
|
||||
package org.musicpd;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
public class Loader {
|
||||
private static final String TAG = "MPD";
|
||||
|
||||
public static boolean loaded = false;
|
||||
public static String error;
|
||||
|
||||
static {
|
||||
try {
|
||||
System.loadLibrary("mpd");
|
||||
loaded = true;
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
error = e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
45
android/app/src/main/java/org/musicpd/Loader.kt
Normal file
45
android/app/src/main/java/org/musicpd/Loader.kt
Normal file
@ -0,0 +1,45 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Copyright The Music Player Daemon Project
|
||||
package org.musicpd
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
|
||||
object Loader {
|
||||
private const val TAG = "Loader"
|
||||
|
||||
private var loaded: Boolean = false
|
||||
private var error: String? = null
|
||||
private val failReason: String get() = error ?: ""
|
||||
|
||||
val isLoaded: Boolean get() = loaded
|
||||
|
||||
init {
|
||||
load()
|
||||
}
|
||||
|
||||
private fun load() {
|
||||
if (loaded) return
|
||||
loaded = try {
|
||||
error = null
|
||||
System.loadLibrary("mpd")
|
||||
Log.i(TAG, "mpd lib loaded")
|
||||
true
|
||||
} catch (e: Throwable) {
|
||||
error = e.message ?: e.javaClass.simpleName
|
||||
Log.e(TAG, "failed to load mpd lib: $failReason")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun loadFailureMessage(context: Context): String {
|
||||
return context.getString(
|
||||
R.string.mpd_load_failure_message,
|
||||
Build.SUPPORTED_ABIS.joinToString(),
|
||||
Build.PRODUCT,
|
||||
Build.FINGERPRINT,
|
||||
failReason
|
||||
)
|
||||
}
|
||||
}
|
@ -59,6 +59,9 @@ class Main : Service(), Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@ -104,6 +107,11 @@ class Main : Service(), Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
mpdLoader = Loader
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun sendMessage(
|
||||
@Suppress("SameParameterValue") what: Int,
|
||||
@ -152,19 +160,6 @@ class Main : Service(), Runnable {
|
||||
}
|
||||
|
||||
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)
|
||||
@ -245,7 +240,9 @@ class Main : Service(), Runnable {
|
||||
.setContentIntent(contentIntent)
|
||||
.build()
|
||||
|
||||
mThread = Thread(this).apply { start() }
|
||||
if (mpdLoader.isLoaded) {
|
||||
mThread = Thread(this).apply { start() }
|
||||
}
|
||||
|
||||
val player = MPDPlayer(Looper.getMainLooper())
|
||||
mMediaSession = MediaSession.Builder(this, player).build()
|
||||
|
@ -6,6 +6,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import org.musicpd.Loader
|
||||
import org.musicpd.MainServiceClient
|
||||
import org.musicpd.Preferences
|
||||
import org.musicpd.data.LoggingRepository
|
||||
@ -17,6 +18,7 @@ class SettingsViewModel @Inject constructor(
|
||||
private var loggingRepository: LoggingRepository
|
||||
) : ViewModel() {
|
||||
private var mClient: MainServiceClient? = null
|
||||
val mpdLoader = Loader
|
||||
|
||||
data class StatusUiState(
|
||||
val statusMessage: String = "",
|
||||
|
@ -1,13 +1,18 @@
|
||||
package org.musicpd.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.TypedValue
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Circle
|
||||
import androidx.compose.material3.Button
|
||||
@ -20,6 +25,7 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@ -53,13 +59,24 @@ fun StatusScreen(settingsViewModel: SettingsViewModel) {
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
NetworkAddress()
|
||||
ServerStatus(settingsViewModel)
|
||||
ServerStatus(settingsViewModel, storagePermissionState)
|
||||
AudioMediaPermission(storagePermissionState)
|
||||
MPDLoaderStatus(settingsViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun getThemeColorAttribute(context: Context, @AttrRes attr: Int): Int {
|
||||
val value = TypedValue()
|
||||
if (context.theme.resolveAttribute(attr, value, true)) {
|
||||
return value.data
|
||||
}
|
||||
return android.graphics.Color.BLACK
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun ServerStatus(settingsViewModel: SettingsViewModel) {
|
||||
fun ServerStatus(settingsViewModel: SettingsViewModel, storagePermissionState: PermissionState) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val statusUiState by settingsViewModel.statusUIState.collectAsState()
|
||||
@ -72,21 +89,35 @@ fun ServerStatus(settingsViewModel: SettingsViewModel) {
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
Row {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Circle,
|
||||
contentDescription = "",
|
||||
tint = if (statusUiState.running) Color(0xFFB8F397) else Color(0xFFFFDAD6)
|
||||
tint = Color(
|
||||
getThemeColorAttribute(
|
||||
context,
|
||||
if (statusUiState.running) R.attr.appColorPositive else R.attr.appColorNegative
|
||||
)
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
.alpha(0.6f)
|
||||
)
|
||||
Text(text = if (statusUiState.running) "Running" else "Stopped")
|
||||
Text(text = stringResource(id = if (statusUiState.running) R.string.running else R.string.stopped))
|
||||
}
|
||||
Button(onClick = {
|
||||
if (statusUiState.running)
|
||||
settingsViewModel.stopMPD()
|
||||
else
|
||||
settingsViewModel.startMPD(context)
|
||||
}) {
|
||||
Text(text = if (statusUiState.running) "Stop MPD" else "Start MPD")
|
||||
Button(
|
||||
onClick = {
|
||||
if (statusUiState.running)
|
||||
settingsViewModel.stopMPD()
|
||||
else
|
||||
settingsViewModel.startMPD(context)
|
||||
},
|
||||
enabled = settingsViewModel.mpdLoader.isLoaded
|
||||
&& storagePermissionState.status.isGranted
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = if (statusUiState.running) R.string.stopMPD else R.string.startMPD)
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
@ -139,4 +170,19 @@ fun AudioMediaPermission(storagePermissionState: PermissionState) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MPDLoaderStatus(settingsViewModel: SettingsViewModel) {
|
||||
val loader = settingsViewModel.mpdLoader
|
||||
if (!loader.isLoaded) {
|
||||
val context = LocalContext.current
|
||||
SelectionContainer {
|
||||
Text(
|
||||
loader.loadFailureMessage(context),
|
||||
Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<resources>
|
||||
<string name="app_name">MPD</string>
|
||||
<string name="notification_title_mpd_running">Music Player Daemon is running</string>
|
||||
<string name="notification_text_mpd_running">Touch for MPD options.</string>
|
||||
<string name="toggle_button_run_on">MPD is running</string>
|
||||
<string name="toggle_button_run_off">MPD is not running</string>
|
||||
<string name="checkbox_run_on_boot">Run MPD automatically on boot</string>
|
||||
<string name="checkbox_wakelock">Prevent suspend when MPD is running (Wakelock)</string>
|
||||
<string name="checkbox_pause_on_headphones_disconnect">Pause MPD when headphones disconnect</string>
|
||||
<string name="external_files_permission_request">MPD requires access to external files to play local music. Please grant the permission.</string>
|
||||
<string name="title_open_app_info">Open app info</string>
|
||||
<string name="app_name">MPD</string>
|
||||
<string name="notification_title_mpd_running">Music Player Daemon is running</string>
|
||||
<string name="notification_text_mpd_running">Touch for MPD options.</string>
|
||||
<string name="toggle_button_run_on">MPD is running</string>
|
||||
<string name="toggle_button_run_off">MPD is not running</string>
|
||||
<string name="checkbox_run_on_boot">Run MPD automatically on boot</string>
|
||||
<string name="checkbox_wakelock">Prevent suspend when MPD is running (Wakelock)</string>
|
||||
<string name="checkbox_pause_on_headphones_disconnect">Pause MPD when headphones disconnect</string>
|
||||
<string name="external_files_permission_request">MPD requires access to external files to play local music. Please grant the permission.</string>
|
||||
<string name="title_open_app_info">Open app info</string>
|
||||
<string name="mpd_load_failure_message">"Failed to load the native MPD library.
|
||||
Report this problem to us, and include the following information:
|
||||
SUPPORTED_ABIS=%1$s
|
||||
PRODUCT=%2$s
|
||||
FINGERPRINT=%3$s
|
||||
error=%4$s"
|
||||
</string>
|
||||
<string name="stopped">Stopped</string>
|
||||
<string name="running">Running</string>
|
||||
<string name="stopMPD">Stop MPD</string>
|
||||
<string name="startMPD">Start MPD</string>
|
||||
</resources>
|
||||
|
@ -1,4 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.MPD" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
<color name="red_500">#F44336</color>
|
||||
<color name="red_900">#B71C1C</color>
|
||||
<color name="green_300">#81C784</color>
|
||||
<color name="green_700">#388E3C</color>
|
||||
|
||||
<color name="colorErrorOnLight">@color/red_900</color>
|
||||
<color name="colorErrorOnDark">@color/red_500</color>
|
||||
|
||||
<color name="colorSuccessOnLight">@color/green_700</color>
|
||||
<color name="colorSuccessOnDark">@color/green_300</color>
|
||||
|
||||
<attr name="appColorNegative" format="color|reference" />
|
||||
<attr name="appColorPositive" format="color|reference" />
|
||||
|
||||
<style name="Theme.MPD" parent="android:Theme.Material.Light.NoActionBar">
|
||||
<item name="appColorNegative">@color/colorErrorOnLight</item>
|
||||
<item name="appColorPositive">@color/colorSuccessOnLight</item>
|
||||
</style>
|
||||
</resources>
|
Loading…
x
Reference in New Issue
Block a user