From ddc048e2c3e48368281a111e9066f6fa4a6a45f6 Mon Sep 17 00:00:00 2001 From: Colin Edwards Date: Fri, 22 Dec 2023 22:52:33 -0600 Subject: [PATCH] android: Move settings to compose and material UI 3 This changes lets us use the latest UI design from Google, Material 3. Google only provides the material UI 3 themes for compose, compose only works with kotlin. --- android/app/build.gradle.kts | 7 + android/app/src/main/AndroidManifest.xml | 7 +- .../app/src/main/java/org/musicpd/Main.java | 4 +- .../main/java/org/musicpd/NetworkUtil.java | 3 + .../src/main/java/org/musicpd/Settings.java | 267 ------------------ .../java/org/musicpd/ui/NetworkAddress.kt | 39 +++ .../main/java/org/musicpd/ui/Preferences.kt | 52 ++++ .../java/org/musicpd/ui/SettingsActivity.kt | 212 ++++++++++++++ .../java/org/musicpd/ui/SettingsViewModel.kt | 84 ++++++ android/app/src/main/res/layout/log_item.xml | 5 - android/app/src/main/res/layout/settings.xml | 65 ----- android/app/src/main/res/values/strings.xml | 1 + android/app/src/main/res/values/themes.xml | 4 + 13 files changed, 410 insertions(+), 340 deletions(-) delete mode 100644 android/app/src/main/java/org/musicpd/Settings.java create mode 100644 android/app/src/main/java/org/musicpd/ui/NetworkAddress.kt create mode 100644 android/app/src/main/java/org/musicpd/ui/Preferences.kt create mode 100644 android/app/src/main/java/org/musicpd/ui/SettingsActivity.kt create mode 100644 android/app/src/main/java/org/musicpd/ui/SettingsViewModel.kt delete mode 100644 android/app/src/main/res/layout/log_item.xml delete mode 100644 android/app/src/main/res/layout/settings.xml create mode 100644 android/app/src/main/res/values/themes.xml diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 8cc9f7e20..639e477d7 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -56,6 +56,13 @@ dependencies { implementation("androidx.compose.material3:material3") implementation("androidx.activity:activity-compose:1.8.2") + implementation("androidx.compose.material:material-icons-extended") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2") + implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.2") + + implementation("com.github.alorma:compose-settings-ui-m3:1.0.3") + implementation("com.github.alorma:compose-settings-storage-preferences:1.0.3") + implementation("com.google.accompanist:accompanist-permissions:0.33.2-alpha") // Android Studio Preview support implementation("androidx.compose.ui:ui-tooling-preview") diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7dc120a0f..f5d6d6d8b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -24,16 +24,19 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:requestLegacyExternalStorage="true" - android:roundIcon="@mipmap/ic_launcher_round"> + android:roundIcon="@mipmap/ic_launcher_round" + android:theme="@style/Theme.MPD"> + + diff --git a/android/app/src/main/java/org/musicpd/Main.java b/android/app/src/main/java/org/musicpd/Main.java index e943a6089..7196e0c5f 100644 --- a/android/app/src/main/java/org/musicpd/Main.java +++ b/android/app/src/main/java/org/musicpd/Main.java @@ -25,6 +25,8 @@ import android.widget.RemoteViews; import androidx.core.app.ServiceCompat; +import org.musicpd.ui.SettingsActivity; + import java.lang.reflect.Constructor; import java.lang.reflect.Method; @@ -197,7 +199,7 @@ public class Main extends Service implements Runnable { } }, filter); - final Intent mainIntent = new Intent(this, Settings.class); + final Intent mainIntent = new Intent(this, SettingsActivity.class); mainIntent.setAction("android.intent.action.MAIN"); mainIntent.addCategory("android.intent.category.LAUNCHER"); final PendingIntent contentIntent = PendingIntent.getActivity(this, 0, diff --git a/android/app/src/main/java/org/musicpd/NetworkUtil.java b/android/app/src/main/java/org/musicpd/NetworkUtil.java index 2579f2d51..7f664ca39 100644 --- a/android/app/src/main/java/org/musicpd/NetworkUtil.java +++ b/android/app/src/main/java/org/musicpd/NetworkUtil.java @@ -5,12 +5,15 @@ import android.net.ConnectivityManager; import android.net.LinkAddress; import android.net.LinkProperties; +import androidx.annotation.Nullable; + import java.net.Inet4Address; import java.net.InetAddress; import java.util.List; public class NetworkUtil { + @Nullable public static String getDeviceIPV4Address(Context context) { ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); LinkProperties linkProperties = connectivityManager.getLinkProperties(connectivityManager.getActiveNetwork()); diff --git a/android/app/src/main/java/org/musicpd/Settings.java b/android/app/src/main/java/org/musicpd/Settings.java deleted file mode 100644 index 4d035f9e5..000000000 --- a/android/app/src/main/java/org/musicpd/Settings.java +++ /dev/null @@ -1,267 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// Copyright The Music Player Daemon Project - -package org.musicpd; - -import java.util.LinkedList; - -import android.Manifest; -import android.app.Activity; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.util.Log; -import android.widget.ArrayAdapter; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.CompoundButton.OnCheckedChangeListener; -import android.widget.ListView; -import android.widget.TextView; -import android.widget.ToggleButton; - -public class Settings extends Activity { - private static final String TAG = "Settings"; - private Main.Client mClient; - private TextView mTextStatus; - private ToggleButton mRunButton; - private boolean mFirstRun; - private LinkedList mLogListArray = new LinkedList(); - private ListView mLogListView; - private ArrayAdapter mLogListAdapter; - - private static final int MAX_LOGS = 500; - - private static final int MSG_ERROR = 0; - private static final int MSG_STOPPED = 1; - private static final int MSG_STARTED = 2; - private static final int MSG_LOG = 3; - - private Handler mHandler = new Handler(new Handler.Callback() { - @Override - public boolean handleMessage(Message msg) { - switch (msg.what) { - case MSG_ERROR: - Log.d(TAG, "onError"); - - mClient.release(); - connectClient(); - - mRunButton.setEnabled(false); - mRunButton.setChecked(false); - - mTextStatus.setText((String)msg.obj); - mFirstRun = true; - break; - case MSG_STOPPED: - Log.d(TAG, "onStopped"); - mRunButton.setEnabled(true); - if (!mFirstRun && Preferences.getBoolean(Settings.this, Preferences.KEY_RUN_ON_BOOT, false)) - mRunButton.setChecked(true); - else - mRunButton.setChecked(false); - mFirstRun = true; - mTextStatus.setText(""); - break; - case MSG_STARTED: - Log.d(TAG, "onStarted"); - mRunButton.setChecked(true); - mFirstRun = true; - mTextStatus.setText("MPD service started"); - break; - case MSG_LOG: - if (mLogListArray.size() > MAX_LOGS) - mLogListArray.remove(0); - String priority; - switch (msg.arg1) { - case Log.DEBUG: - priority = "D"; - break; - case Log.ERROR: - priority = "E"; - break; - case Log.INFO: - priority = "I"; - break; - case Log.VERBOSE: - priority = "V"; - break; - case Log.WARN: - priority = "W"; - break; - default: - priority = ""; - } - mLogListArray.add(priority + "/ " + (String)msg.obj); - mLogListAdapter.notifyDataSetChanged(); - - break; - } - return true; - } - }); - - private final OnCheckedChangeListener mOnRunChangeListener = new OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (mClient != null) { - if (isChecked) { - mClient.start(); - if (Preferences.getBoolean(Settings.this, - Preferences.KEY_WAKELOCK, false)) - mClient.setWakelockEnabled(true); - if (Preferences.getBoolean(Settings.this, - Preferences.KEY_PAUSE_ON_HEADPHONES_DISCONNECT, false)) - mClient.setPauseOnHeadphonesDisconnect(true); - } else { - mClient.stop(); - } - } - } - }; - - private final OnCheckedChangeListener mOnRunOnBootChangeListener = new OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - Preferences.putBoolean(Settings.this, Preferences.KEY_RUN_ON_BOOT, isChecked); - if (isChecked && mClient != null && !mRunButton.isChecked()) - mRunButton.setChecked(true); - } - }; - - private final OnCheckedChangeListener mOnWakelockChangeListener = new OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - Preferences.putBoolean(Settings.this, Preferences.KEY_WAKELOCK, isChecked); - if (mClient != null && mClient.isRunning()) - mClient.setWakelockEnabled(isChecked); - } - }; - - private final OnCheckedChangeListener mOnPauseOnHeadphonesDisconnectChangeListener = new OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - Preferences.putBoolean(Settings.this, Preferences.KEY_PAUSE_ON_HEADPHONES_DISCONNECT, isChecked); - if (mClient != null && mClient.isRunning()) - mClient.setPauseOnHeadphonesDisconnect(isChecked); - } - }; - - @Override - protected void onCreate(Bundle savedInstanceState) { - /* TODO: this sure is the wrong place to request - permissions - it will cause MPD to quit - immediately; we should request permissions when we - need them, but implementing that is complicated, so - for now, we do it here to give users a quick - solution for the problem */ - requestAllPermissions(); - - setContentView(R.layout.settings); - mRunButton = (ToggleButton) findViewById(R.id.run); - mRunButton.setOnCheckedChangeListener(mOnRunChangeListener); - - mTextStatus = (TextView) findViewById(R.id.status); - - mLogListAdapter = new ArrayAdapter(this, R.layout.log_item, mLogListArray); - - mLogListView = (ListView) findViewById(R.id.log_list); - mLogListView.setAdapter(mLogListAdapter); - mLogListView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL); - - CheckBox checkbox = (CheckBox) findViewById(R.id.run_on_boot); - checkbox.setOnCheckedChangeListener(mOnRunOnBootChangeListener); - if (Preferences.getBoolean(this, Preferences.KEY_RUN_ON_BOOT, false)) - checkbox.setChecked(true); - - checkbox = (CheckBox) findViewById(R.id.wakelock); - checkbox.setOnCheckedChangeListener(mOnWakelockChangeListener); - if (Preferences.getBoolean(this, Preferences.KEY_WAKELOCK, false)) - checkbox.setChecked(true); - - checkbox = (CheckBox) findViewById(R.id.pause_on_headphones_disconnect); - checkbox.setOnCheckedChangeListener(mOnPauseOnHeadphonesDisconnectChangeListener); - if (Preferences.getBoolean(this, Preferences.KEY_PAUSE_ON_HEADPHONES_DISCONNECT, false)) - checkbox.setChecked(true); - - TextView networkAddressTextView = (TextView) findViewById(R.id.networkAddress); - String deviceIPV4Address = NetworkUtil.getDeviceIPV4Address(this); - networkAddressTextView.setText(deviceIPV4Address); - - super.onCreate(savedInstanceState); - } - - private void checkRequestPermission(String permission) { - if (checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) - return; - - try { - this.requestPermissions(new String[]{permission}, 0); - } catch (Exception e) { - Log.e(TAG, "requestPermissions(" + permission + ") failed", - e); - } - } - - private void requestAllPermissions() { - if (android.os.Build.VERSION.SDK_INT < 23) - /* we don't need to request permissions on - this old Android version */ - return; - - /* starting with Android 6.0, we need to explicitly - request all permissions before using them; - mentioning them in the manifest is not enough */ - - checkRequestPermission(Manifest.permission.READ_EXTERNAL_STORAGE); - } - - private void connectClient() { - mClient = new Main.Client(this, new Main.Client.Callback() { - - private void removeMessages() { - /* don't remove log messages */ - mHandler.removeMessages(MSG_STOPPED); - mHandler.removeMessages(MSG_STARTED); - mHandler.removeMessages(MSG_ERROR); - } - - @Override - public void onStopped() { - removeMessages(); - mHandler.sendEmptyMessage(MSG_STOPPED); - } - - @Override - public void onStarted() { - removeMessages(); - mHandler.sendEmptyMessage(MSG_STARTED); - } - - @Override - public void onError(String error) { - removeMessages(); - mHandler.sendMessage(Message.obtain(mHandler, MSG_ERROR, error)); - } - - @Override - public void onLog(int priority, String msg) { - mHandler.sendMessage(Message.obtain(mHandler, MSG_LOG, priority, 0, msg)); - } - }); - } - - @Override - protected void onStart() { - mFirstRun = false; - connectClient(); - super.onStart(); - } - - @Override - protected void onStop() { - mClient.release(); - mClient = null; - super.onStop(); - } -} diff --git a/android/app/src/main/java/org/musicpd/ui/NetworkAddress.kt b/android/app/src/main/java/org/musicpd/ui/NetworkAddress.kt new file mode 100644 index 000000000..fd387ca6a --- /dev/null +++ b/android/app/src/main/java/org/musicpd/ui/NetworkAddress.kt @@ -0,0 +1,39 @@ +package org.musicpd.ui + +import android.app.Application +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Wifi +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import org.musicpd.NetworkUtil + +@Composable +fun NetworkAddress() { + val address = NetworkUtil.getDeviceIPV4Address(LocalContext.current) + val padding = 4.dp + Row( + Modifier + .padding(padding) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + imageVector = Icons.Default.Wifi, + contentDescription = "Wifi") + Spacer(Modifier.size(padding)) + Text(text = address ?: "") + } +} + diff --git a/android/app/src/main/java/org/musicpd/ui/Preferences.kt b/android/app/src/main/java/org/musicpd/ui/Preferences.kt new file mode 100644 index 000000000..da357559d --- /dev/null +++ b/android/app/src/main/java/org/musicpd/ui/Preferences.kt @@ -0,0 +1,52 @@ +package org.musicpd.ui + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.BatteryAlert +import androidx.compose.material.icons.filled.Headphones +import androidx.compose.material.icons.filled.PowerSettingsNew +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.alorma.compose.settings.storage.preferences.rememberPreferenceBooleanSettingState +import com.alorma.compose.settings.ui.SettingsSwitch +import org.musicpd.Preferences +import org.musicpd.R + +@Composable +fun SettingsOptions( + onBootChanged: (Boolean) -> Unit, + onWakeLockChanged: (Boolean) -> Unit, + onHeadphonesChanged: (Boolean) -> Unit +) { + val bootState = rememberPreferenceBooleanSettingState( + key = Preferences.KEY_RUN_ON_BOOT, + defaultValue = false + ) + val wakelockState = + rememberPreferenceBooleanSettingState(key = Preferences.KEY_WAKELOCK, defaultValue = false) + val headphoneState = rememberPreferenceBooleanSettingState( + key = Preferences.KEY_PAUSE_ON_HEADPHONES_DISCONNECT, + defaultValue = false + ) + + SettingsSwitch( + icon = { Icon(imageVector = Icons.Default.PowerSettingsNew, contentDescription = "Power") }, + title = { Text(text = stringResource(R.string.checkbox_run_on_boot)) }, + onCheckedChange = onBootChanged, + state = bootState + ) + SettingsSwitch( + icon = { Icon(imageVector = Icons.Default.BatteryAlert, contentDescription = "Battery") }, + title = { Text(text = stringResource(R.string.checkbox_wakelock)) }, + onCheckedChange = onWakeLockChanged, + state = wakelockState + ) + SettingsSwitch( + icon = { Icon(imageVector = Icons.Default.Headphones, contentDescription = "Headphones") }, + title = { Text(text = stringResource(R.string.checkbox_pause_on_headphones_disconnect)) }, + onCheckedChange = onHeadphonesChanged, + state = headphoneState + ) + +} diff --git a/android/app/src/main/java/org/musicpd/ui/SettingsActivity.kt b/android/app/src/main/java/org/musicpd/ui/SettingsActivity.kt new file mode 100644 index 000000000..56e4b0f92 --- /dev/null +++ b/android/app/src/main/java/org/musicpd/ui/SettingsActivity.kt @@ -0,0 +1,212 @@ +package org.musicpd.ui + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Circle +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.google.accompanist.permissions.shouldShowRationale +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.musicpd.Main +import org.musicpd.R + +class SettingsActivity : ComponentActivity() { + + private val settingsViewModel: SettingsViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + MaterialTheme { + SettingsContainer(settingsViewModel) + } + } + } + + private fun connectClient() { + val client = Main.Client(this, object : Main.Client.Callback { + override fun onStopped() { + settingsViewModel.updateStatus("", false) + } + + override fun onStarted() { + settingsViewModel.updateStatus("MPD Service Started", true) + } + + override fun onError(error: String) { + settingsViewModel.removeClient() + settingsViewModel.updateStatus(error, false) + connectClient() + } + + override fun onLog(priority: Int, msg: String) { + settingsViewModel.addLogItem(priority, msg) + } + }) + + settingsViewModel.setClient(client) + } + + override fun onStart() { + //mFirstRun = false + connectClient() + super.onStart() + } + + override fun onStop() { + settingsViewModel.removeClient() + super.onStop() + } +} + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun SettingsContainer(settingsViewModel: SettingsViewModel = viewModel()) { + val context = LocalContext.current + + val storagePermissionState = rememberPermissionState( + android.Manifest.permission.READ_EXTERNAL_STORAGE + ) + + if (storagePermissionState.status.shouldShowRationale) { + Column(Modifier + .padding(4.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text(stringResource(id = R.string.external_files_permission_request)) + Button(onClick = { }) { + Text("Request permission") + } + } + } else { + Column { + NetworkAddress() + ServerStatus(settingsViewModel) + if (!storagePermissionState.status.isGranted) { + OutlinedButton(onClick = { storagePermissionState.launchPermissionRequest() }, Modifier + .padding(4.dp) + .fillMaxWidth()) { + Text("Request external storage permission", color = MaterialTheme.colorScheme.secondary) + } + } + SettingsOptions( + onBootChanged = { newValue -> + if (newValue) { + settingsViewModel.startMPD(context) + } + }, + onWakeLockChanged = { newValue -> + settingsViewModel.setWakelockEnabled(newValue) + }, + onHeadphonesChanged = { newValue -> + settingsViewModel.setPauseOnHeadphonesDisconnect(newValue) + } + ) + LogView(settingsViewModel.logItemFLow.collectAsStateWithLifecycle()) + } + } +} + +@Composable +fun ServerStatus(settingsViewModel: SettingsViewModel) { + val context = LocalContext.current + + val statusUiState by settingsViewModel.statusUIState.collectAsState() + + Column { + Row( + Modifier + .padding(4.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Row { + Icon( + imageVector = Icons.Default.Circle, + contentDescription = "", + tint = if (statusUiState.running) Color(0xFFB8F397) else Color(0xFFFFDAD6) + ) + Text(text = if (statusUiState.running) "Running" else "Stopped") + } + Button(onClick = { + if (statusUiState.running) + settingsViewModel.stopMPD() + else + settingsViewModel.startMPD(context) + }) { + Text(text = if (statusUiState.running) "Stop MPD" else "Start MPD") + } + } + Row( + Modifier + .padding(4.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Text(text = statusUiState.statusMessage) + } + } +} + +@Composable +fun LogView(messages: State>) { + val state = rememberLazyListState() + + LazyColumn( + Modifier.padding(4.dp), + state + ) { + items(messages.value) { message -> + Text(text = message, fontFamily = FontFamily.Monospace) + } + CoroutineScope(Dispatchers.Main).launch { + state.scrollToItem(messages.value.count(), 0) + } + } +} + +@Preview(showBackground = true) +@Composable +fun SettingsPreview() { + MaterialTheme { + SettingsContainer() + } +} \ No newline at end of file diff --git a/android/app/src/main/java/org/musicpd/ui/SettingsViewModel.kt b/android/app/src/main/java/org/musicpd/ui/SettingsViewModel.kt new file mode 100644 index 000000000..3d09370ee --- /dev/null +++ b/android/app/src/main/java/org/musicpd/ui/SettingsViewModel.kt @@ -0,0 +1,84 @@ +package org.musicpd.ui + +import android.content.Context +import android.util.Log +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.musicpd.Main +import org.musicpd.Preferences + +private const val MAX_LOGS = 500 + +class SettingsViewModel : ViewModel() { + + private var mClient: Main.Client? = null + + private val _logItemFLow = MutableStateFlow(listOf()) + val logItemFLow: StateFlow> = _logItemFLow + + data class StatusUiState( + val statusMessage: String = "", + val running: Boolean = false + ) + + private val _statusUIState = MutableStateFlow(StatusUiState()) + val statusUIState: StateFlow = _statusUIState.asStateFlow() + + fun addLogItem(priority: Int, message: String) { + if (_logItemFLow.value.size > MAX_LOGS) { + _logItemFLow.value = _logItemFLow.value.drop(1) + } + + val priorityString: String = when (priority) { + Log.DEBUG -> "D" + Log.ERROR -> "E" + Log.INFO -> "I" + Log.VERBOSE -> "V" + Log.WARN -> "W" + else -> "" + } + + _logItemFLow.value = _logItemFLow.value + ("$priorityString/$message") + } + + fun updateStatus(message: String, running: Boolean) { + _statusUIState.value = StatusUiState(message, running) + } + + fun setClient(client: Main.Client) { + mClient = client + } + + fun removeClient() { + mClient?.release() + mClient = null + } + + fun startMPD(context: Context) { + mClient?.start() + if (Preferences.getBoolean( + context, + Preferences.KEY_WAKELOCK, false + ) + ) mClient?.setWakelockEnabled(true) + if (Preferences.getBoolean( + context, + Preferences.KEY_PAUSE_ON_HEADPHONES_DISCONNECT, false + ) + ) mClient?.setPauseOnHeadphonesDisconnect(true) + } + + fun stopMPD() { + mClient?.stop() + } + + fun setWakelockEnabled(enabled: Boolean) { + mClient?.setWakelockEnabled(enabled) + } + + fun setPauseOnHeadphonesDisconnect(enabled: Boolean) { + mClient?.setPauseOnHeadphonesDisconnect(enabled) + } +} \ No newline at end of file diff --git a/android/app/src/main/res/layout/log_item.xml b/android/app/src/main/res/layout/log_item.xml deleted file mode 100644 index e6e74c913..000000000 --- a/android/app/src/main/res/layout/log_item.xml +++ /dev/null @@ -1,5 +0,0 @@ - - diff --git a/android/app/src/main/res/layout/settings.xml b/android/app/src/main/res/layout/settings.xml deleted file mode 100644 index c7c6c400b..000000000 --- a/android/app/src/main/res/layout/settings.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 2df00470e..92f8139b2 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -9,4 +9,5 @@ Run MPD automatically on boot Prevent suspend when MPD is running (Wakelock) Pause MPD when headphones disconnect + MPD requires access to external files to play local music. Please grant the permission. diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml new file mode 100644 index 000000000..c9502c8f8 --- /dev/null +++ b/android/app/src/main/res/values/themes.xml @@ -0,0 +1,4 @@ + + +