This commit is contained in:
Max Kellermann 2024-04-08 08:23:25 +02:00
commit cd3c34e7b9
7 changed files with 216 additions and 158 deletions

View File

@ -53,6 +53,16 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".AutomationReceiver"
android:exported="true">
<intent-filter>
<action android:name="org.musicpd.action.StartService" />
</intent-filter>
<intent-filter>
<action android:name="org.musicpd.action.StopService" />
</intent-filter>
</receiver>
<service <service
android:name=".Main" /> android:name=".Main" />
</application> </application>

View File

@ -0,0 +1,24 @@
package org.musicpd
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
class AutomationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when(intent.action) {
"org.musicpd.action.StartService" -> {
val wakelock = Preferences.getBoolean(
context,
Preferences.KEY_WAKELOCK, false
)
Main.startService(context, wakelock)
}
"org.musicpd.action.StopService" -> {
context.startService(Intent(context, Main::class.java)
.setAction(Main.SHUTDOWN_ACTION))
}
}
}
}

View File

@ -8,11 +8,9 @@ import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.media.AudioManager; import android.media.AudioManager;
import android.os.Build; import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
@ -26,10 +24,12 @@ import androidx.annotation.OptIn;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.session.MediaSession; import androidx.media3.session.MediaSession;
import org.jetbrains.annotations.NotNull;
import org.musicpd.data.LoggingRepository; import org.musicpd.data.LoggingRepository;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Objects;
import javax.inject.Inject; import javax.inject.Inject;
@ -37,9 +37,10 @@ import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint @AndroidEntryPoint
public class Main extends Service implements Runnable { public class Main extends Service implements Runnable {
private static final String TAG = "Main"; private static final String TAG = "Main";
private static final String WAKELOCK_TAG = "mpd:wakelockmain"; private static final String WAKELOCK_TAG = "mpd:wakelockmain";
private static final String REMOTE_ERROR = "MPD process was killed";
private static final int MAIN_STATUS_ERROR = -1; private static final int MAIN_STATUS_ERROR = -1;
private static final int MAIN_STATUS_STOPPED = 0; private static final int MAIN_STATUS_STOPPED = 0;
private static final int MAIN_STATUS_STARTED = 1; private static final int MAIN_STATUS_STARTED = 1;
@ -60,6 +61,9 @@ public class Main extends Service implements Runnable {
@Inject @Inject
LoggingRepository logging; LoggingRepository logging;
@NotNull
public static final String SHUTDOWN_ACTION = "org.musicpd.action.ShutdownMPD";
static class MainStub extends IMain.Stub { static class MainStub extends IMain.Stub {
private Main mService; private Main mService;
MainStub(Main service) { MainStub(Main service) {
@ -129,9 +133,13 @@ public class Main extends Service implements Runnable {
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
if (Objects.equals(intent.getAction(), SHUTDOWN_ACTION)) {
stop();
} else {
start(); start();
if (intent != null && intent.getBooleanExtra("wakelock", false)) if (intent.getBooleanExtra("wakelock", false))
setWakelockEnabled(true); setWakelockEnabled(true);
}
return START_STICKY; return START_STICKY;
} }
@ -307,156 +315,10 @@ public class Main extends Service implements Runnable {
} }
} }
/*
* Client that bind the Main Service in order to send commands and receive callback
*/
public static class Client {
public interface Callback {
public void onStarted();
public void onStopped();
public void onError(String error);
}
private boolean mBound = false;
private final Context mContext;
private Callback mCallback;
private IMain mIMain = null;
private final IMainCallback.Stub mICallback = new IMainCallback.Stub() {
@Override
public void onStopped() throws RemoteException {
mCallback.onStopped();
}
@Override
public void onStarted() throws RemoteException {
mCallback.onStarted();
}
@Override
public void onError(String error) throws RemoteException {
mCallback.onError(error);
}
};
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (this) {
mIMain = IMain.Stub.asInterface(service);
try {
if (mCallback != null)
mIMain.registerCallback(mICallback);
} catch (RemoteException e) {
if (mCallback != null)
mCallback.onError(REMOTE_ERROR);
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
if (mCallback != null)
mCallback.onError(REMOTE_ERROR);
}
};
public Client(Context context, Callback cb) throws IllegalArgumentException {
if (context == null)
throw new IllegalArgumentException("Context can't be null");
mContext = context;
mCallback = cb;
mBound = mContext.bindService(new Intent(mContext, Main.class), mServiceConnection, Context.BIND_AUTO_CREATE);
}
public boolean start() {
synchronized (this) {
if (mIMain != null) {
try {
mIMain.start();
return true;
} catch (RemoteException e) {
}
}
return false;
}
}
public boolean stop() {
synchronized (this) {
if (mIMain != null) {
try {
mIMain.stop();
return true;
} catch (RemoteException e) {
}
}
return false;
}
}
public boolean setPauseOnHeadphonesDisconnect(boolean enabled) {
synchronized (this) {
if (mIMain != null) {
try {
mIMain.setPauseOnHeadphonesDisconnect(enabled);
return true;
} catch (RemoteException e) {
}
}
return false;
}
}
public boolean setWakelockEnabled(boolean enabled) {
synchronized (this) {
if (mIMain != null) {
try {
mIMain.setWakelockEnabled(enabled);
return true;
} catch (RemoteException e) {
}
}
return false;
}
}
public boolean isRunning() {
synchronized (this) {
if (mIMain != null) {
try {
return mIMain.isRunning();
} catch (RemoteException e) {
}
}
return false;
}
}
public void release() {
if (mBound) {
synchronized (this) {
if (mIMain != null && mICallback != null) {
try {
if (mCallback != null)
mIMain.unregisterCallback(mICallback);
} catch (RemoteException e) {
}
}
}
mBound = false;
mContext.unbindService(mServiceConnection);
}
}
}
/* /*
* start Main service without any callback * start Main service without any callback
*/ */
public static void start(Context context, boolean wakelock) { public static void startService(Context context, boolean wakelock) {
Intent intent = new Intent(context, Main.class) Intent intent = new Intent(context, Main.class)
.putExtra("wakelock", wakelock); .putExtra("wakelock", wakelock);
@ -468,4 +330,9 @@ public class Main extends Service implements Runnable {
else else
context.startService(intent); context.startService(intent);
} }
public static void stopService(Context context) {
Intent intent = new Intent(context, Main.class);
context.stopService(intent);
}
} }

View File

@ -25,7 +25,7 @@ class MainActivity : ComponentActivity() {
} }
private fun connectClient() { private fun connectClient() {
val client = Main.Client(this, object : Main.Client.Callback { val client = MainServiceClient(this, object : MainServiceClient.Callback {
override fun onStopped() { override fun onStopped() {
settingsViewModel.updateStatus("", false) settingsViewModel.updateStatus("", false)
} }

View File

@ -0,0 +1,157 @@
package org.musicpd;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
/*
* Client that bind the Main Service in order to send commands and receive callback
*/
public class MainServiceClient {
private static final String REMOTE_ERROR = "MPD process was killed";
public interface Callback {
public void onStarted();
public void onStopped();
public void onError(String error);
}
private boolean mBound = false;
private final Context mContext;
private Callback mCallback;
private IMain mIMain = null;
private final IMainCallback.Stub mICallback = new IMainCallback.Stub() {
@Override
public void onStopped() throws RemoteException {
mCallback.onStopped();
}
@Override
public void onStarted() throws RemoteException {
mCallback.onStarted();
}
@Override
public void onError(String error) throws RemoteException {
mCallback.onError(error);
}
};
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (this) {
mIMain = IMain.Stub.asInterface(service);
try {
if (mCallback != null)
mIMain.registerCallback(mICallback);
} catch (RemoteException e) {
if (mCallback != null)
mCallback.onError(REMOTE_ERROR);
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
if (mCallback != null)
mCallback.onError(REMOTE_ERROR);
}
};
public MainServiceClient(Context context, Callback cb) throws IllegalArgumentException {
if (context == null)
throw new IllegalArgumentException("Context can't be null");
mContext = context;
mCallback = cb;
mBound = mContext.bindService(new Intent(mContext, Main.class), mServiceConnection, Context.BIND_AUTO_CREATE);
}
public boolean start() {
synchronized (this) {
if (mIMain != null) {
try {
mIMain.start();
return true;
} catch (RemoteException e) {
}
}
return false;
}
}
public boolean stop() {
synchronized (this) {
if (mIMain != null) {
try {
mIMain.stop();
return true;
} catch (RemoteException e) {
}
}
return false;
}
}
public boolean setPauseOnHeadphonesDisconnect(boolean enabled) {
synchronized (this) {
if (mIMain != null) {
try {
mIMain.setPauseOnHeadphonesDisconnect(enabled);
return true;
} catch (RemoteException e) {
}
}
return false;
}
}
public boolean setWakelockEnabled(boolean enabled) {
synchronized (this) {
if (mIMain != null) {
try {
mIMain.setWakelockEnabled(enabled);
return true;
} catch (RemoteException e) {
}
}
return false;
}
}
public boolean isRunning() {
synchronized (this) {
if (mIMain != null) {
try {
return mIMain.isRunning();
} catch (RemoteException e) {
}
}
return false;
}
}
public void release() {
if (mBound) {
synchronized (this) {
if (mIMain != null && mICallback != null) {
try {
if (mCallback != null)
mIMain.unregisterCallback(mICallback);
} catch (RemoteException e) {
}
}
}
mBound = false;
mContext.unbindService(mServiceConnection);
}
}
}

View File

@ -27,7 +27,7 @@ public class Receiver extends BroadcastReceiver {
final boolean wakelock = final boolean wakelock =
Preferences.getBoolean(context, Preferences.getBoolean(context,
Preferences.KEY_WAKELOCK, false); Preferences.KEY_WAKELOCK, false);
Main.start(context, wakelock); Main.startService(context, wakelock);
} }
} }
} }

View File

@ -6,7 +6,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import org.musicpd.Main import org.musicpd.MainServiceClient
import org.musicpd.Preferences import org.musicpd.Preferences
import org.musicpd.data.LoggingRepository import org.musicpd.data.LoggingRepository
import javax.inject.Inject import javax.inject.Inject
@ -16,7 +16,7 @@ import javax.inject.Inject
class SettingsViewModel @Inject constructor( class SettingsViewModel @Inject constructor(
private var loggingRepository: LoggingRepository private var loggingRepository: LoggingRepository
) : ViewModel() { ) : ViewModel() {
private var mClient: Main.Client? = null private var mClient: MainServiceClient? = null
data class StatusUiState( data class StatusUiState(
val statusMessage: String = "", val statusMessage: String = "",
@ -34,7 +34,7 @@ class SettingsViewModel @Inject constructor(
_statusUIState.value = StatusUiState(message, running) _statusUIState.value = StatusUiState(message, running)
} }
fun setClient(client: Main.Client) { fun setClient(client: MainServiceClient) {
mClient = client mClient = client
} }