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.
This commit is contained in:
Colin Edwards 2023-12-22 22:52:33 -06:00
parent 625ab6decd
commit ddc048e2c3
13 changed files with 410 additions and 340 deletions

View File

@ -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")

View File

@ -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">
<activity
android:name=".Settings"
android:name=".ui.SettingsActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>

View File

@ -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,

View File

@ -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());

View File

@ -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<String> mLogListArray = new LinkedList<String>();
private ListView mLogListView;
private ArrayAdapter<String> 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<String>(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();
}
}

View File

@ -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 ?: "")
}
}

View File

@ -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
)
}

View File

@ -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<List<String>>) {
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()
}
}

View File

@ -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<String>())
val logItemFLow: StateFlow<List<String>> = _logItemFLow
data class StatusUiState(
val statusMessage: String = "",
val running: Boolean = false
)
private val _statusUIState = MutableStateFlow(StatusUiState())
val statusUIState: StateFlow<StatusUiState> = _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)
}
}

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:typeface="monospace" />

View File

@ -1,65 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="center"
android:src="@drawable/baseline_wifi_24"
android:layout_margin="4dp"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/networkAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginRight="24sp"
android:gravity="center"
android:padding="4dp"
android:textSize="24sp" />
</LinearLayout>
<ToggleButton
android:id="@+id/run"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textOff="@string/toggle_button_run_off"
android:textOn="@string/toggle_button_run_on" />
<CheckBox
android:id="@+id/run_on_boot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/checkbox_run_on_boot" />
<CheckBox
android:id="@+id/wakelock"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/checkbox_wakelock" />
<CheckBox
android:id="@+id/pause_on_headphones_disconnect"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/checkbox_pause_on_headphones_disconnect" />
<TextView
android:id="@+id/status"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ListView
android:id="@+id/log_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dip" />
</LinearLayout>

View File

@ -9,4 +9,5 @@
<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>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.MPD" parent="android:Theme.Material.Light.NoActionBar" />
</resources>