diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 1b88f67f8..0145aebac 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -70,6 +70,8 @@ dependencies { ksp("com.google.dagger:dagger-compiler:2.49") ksp("com.google.dagger:hilt-compiler:2.49") + 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/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/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 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 78bf82fb5..25827b53e 100644 --- a/android/app/src/main/java/org/musicpd/Main.java +++ b/android/app/src/main/java/org/musicpd/Main.java @@ -16,11 +16,16 @@ 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 androidx.annotation.OptIn; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaSession; + import org.musicpd.data.LoggingRepository; import org.musicpd.ui.SettingsActivity; @@ -51,6 +56,8 @@ public class Main extends Service implements Runnable { private boolean mPauseOnHeadphonesDisconnect = false; private PowerManager.WakeLock mWakelock = null; + private MediaSession mMediaSession = null; + @Inject LoggingRepository logging; @@ -184,6 +191,7 @@ public class Main extends Service implements Runnable { } } + @OptIn(markerClass = UnstableApi.class) private void start() { if (mThread != null) return; @@ -225,11 +233,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) { diff --git a/src/Main.cxx b/src/Main.cxx index 4a469bc4f..6b10ff6d4 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