diff --git a/meson.build b/meson.build index 9d3d5bd0..1111662e 100644 --- a/meson.build +++ b/meson.build @@ -47,6 +47,10 @@ linux_dmabuf = wl_mod.scan_xml(xml) xml = wl_mod.find_protocol('viewporter') 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_so = shared_library('android', [ '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/location/android_location_LocationManager.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/sensors/android_hardware_SensorManager.c', 'src/api-impl-jni/util.c', @@ -131,6 +136,7 @@ libtranslationlayer_so = shared_library('translation_layer_main', [ 'src/sk_area/sk_area.c', linux_dmabuf, viewporter, + mpris, ] + marshal_files, include_directories: ['src/sk_area/'], install: true, diff --git a/src/api-impl-jni/generated_headers/android_media_session_MediaSession.h b/src/api-impl-jni/generated_headers/android_media_session_MediaSession.h new file mode 100644 index 00000000..8acb067a --- /dev/null +++ b/src/api-impl-jni/generated_headers/android_media_session_MediaSession.h @@ -0,0 +1,29 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* 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 diff --git a/src/api-impl-jni/media/android_media_session_MediaSession.c b/src/api-impl-jni/media/android_media_session_MediaSession.c new file mode 100644 index 00000000..2ed5a568 --- /dev/null +++ b/src/api-impl-jni/media/android_media_session_MediaSession.c @@ -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); +} diff --git a/src/api-impl-jni/media/org.mpris.MediaPlayer2.xml b/src/api-impl-jni/media/org.mpris.MediaPlayer2.xml new file mode 100644 index 00000000..b977f2a6 --- /dev/null +++ b/src/api-impl-jni/media/org.mpris.MediaPlayer2.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/api-impl/android/app/Notification.java b/src/api-impl/android/app/Notification.java index b1ea8c73..c9a0e544 100644 --- a/src/api-impl/android/app/Notification.java +++ b/src/api-impl/android/app/Notification.java @@ -54,6 +54,7 @@ public class Notification implements Parcelable { PendingIntent intent; String iconPath; boolean ongoing; + Style style; public String toString() { return "Notification [" + title + ", " + text + ", " + actions + "]"; @@ -151,6 +152,7 @@ public class Notification implements Parcelable { } public Builder setStyle(Style style) { + notification.style = style; if (style instanceof MediaStyle) { notification.ongoing = true; } diff --git a/src/api-impl/android/app/NotificationManager.java b/src/api-impl/android/app/NotificationManager.java index 12234625..7d128179 100644 --- a/src/api-impl/android/app/NotificationManager.java +++ b/src/api-impl/android/app/NotificationManager.java @@ -1,5 +1,6 @@ package android.app; +import android.app.Notification.MediaStyle; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -10,6 +11,10 @@ public class NotificationManager { public void cancelAll() {} 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"); long builder = nativeInitBuilder(); for (Notification.Action action : notification.actions) { diff --git a/src/api-impl/android/media/MediaDescription.java b/src/api-impl/android/media/MediaDescription.java index 08af87fc..0a2c168f 100644 --- a/src/api-impl/android/media/MediaDescription.java +++ b/src/api-impl/android/media/MediaDescription.java @@ -6,24 +6,39 @@ import android.os.Bundle; public class MediaDescription { + public Uri iconUri; + public CharSequence title; + public CharSequence subtitle; + public static class Builder { + MediaDescription description = new MediaDescription(); + 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 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 MediaDescription build() { - return new MediaDescription(); + return description; } } diff --git a/src/api-impl/android/media/session/MediaSession.java b/src/api-impl/android/media/session/MediaSession.java index 660d786d..7f616710 100644 --- a/src/api-impl/android/media/session/MediaSession.java +++ b/src/api-impl/android/media/session/MediaSession.java @@ -10,12 +10,20 @@ import android.os.Handler; public class MediaSession { + private List queue; + public static final class Token {} public static abstract class Callback {} 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) {} @@ -26,19 +34,41 @@ public class MediaSession { 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 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 setQueue(List queue) {} + public void setQueue(List queue) { + this.queue = queue; + } 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); } diff --git a/src/api-impl/android/media/session/PlaybackState.java b/src/api-impl/android/media/session/PlaybackState.java index 7f0b426b..a269585d 100644 --- a/src/api-impl/android/media/session/PlaybackState.java +++ b/src/api-impl/android/media/session/PlaybackState.java @@ -2,20 +2,49 @@ package android.media.session; 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 setActions(long actions) {return this;} + public Builder setActions(long actions) { + state.actions = actions; + 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() { - return new PlaybackState(); + return state; } }