diff --git a/android/app/src/main/java/org/musicpd/Loader.java b/android/app/src/main/java/org/musicpd/Loader.java deleted file mode 100644 index 21501e67b..000000000 --- a/android/app/src/main/java/org/musicpd/Loader.java +++ /dev/null @@ -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(); - } - } -} diff --git a/android/app/src/main/java/org/musicpd/Loader.kt b/android/app/src/main/java/org/musicpd/Loader.kt new file mode 100644 index 000000000..2d89d435c --- /dev/null +++ b/android/app/src/main/java/org/musicpd/Loader.kt @@ -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 + ) + } +} diff --git a/android/app/src/main/java/org/musicpd/Main.kt b/android/app/src/main/java/org/musicpd/Main.kt index e2fcac4f5..b7037feb9 100644 --- a/android/app/src/main/java/org/musicpd/Main.kt +++ b/android/app/src/main/java/org/musicpd/Main.kt @@ -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() diff --git a/android/app/src/main/java/org/musicpd/ui/SettingsViewModel.kt b/android/app/src/main/java/org/musicpd/ui/SettingsViewModel.kt index 62929ee4f..c6f06ef3d 100644 --- a/android/app/src/main/java/org/musicpd/ui/SettingsViewModel.kt +++ b/android/app/src/main/java/org/musicpd/ui/SettingsViewModel.kt @@ -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 = "", diff --git a/android/app/src/main/java/org/musicpd/ui/StatusScreen.kt b/android/app/src/main/java/org/musicpd/ui/StatusScreen.kt index b913c57b5..cbd56a4df 100644 --- a/android/app/src/main/java/org/musicpd/ui/StatusScreen.kt +++ b/android/app/src/main/java/org/musicpd/ui/StatusScreen.kt @@ -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 + ) + } + } } \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 32877eb03..6c47fa042 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -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> diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml index c9502c8f8..d53aa8c45 100644 --- a/android/app/src/main/res/values/themes.xml +++ b/android/app/src/main/res/values/themes.xml @@ -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> \ No newline at end of file