implementing MediaSession using MPRIS

NotificationManager will now ignore MediaStyle notifications
This commit is contained in:
Julian Winkler
2024-07-15 16:39:45 +02:00
parent eddd827e27
commit b54bed4784
9 changed files with 272 additions and 14 deletions

View File

@@ -47,6 +47,10 @@ linux_dmabuf = wl_mod.scan_xml(xml)
xml = wl_mod.find_protocol('viewporter') xml = wl_mod.find_protocol('viewporter')
viewporter = wl_mod.scan_xml(xml) viewporter = wl_mod.scan_xml(xml)
mpris = gnome.gdbus_codegen('mpris-dbus',
'src/api-impl-jni/media/org.mpris.MediaPlayer2.xml',
interface_prefix: 'org.mpris')
# libandroid # libandroid
libandroid_so = shared_library('android', [ libandroid_so = shared_library('android', [
'src/libandroid/asset_manager.c', 'src/libandroid/asset_manager.c',
@@ -105,6 +109,7 @@ libtranslationlayer_so = shared_library('translation_layer_main', [
'src/api-impl-jni/graphics/android_graphics_drawable_DrawableContainer.c', 'src/api-impl-jni/graphics/android_graphics_drawable_DrawableContainer.c',
'src/api-impl-jni/location/android_location_LocationManager.c', 'src/api-impl-jni/location/android_location_LocationManager.c',
'src/api-impl-jni/media/android_media_MediaCodec.c', 'src/api-impl-jni/media/android_media_MediaCodec.c',
'src/api-impl-jni/media/android_media_session_MediaSession.c',
'src/api-impl-jni/net/android_net_ConnectivityManager.c', 'src/api-impl-jni/net/android_net_ConnectivityManager.c',
'src/api-impl-jni/sensors/android_hardware_SensorManager.c', 'src/api-impl-jni/sensors/android_hardware_SensorManager.c',
'src/api-impl-jni/util.c', 'src/api-impl-jni/util.c',
@@ -131,6 +136,7 @@ libtranslationlayer_so = shared_library('translation_layer_main', [
'src/sk_area/sk_area.c', 'src/sk_area/sk_area.c',
linux_dmabuf, linux_dmabuf,
viewporter, viewporter,
mpris,
] + marshal_files, ] + marshal_files,
include_directories: ['src/sk_area/'], include_directories: ['src/sk_area/'],
install: true, install: true,

View File

@@ -0,0 +1,29 @@
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class android_media_session_MediaSession */
#ifndef _Included_android_media_session_MediaSession
#define _Included_android_media_session_MediaSession
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: android_media_session_MediaSession
* Method: nativeSetState
* Signature: (IJJJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_android_media_session_MediaSession_nativeSetState
(JNIEnv *, jobject, jint, jlong, jlong, jlong, jstring, jstring, jstring);
/*
* Class: android_media_session_MediaSession
* Method: nativeSetCallback
* Signature: (Landroid/media/session/MediaSession/Callback;)V
*/
JNIEXPORT void JNICALL Java_android_media_session_MediaSession_nativeSetCallback
(JNIEnv *, jobject, jobject);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,117 @@
#include "../util.h"
#include "../defines.h"
#include "mpris-dbus.h"
#include "../generated_headers/android_media_session_MediaSession.h"
#include "../generated_headers/android_os_SystemClock.h"
#define MPRIS_BUS_NAME_PREFIX "org.mpris.MediaPlayer2."
#define MPRIS_OBJECT_NAME "/org/mpris/MediaPlayer2"
MediaPlayer2Player *mpris_player = NULL;
static jobject callback = NULL;
static jlong last_position = 0; // playback_position - SystemClock.elapsedRealtime in ms
static void on_bus_acquired(GDBusConnection *connection, const char *name, gpointer user_data)
{
g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON (mpris_player),
connection, MPRIS_OBJECT_NAME, NULL);
}
static gboolean on_media_player_handle_action(MediaPlayer2Player *mpris_player, GDBusMethodInvocation *invocation, char *method)
{
if (callback) {
JNIEnv *env = get_jni_env();
(*env)->CallVoidMethod(env, callback, _METHOD(_CLASS(callback), method, "()V"));
}
g_dbus_method_invocation_return_value(invocation, g_variant_new("()"));
return TRUE;
}
static gboolean on_media_player_handle_play_pause(MediaPlayer2Player *mpris_player, GDBusMethodInvocation *invocation, gpointer user_data)
{
gboolean is_playing = !strcmp("Playing", media_player2_player_get_playback_status(mpris_player));
return on_media_player_handle_action(mpris_player, invocation, is_playing ? "onPause" : "onPlay");
}
static gboolean on_media_player_handle_seek(MediaPlayer2Player *mpris_player, GDBusMethodInvocation *invocation, gint64 offset_us, gpointer user_data)
{
if (callback) {
JNIEnv *env = get_jni_env();
last_position += offset_us / 1000;
(*env)->CallVoidMethod(env, callback, _METHOD(_CLASS(callback), "onSeekTo", "(J)V"), last_position + Java_android_os_SystemClock_elapsedRealtime(env, NULL));
}
media_player2_player_complete_seek(mpris_player, invocation);
return TRUE;
}
static gboolean on_media_player_handle_set_position(MediaPlayer2Player *mpris_player, GDBusMethodInvocation *invocation, GVariant *trackid, gint64 pos_us, gpointer user_data)
{
if (callback) {
JNIEnv *env = get_jni_env();
(*env)->CallVoidMethod(env, callback, _METHOD(_CLASS(callback), "onSeekTo", "(J)V"), pos_us / 1000);
}
media_player2_player_complete_set_position(mpris_player, invocation);
return TRUE;
}
#define ACTION_PAUSE (1 << 1)
#define ACTION_PLAY (1 << 2)
#define ACTION_SKIP_TO_PREVIOUS (1 << 4)
#define ACTION_SKIP_TO_NEXT (1 << 5)
#define ACTION_SEEK_TO (1 << 8)
JNIEXPORT void JNICALL Java_android_media_session_MediaSession_nativeSetState(JNIEnv *env, jobject this, jint state,
jlong actions, jlong position, jlong update_time, jstring title_str, jstring artist_str, jstring art_url_str)
{
const char *playback_states[] = {"None", "Stopped", "Paused", "Playing"};
if (!mpris_player) {
mpris_player = media_player2_player_skeleton_new();
g_object_connect(mpris_player,
"signal::handle-play", on_media_player_handle_action, "onPlay",
"signal::handle-pause", on_media_player_handle_action, "onPause",
"signal::handle-next", on_media_player_handle_action, "onSkipToNext",
"signal::handle-previous", on_media_player_handle_action, "onSkipToPrevious",
"signal::handle-play-pause", on_media_player_handle_play_pause, NULL,
"signal::handle-seek", on_media_player_handle_seek, NULL,
"signal::handle-set-position", on_media_player_handle_set_position, NULL,
NULL);
g_bus_own_name(G_BUS_TYPE_SESSION, MPRIS_BUS_NAME_PREFIX "ATL", G_BUS_NAME_OWNER_FLAGS_NONE,
on_bus_acquired, NULL, NULL, mpris_player, NULL);
}
media_player2_player_set_playback_status(mpris_player, playback_states[state < 4 ? state : 0]);
media_player2_player_set_position(mpris_player, position * 1000);
last_position = position - update_time;
media_player2_player_set_can_control(mpris_player, !!(actions));
media_player2_player_set_can_play(mpris_player, !!(actions & ACTION_PLAY));
media_player2_player_set_can_pause(mpris_player, !!(actions & ACTION_PAUSE));
media_player2_player_set_can_seek(mpris_player, !!(actions & ACTION_SEEK_TO));
media_player2_player_set_can_go_next(mpris_player, !!(actions & ACTION_SKIP_TO_NEXT));
media_player2_player_set_can_go_previous(mpris_player, !!(actions & ACTION_SKIP_TO_PREVIOUS));
GVariantDict dict;
g_variant_dict_init(&dict, NULL);
g_variant_dict_insert(&dict, "mpris:trackid", "s", MPRIS_OBJECT_NAME "/Track/0");
if (art_url_str) {
const char *art_url = (*env)->GetStringUTFChars(env, art_url_str, NULL);
g_variant_dict_insert(&dict, "mpris:artUrl", "s", art_url);
(*env)->ReleaseStringUTFChars(env, art_url_str, art_url);
}
if (title_str) {
const char *title = (*env)->GetStringUTFChars(env, title_str, NULL);
g_variant_dict_insert(&dict, "xesam:title", "s", title);
(*env)->ReleaseStringUTFChars(env, title_str, title);
}
if (artist_str) {
const char *artist = (*env)->GetStringUTFChars(env, artist_str, NULL);
g_variant_dict_insert(&dict, "xesam:artist", "s", artist);
(*env)->ReleaseStringUTFChars(env, artist_str, artist);
}
media_player2_player_set_metadata(mpris_player, g_variant_dict_end(&dict));
}
JNIEXPORT void JNICALL Java_android_media_session_MediaSession_nativeSetCallback(JNIEnv *env, jobject this, jobject new_callback)
{
callback = _REF(new_callback);
}

View File

@@ -0,0 +1,25 @@
<node>
<interface name="org.mpris.MediaPlayer2.Player">
<method name="Play"/>
<method name="Pause"/>
<method name="PlayPause"/>
<method name="Next"/>
<method name="Previous"/>
<method name="Seek">
<arg name="Offset" direction="in" type="x"/>
</method>
<method name="SetPosition">
<arg name="TrackId" direction="in" type="o"/>
<arg name="Position" direction="in" type="x"/>
</method>
<property name="CanControl" type="b" access="read"/>
<property name="CanPlay" type="b" access="read"/>
<property name="CanPause" type="b" access="read"/>
<property name="CanSeek" type="b" access="read"/>
<property name="CanGoNext" type="b" access="read"/>
<property name="CanGoPrevious" type="b" access="read"/>
<property name="Metadata" type="a{sv}" access="read"/>
<property name="PlaybackStatus" type="s" access="read"/>
<property name="Position" type="x" access="read"/>
</interface>
</node>

View File

@@ -54,6 +54,7 @@ public class Notification implements Parcelable {
PendingIntent intent; PendingIntent intent;
String iconPath; String iconPath;
boolean ongoing; boolean ongoing;
Style style;
public String toString() { public String toString() {
return "Notification [" + title + ", " + text + ", " + actions + "]"; return "Notification [" + title + ", " + text + ", " + actions + "]";
@@ -151,6 +152,7 @@ public class Notification implements Parcelable {
} }
public Builder setStyle(Style style) { public Builder setStyle(Style style) {
notification.style = style;
if (style instanceof MediaStyle) { if (style instanceof MediaStyle) {
notification.ongoing = true; notification.ongoing = true;
} }

View File

@@ -1,5 +1,6 @@
package android.app; package android.app;
import android.app.Notification.MediaStyle;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@@ -10,6 +11,10 @@ public class NotificationManager {
public void cancelAll() {} public void cancelAll() {}
public void notify(String tag, int id, Notification notification) { public void notify(String tag, int id, Notification notification) {
if (notification.style instanceof MediaStyle) {
return; // MPRIS is handled by MediaSession implementation
}
System.out.println("notify(" + tag + ", " + id + ", " + notification + ") called"); System.out.println("notify(" + tag + ", " + id + ", " + notification + ") called");
long builder = nativeInitBuilder(); long builder = nativeInitBuilder();
for (Notification.Action action : notification.actions) { for (Notification.Action action : notification.actions) {

View File

@@ -6,24 +6,39 @@ import android.os.Bundle;
public class MediaDescription { public class MediaDescription {
public Uri iconUri;
public CharSequence title;
public CharSequence subtitle;
public static class Builder { public static class Builder {
MediaDescription description = new MediaDescription();
public Builder setMediaId(String mediaId) {return this;} public Builder setMediaId(String mediaId) {return this;}
public Builder setTitle(CharSequence title) {return this;} public Builder setTitle(CharSequence title) {
description.title = title;
return this;
}
public Builder setSubtitle(CharSequence subtitle) {return this;} public Builder setSubtitle(CharSequence subtitle) {
description.subtitle = subtitle;
return this;
}
public Builder setDescription(CharSequence description) {return this;} public Builder setDescription(CharSequence description) {return this;}
public Builder setIconBitmap(Bitmap iconBitmap) {return this;} public Builder setIconBitmap(Bitmap iconBitmap) {return this;}
public Builder setIconUri(Uri iconUri) {return this;} public Builder setIconUri(Uri iconUri) {
description.iconUri = iconUri;
return this;
}
public Builder setExtras(Bundle extras) {return this;} public Builder setExtras(Bundle extras) {return this;}
public MediaDescription build() { public MediaDescription build() {
return new MediaDescription(); return description;
} }
} }

View File

@@ -10,12 +10,20 @@ import android.os.Handler;
public class MediaSession { public class MediaSession {
private List<QueueItem> queue;
public static final class Token {} public static final class Token {}
public static abstract class Callback {} public static abstract class Callback {}
public static class QueueItem { public static class QueueItem {
public QueueItem(MediaDescription description, long id) {} long id;
MediaDescription description;
public QueueItem(MediaDescription description, long id) {
this.description = description;
this.id = id;
}
} }
public MediaSession(Context context, String tag) {} public MediaSession(Context context, String tag) {}
@@ -26,19 +34,41 @@ public class MediaSession {
public void setFlags(int flags) {} public void setFlags(int flags) {}
public void setCallback(Callback callback, Handler handler) {} public void setCallback(Callback callback, Handler handler) {
nativeSetCallback(callback);
}
public void setCallback(Callback callback) {} public void setCallback(Callback callback) {
nativeSetCallback(callback);
}
public void setMediaButtonReceiver(PendingIntent pendingIntent) {} public void setMediaButtonReceiver(PendingIntent pendingIntent) {}
public void setActive(boolean active) {} public void setActive(boolean active) {}
public void setPlaybackState(PlaybackState state) {} public void setPlaybackState(PlaybackState state) {
String title = null;
String subTitle = null;
String artUrl = null;
if (queue != null) for (QueueItem item : queue) {
if (item.id == state.activeQueueItemId) {
title = item.description.title.toString();
subTitle = item.description.subtitle.toString();
artUrl = item.description.iconUri.toString();
break;
}
}
nativeSetState(state.state, state.actions, state.position, state.updateTime, title, subTitle, artUrl);
}
public void setMetadata(MediaMetadata metadata) {} public void setMetadata(MediaMetadata metadata) {}
public void setQueue(List<QueueItem> queue) {} public void setQueue(List<QueueItem> queue) {
this.queue = queue;
}
public void release() {} public void release() {}
protected native void nativeSetState(int state, long actions, long position, long updateTime, String title, String subTitle, String artUrl);
protected native void nativeSetCallback(Callback callback);
} }

View File

@@ -2,20 +2,49 @@ package android.media.session;
public class PlaybackState { public class PlaybackState {
public class Builder { public int state;
public long position;
public float playbackSpeed;
public long updateTime;
public long actions;
public long activeQueueItemId;
public Builder setState(int state, long position, float playbackSpeed, long updateTime) {return this;} public static class Builder {
PlaybackState state;
public Builder() {
state = new PlaybackState();
}
public Builder(PlaybackState from) {
state = from;
}
public Builder setState(int state, long position, float playbackSpeed, long updateTime) {
this.state.state = state;
this.state.position = position;
this.state.playbackSpeed = playbackSpeed;
this.state.updateTime = updateTime;
return this;
}
public Builder setBufferedPosition(long bufferedPosition) {return this;} public Builder setBufferedPosition(long bufferedPosition) {return this;}
public Builder setActions(long actions) {return this;} public Builder setActions(long actions) {
state.actions = actions;
return this;
}
public Builder setErrorMessage(CharSequence errorMessage) {return this;} public Builder setErrorMessage(CharSequence errorMessage) {return this;}
public Builder setActiveQueueItemId(long activeQueueItemId) {return this;} public Builder setActiveQueueItemId(long activeQueueItemId) {
state.activeQueueItemId = activeQueueItemId;
return this;
}
public PlaybackState build() { public PlaybackState build() {
return new PlaybackState(); return state;
} }
} }