From e086f09d48cd2187b995b75468a74241343cd5c2 Mon Sep 17 00:00:00 2001
From: Colin Edwards <colin@recursivepenguin.com>
Date: Thu, 28 Dec 2023 22:46:59 -0600
Subject: [PATCH 1/2] android: add next and previous track to the jni bridge

This will allow the android client to directly make calls to the mpd process to change tracks

I went with camel case on the function names here, if you use an underscore
javac generates a function tht looks like this:
 JNIEXPORT void JNICALL Java_org_musicpd_Bridge_play_1previous

I figured what we ended up with looks a little nicer:
JNIEXPORT void JNICALL Java_org_musicpd_Bridge_playPrevious
---
 .../app/src/main/java/org/musicpd/Bridge.java |  2 ++
 src/Main.cxx                                  | 22 +++++++++++++++++++
 2 files changed, 24 insertions(+)

diff --git a/android/app/src/main/java/org/musicpd/Bridge.java b/android/app/src/main/java/org/musicpd/Bridge.java
index 6ad49e2d4..0ca31ebe6 100644
--- a/android/app/src/main/java/org/musicpd/Bridge.java
+++ b/android/app/src/main/java/org/musicpd/Bridge.java
@@ -18,4 +18,6 @@ public class Bridge {
 	public static native void run(Context context, LogListener logListener);
 	public static native void shutdown();
 	public static native void pause();
+	public static native void playNext();
+	public static native void playPrevious();
 }
diff --git a/src/Main.cxx b/src/Main.cxx
index 4a469bc4f..e66e7f277 100644
--- a/src/Main.cxx
+++ b/src/Main.cxx
@@ -614,6 +614,28 @@ Java_org_musicpd_Bridge_pause(JNIEnv *, jclass)
 			partition.pc.LockSetPause(true);
 }
 
+gcc_visibility_default
+JNIEXPORT void JNICALL
+Java_org_musicpd_Bridge_playNext(JNIEnv *, jclass)
+{
+	if (global_instance != nullptr)
+		BlockingCall(global_instance->event_loop, [&](){
+			for (auto &partition : global_instance->partitions)
+				partition.PlayNext();
+		});	
+}
+
+gcc_visibility_default
+JNIEXPORT void JNICALL
+Java_org_musicpd_Bridge_playPrevious(JNIEnv *, jclass)
+{
+		if (global_instance != nullptr)
+		BlockingCall(global_instance->event_loop, [&](){
+			for (auto &partition : global_instance->partitions)
+				partition.PlayPrevious();
+		});	
+}
+
 #else
 
 static inline void

From 3711bd0d24b71abad9c2a7198a2efefae664b493 Mon Sep 17 00:00:00 2001
From: Colin Edwards <colin@recursivepenguin.com>
Date: Fri, 5 Jan 2024 18:17:54 -0600
Subject: [PATCH 2/2] android: Implement basic media session handling for next
 and previous track

This starts a Media3 MediaSession when the service starts. A custom player class gets passed into that session to receive commands from other apps and the android os.

Currently we pad out some dummy items to make SimpleBasePlayer think we can do next and previous tracks.

MPD handles the threading for the native calls so we can just directly call the bridge from the player class.
---
 android/app/build.gradle.kts                  |  2 +
 .../src/main/java/org/musicpd/MPDPlayer.java  | 65 +++++++++++++++++++
 .../app/src/main/java/org/musicpd/Main.java   | 15 ++++-
 3 files changed, 79 insertions(+), 3 deletions(-)
 create mode 100644 android/app/src/main/java/org/musicpd/MPDPlayer.java

diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index cb0bb9771..402bed883 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -64,6 +64,8 @@ dependencies {
     implementation("com.github.alorma:compose-settings-storage-preferences:1.0.3")
     implementation("com.google.accompanist:accompanist-permissions:0.33.2-alpha")
 
+    implementation("androidx.media3:media3-session:1.2.0")
+
     // Android Studio Preview support
     implementation("androidx.compose.ui:ui-tooling-preview")
     debugImplementation("androidx.compose.ui:ui-tooling")
diff --git a/android/app/src/main/java/org/musicpd/MPDPlayer.java b/android/app/src/main/java/org/musicpd/MPDPlayer.java
new file mode 100644
index 000000000..3a1d8c087
--- /dev/null
+++ b/android/app/src/main/java/org/musicpd/MPDPlayer.java
@@ -0,0 +1,65 @@
+package org.musicpd;
+
+import android.annotation.SuppressLint;
+import android.os.Looper;
+
+import androidx.annotation.NonNull;
+import androidx.media3.common.Player;
+import androidx.media3.common.SimpleBasePlayer;
+import androidx.media3.common.util.UnstableApi;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.Arrays;
+import java.util.List;
+
+@UnstableApi
+public class MPDPlayer extends SimpleBasePlayer {
+
+    List<MediaItemData> placeholderItems;
+    public MPDPlayer(Looper looper) {
+        super(looper);
+
+        // Dummy items to let us receive next and previous commands
+        MediaItemData item0 = new MediaItemData.Builder(0)
+                .build();
+        MediaItemData item1 = new MediaItemData.Builder(1)
+                .build();
+        MediaItemData item2 = new MediaItemData.Builder(2)
+                .build();
+        MediaItemData[] items = new MediaItemData[] { item0, item1, item2 };
+
+        placeholderItems = Arrays.asList(items);
+    }
+
+    @NonNull
+    @Override
+    protected State getState() {
+        Commands commands = new Commands.Builder().addAll(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, COMMAND_SEEK_TO_NEXT_MEDIA_ITEM).build();
+
+        return new State.Builder()
+                .setAvailableCommands(commands)
+                .setPlaybackState(Player.STATE_READY)
+                .setPlaylist(placeholderItems)
+                .setCurrentMediaItemIndex(1)
+                .build();
+    }
+
+    @NonNull
+    @SuppressLint("SwitchIntDef")
+    @Override
+    protected ListenableFuture<?> handleSeek(int mediaItemIndex, long positionMs, int seekCommand) {
+        switch (seekCommand) {
+            case COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM:
+            case COMMAND_SEEK_TO_PREVIOUS:
+                Bridge.playPrevious();
+                break;
+            case COMMAND_SEEK_TO_NEXT_MEDIA_ITEM:
+            case COMMAND_SEEK_TO_NEXT:
+                Bridge.playNext();
+                break;
+        }
+        return Futures.immediateVoidFuture();
+    }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/org/musicpd/Main.java b/android/app/src/main/java/org/musicpd/Main.java
index 7196e0c5f..532039b48 100644
--- a/android/app/src/main/java/org/musicpd/Main.java
+++ b/android/app/src/main/java/org/musicpd/Main.java
@@ -3,7 +3,6 @@
 
 package org.musicpd;
 
-import android.annotation.TargetApi;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
@@ -17,13 +16,15 @@ import android.content.ServiceConnection;
 import android.media.AudioManager;
 import android.os.Build;
 import android.os.IBinder;
+import android.os.Looper;
 import android.os.PowerManager;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.util.Log;
-import android.widget.RemoteViews;
 
-import androidx.core.app.ServiceCompat;
+import androidx.annotation.OptIn;
+import androidx.media3.common.util.UnstableApi;
+import androidx.media3.session.MediaSession;
 
 import org.musicpd.ui.SettingsActivity;
 
@@ -50,6 +51,8 @@ public class Main extends Service implements Runnable {
 	private boolean mPauseOnHeadphonesDisconnect = false;
 	private PowerManager.WakeLock mWakelock = null;
 
+	private MediaSession mMediaSession = null;
+
 	static class MainStub extends IMain.Stub {
 		private Main mService;
 		MainStub(Main service) {
@@ -183,6 +186,7 @@ public class Main extends Service implements Runnable {
 		}
 	}
 
+	@OptIn(markerClass = UnstableApi.class)
 	private void start() {
 		if (mThread != null)
 			return;
@@ -224,11 +228,16 @@ public class Main extends Service implements Runnable {
 		mThread = new Thread(this);
 		mThread.start();
 
+		MPDPlayer player = new MPDPlayer(Looper.getMainLooper());
+		mMediaSession = new MediaSession.Builder(this, player).build();
+
 		startForeground(R.string.notification_title_mpd_running, notification);
 		startService(new Intent(this, Main.class));
 	}
 
 	private void stop() {
+		mMediaSession.release();
+		mMediaSession = null;
 		if (mThread != null) {
 			if (mThread.isAlive()) {
 				synchronized (this) {