From f1e43cb49866ac6a1817ad4c5b09973cbe8e2ce6 Mon Sep 17 00:00:00 2001
From: gd <gd@iotide.com>
Date: Thu, 6 Feb 2025 14:23:26 +0200
Subject: [PATCH] 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.
---
 .../app/src/main/java/org/musicpd/Loader.java | 23 ------
 .../app/src/main/java/org/musicpd/Loader.kt   | 45 ++++++++++++
 android/app/src/main/java/org/musicpd/Main.kt | 25 +++----
 .../java/org/musicpd/ui/SettingsViewModel.kt  |  2 +
 .../main/java/org/musicpd/ui/StatusScreen.kt  | 70 +++++++++++++++----
 android/app/src/main/res/values/strings.xml   | 31 +++++---
 android/app/src/main/res/values/themes.xml    | 19 ++++-
 7 files changed, 155 insertions(+), 60 deletions(-)
 delete mode 100644 android/app/src/main/java/org/musicpd/Loader.java
 create mode 100644 android/app/src/main/java/org/musicpd/Loader.kt

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