From d0eb577079f2de81f49c74c0b16ce69952458c6e Mon Sep 17 00:00:00 2001 From: Enno Boland Date: Fri, 4 Oct 2024 17:42:05 +0200 Subject: [PATCH] hook up device events --- app/src/control_msg.h | 16 ++ app/src/device_msg.c | 66 +++++-- app/src/device_msg.h | 10 + app/src/receiver.c | 22 +++ .../java/com/genymobile/scrcpy/Options.java | 5 + .../java/com/genymobile/scrcpy/Server.java | 42 ++++ .../scrcpy/control/ControlMessage.java | 33 ++++ .../scrcpy/control/ControlMessageReader.java | 16 ++ .../genymobile/scrcpy/control/Controller.java | 13 ++ .../scrcpy/control/DeviceMessage.java | 27 +++ .../scrcpy/control/DeviceMessageWriter.java | 12 +- .../scrcpy/wrappers/MediaManager.java | 186 ++++++++++++++++++ .../scrcpy/wrappers/ServiceManager.java | 8 + 13 files changed, 438 insertions(+), 18 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/wrappers/MediaManager.java diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 74dbcba8..72a9cee0 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -43,6 +43,14 @@ enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, SC_CONTROL_MSG_TYPE_START_APP, SC_CONTROL_MSG_TYPE_RESET_VIDEO, + SC_CONTROL_MSG_TYPE_MEDIA_STATE, + SC_CONTROL_MSG_TYPE_MEDIA_SEEK, +}; + +enum sc_screen_power_mode { + // see + SC_SCREEN_POWER_MODE_OFF = 0, + SC_SCREEN_POWER_MODE_NORMAL = 2, }; enum sc_copy_key { @@ -111,6 +119,14 @@ struct sc_control_msg { struct { char *name; } start_app; + struct { + uint16_t player_id; + uint64_t position; + } media_seek; + struct { + uint16_t player_id; + uint8_t state; + } media_state; }; }; diff --git a/app/src/device_msg.c b/app/src/device_msg.c index 7621c040..06b42ddc 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -7,6 +7,20 @@ #include "util/binary.h" #include "util/log.h" +static int read_message(uint8_t **target, const uint8_t *src, const uint16_t size) { + uint8_t *data = malloc(size + 1); + if (!data) { + LOG_OOM(); + return -1; + } + if (size) { + data[size] = '\0'; + memcpy(data, src, size); + } + *target = data; + return 0; +} + ssize_t sc_device_msg_deserialize(const uint8_t *buf, size_t len, struct sc_device_msg *msg) { @@ -25,17 +39,10 @@ sc_device_msg_deserialize(const uint8_t *buf, size_t len, if (clipboard_len > len - 5) { return 0; // no complete message } - char *text = malloc(clipboard_len + 1); - if (!text) { - LOG_OOM(); + if (read_message((uint8_t **)&msg->clipboard.text, &buf[5], clipboard_len) == -1) { return -1; } - if (clipboard_len) { - memcpy(text, &buf[5], clipboard_len); - } - text[clipboard_len] = '\0'; - msg->clipboard.text = text; return 5 + clipboard_len; } case DEVICE_MSG_TYPE_ACK_CLIPBOARD: { @@ -56,20 +63,42 @@ sc_device_msg_deserialize(const uint8_t *buf, size_t len, if (size < len - 5) { return 0; // not available } - uint8_t *data = malloc(size); - if (!data) { - LOG_OOM(); - return -1; - } - if (size) { - memcpy(data, &buf[5], size); - } msg->uhid_output.id = id; msg->uhid_output.size = size; - msg->uhid_output.data = data; + if (read_message(&msg->uhid_output.data, &buf[5], size) == -1) { + return -1; + } return 5 + size; + case DEVICE_MSG_TYPE_MEDIA_UPDATE: { + if (len < 5) { + // at least id + size + return 0; // not available + } + uint16_t id = sc_read16be(&buf[1]); + size_t size = sc_read16be(&buf[3]); + if (size < len - 5) { + return 0; // not available + } + + msg->media_update.id = id; + msg->media_update.size = size; + if (read_message(&msg->media_update.data, &buf[5], size) == -1) { + return -1; + } + + return 5 + size; + } + case DEVICE_MSG_TYPE_MEDIA_REMOVE: { + if (len < 3) { + // at least id + return 0; // not available + } + uint16_t id = sc_read16be(&buf[1]); + msg->media_remove.id = id; + return 3; + } } default: LOGW("Unknown device message type: %d", (int) msg->type); @@ -86,6 +115,9 @@ sc_device_msg_destroy(struct sc_device_msg *msg) { case DEVICE_MSG_TYPE_UHID_OUTPUT: free(msg->uhid_output.data); break; + case DEVICE_MSG_TYPE_MEDIA_UPDATE: + free(msg->media_update.data); + break; default: // nothing to do break; diff --git a/app/src/device_msg.h b/app/src/device_msg.h index d6c701bb..ea2797d1 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -15,6 +15,8 @@ enum sc_device_msg_type { DEVICE_MSG_TYPE_CLIPBOARD, DEVICE_MSG_TYPE_ACK_CLIPBOARD, DEVICE_MSG_TYPE_UHID_OUTPUT, + DEVICE_MSG_TYPE_MEDIA_UPDATE, + DEVICE_MSG_TYPE_MEDIA_REMOVE, }; struct sc_device_msg { @@ -31,6 +33,14 @@ struct sc_device_msg { uint16_t size; uint8_t *data; // owned, to be freed by free() } uhid_output; + struct { + uint16_t id; + uint16_t size; + uint8_t *data; // owned, to be freed by free() + } media_update; + struct { + uint16_t id; + } media_remove; }; }; diff --git a/app/src/receiver.c b/app/src/receiver.c index 2ccb8a8b..c6366901 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -73,6 +73,22 @@ task_uhid_output(void *userdata) { free(data); } +static void +dump_media_update(const struct sc_device_msg* msg) { + uint8_t msg_type = 0; + uint8_t *msg_ptr = NULL; + for (int i = 0; i < msg->media_update.size; i++) { + if (msg_ptr == NULL) { + msg_type = msg->media_update.data[i]; + msg_ptr = &msg->media_update.data[i + 1]; + } else if (msg->media_update.data[i] == 0) { + LOGI("Media update: %i, %s", (int)msg_type, msg_ptr); + msg_ptr = NULL; + msg_type = 0; + } + } +} + static void process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { switch (msg->type) { @@ -149,6 +165,12 @@ process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { return; } + break; + case DEVICE_MSG_TYPE_MEDIA_UPDATE: + dump_media_update(msg); + break; + case DEVICE_MSG_TYPE_MEDIA_REMOVE: + LOGI("Media remove: %i", msg->media_remove.id); break; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 66bb68e8..a81b1b63 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -26,6 +26,7 @@ public class Options { private int scid = -1; // 31-bit non-negative value, or -1 private boolean video = true; private boolean audio = true; + private boolean mediaControl = true; private int maxSize; private VideoCodec videoCodec = VideoCodec.H264; private AudioCodec audioCodec = AudioCodec.OPUS; @@ -96,6 +97,10 @@ public class Options { return audio; } + public boolean getMediaControls() { + return mediaControl; + } + public int getMaxSize() { return maxSize; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index a08c948c..c2cd45d7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -23,8 +23,12 @@ import com.genymobile.scrcpy.video.ScreenCapture; import com.genymobile.scrcpy.video.SurfaceCapture; import com.genymobile.scrcpy.video.SurfaceEncoder; import com.genymobile.scrcpy.video.VideoSource; +import com.genymobile.scrcpy.wrappers.MediaManager; import android.annotation.SuppressLint; +import android.media.MediaMetadata; +import android.media.session.PlaybackState; +import android.os.BatteryManager; import android.os.Build; import android.os.Looper; @@ -95,6 +99,7 @@ public final class Server { boolean control = options.getControl(); boolean video = options.getVideo(); boolean audio = options.getAudio(); + boolean media = options.getMediaControls(); boolean sendDummyByte = options.getSendDummyByte(); Workarounds.apply(); @@ -113,6 +118,40 @@ public final class Server { ControlChannel controlChannel = connection.getControlChannel(); controller = new Controller(controlChannel, cleanUp, options); asyncProcessors.add(controller); + + if (media) { + MediaManager mediaManager = MediaManager.create(); + + mediaManager.setMediaChangeListener(new MediaManager.MediaChange() { + @Override + public void onMetadataChange(int id, MediaMetadata metadata) { + Ln.i("onMetadataChange " + id); + byte[] data = MediaManager.mediaMetadataSerialize(metadata); + DeviceMessage msg = DeviceMessage.createMediaUpdate(id, data); + controller.getSender().send(msg); + } + + @Override + public void onPlaybackStateChange(int id, PlaybackState playbackState) { + Ln.i("onPlaybackStateChange " + id); + int state = MediaManager.create().playbackStateSerialize(playbackState); + if(state < 0) { + return; + } + DeviceMessage msg = DeviceMessage.createMediaState(id, state); + controller.getSender().send(msg); + } + + @Override + public void onRemove(int id) { + Ln.i("onRemove " + id); + DeviceMessage msg = DeviceMessage.createMediaRemove(id); + controller.getSender().send(msg); + } + }); + + mediaManager.start(); + } } if (audio) { @@ -158,6 +197,9 @@ public final class Server { } } + + + Completion completion = new Completion(asyncProcessors.size()); for (AsyncProcessor asyncProcessor : asyncProcessors) { asyncProcessor.start((fatalError) -> { diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java index 0eb96adc..83e21e20 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -25,6 +25,8 @@ public final class ControlMessage { public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15; public static final int TYPE_START_APP = 16; public static final int TYPE_RESET_VIDEO = 17; + public static final int TYPE_MEDIA_STATE = 18; + public static final int TYPE_MEDIA_SEEK = 19; public static final long SEQUENCE_INVALID = 0; @@ -50,9 +52,15 @@ public final class ControlMessage { private long sequence; private int id; private byte[] data; +<<<<<<< HEAD private boolean on; private int vendorId; private int productId; +======= + private int mediaState; + private long mediaSeek; + +>>>>>>> 38b99700 (hook up device events) private ControlMessage() { } @@ -159,10 +167,26 @@ public final class ControlMessage { return msg; } +<<<<<<< HEAD public static ControlMessage createStartApp(String name) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_START_APP; msg.text = name; +======= + public static ControlMessage createMediaState(int receiverId, byte state) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_MEDIA_STATE; + msg.id = receiverId; + msg.mediaState = state; + return msg; + } + + public static ControlMessage createMediaSeek(int receiverId, long position) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_MEDIA_STATE; + msg.id = receiverId; + msg.mediaSeek = position; +>>>>>>> 38b99700 (hook up device events) return msg; } @@ -238,6 +262,7 @@ public final class ControlMessage { return data; } +<<<<<<< HEAD public boolean getOn() { return on; } @@ -248,5 +273,13 @@ public final class ControlMessage { public int getProductId() { return productId; +======= + public long getMediaSeek() { + return mediaSeek; + } + + public int getMediaState() { + return mediaState; +>>>>>>> 38b99700 (hook up device events) } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index 830a7ec7..f171e081 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -56,11 +56,27 @@ public class ControlMessageReader { return parseUhidDestroy(); case ControlMessage.TYPE_START_APP: return parseStartApp(); + case ControlMessage.TYPE_MEDIA_STATE: + return parseMediaPlayStateRequest(); + case ControlMessage.TYPE_MEDIA_SEEK: + return parseMediaSeekRequest(); default: throw new ControlProtocolException("Unknown event type: " + type); } } + private ControlMessage parseMediaSeekRequest() throws IOException { + int receiverId = dis.readUnsignedShort(); + long position = dis.readLong(); + return ControlMessage.createMediaSeek(receiverId, position); + } + + private ControlMessage parseMediaPlayStateRequest() throws IOException { + int receiverId = dis.readUnsignedShort(); + byte state = dis.readByte(); + return ControlMessage.createMediaState(receiverId, state); + } + private ControlMessage parseInjectKeycode() throws IOException { int action = dis.readUnsignedByte(); int keycode = dis.readInt(); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index b4a8e3ca..f53517e7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -330,6 +330,11 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { break; case ControlMessage.TYPE_RESET_VIDEO: resetVideo(); + case ControlMessage.TYPE_MEDIA_STATE: + mediaUpdateState(msg.getId(), msg.getMediaState()); + break; + case ControlMessage.TYPE_MEDIA_SEEK: + mediaSeek(msg.getId(), msg.getMediaSeek()); break; default: // do nothing @@ -338,6 +343,14 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { return true; } + private void mediaUpdateState(int id, int mediaState) { + // TODO + } + + private void mediaSeek(int id, long mediaSeek) { + // TODO + } + private boolean injectKeycode(int action, int keycode, int repeat, int metaState) { if (keepDisplayPowerOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { assert displayId != Device.DISPLAY_ID_NONE; diff --git a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java index 079a7a04..8994a788 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java @@ -5,12 +5,16 @@ public final class DeviceMessage { public static final int TYPE_CLIPBOARD = 0; public static final int TYPE_ACK_CLIPBOARD = 1; public static final int TYPE_UHID_OUTPUT = 2; + public static final int TYPE_MEDIA_UPDATE = 3; + public static final int TYPE_MEDIA_REMOVE = 4; + public static final int TYPE_MEDIA_STATE = 4; private int type; private String text; private long sequence; private int id; private byte[] data; + private int mediaState; private DeviceMessage() { } @@ -37,6 +41,29 @@ public final class DeviceMessage { return event; } + public static DeviceMessage createMediaUpdate(int id, byte[] data) { + DeviceMessage event = new DeviceMessage(); + event.type = TYPE_MEDIA_UPDATE; + event.id = id; + event.data = data; + return event; + } + + public static DeviceMessage createMediaState(int id, int state) { + DeviceMessage event = new DeviceMessage(); + event.type = TYPE_MEDIA_STATE; + event.id = id; + event.mediaState = state; + return event; + } + + public static DeviceMessage createMediaRemove(int id) { + DeviceMessage event = new DeviceMessage(); + event.type = TYPE_MEDIA_REMOVE; + event.id = id; + return event; + } + public int getType() { return type; } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java index a18a2e5d..6b9ebe7e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java @@ -22,6 +22,7 @@ public class DeviceMessageWriter { public void write(DeviceMessage msg) throws IOException { int type = msg.getType(); dos.writeByte(type); + byte[] data; switch (type) { case DeviceMessage.TYPE_CLIPBOARD: String text = msg.getText(); @@ -35,10 +36,19 @@ public class DeviceMessageWriter { break; case DeviceMessage.TYPE_UHID_OUTPUT: dos.writeShort(msg.getId()); - byte[] data = msg.getData(); + data = msg.getData(); dos.writeShort(data.length); dos.write(data); break; + case DeviceMessage.TYPE_MEDIA_UPDATE: + dos.writeShort(msg.getId()); + data = msg.getData(); + dos.writeShort(data.length); + dos.write(data); + break; + case DeviceMessage.TYPE_MEDIA_REMOVE: + dos.writeShort(msg.getId()); + break; default: throw new ControlProtocolException("Unknown event type: " + type); } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/MediaManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/MediaManager.java new file mode 100644 index 00000000..2efee1f4 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/MediaManager.java @@ -0,0 +1,186 @@ +package com.genymobile.scrcpy.wrappers; + +import android.media.MediaMetadata; +import android.media.session.MediaController; +import android.media.session.MediaSessionManager; +import android.media.session.PlaybackState; + +import com.genymobile.scrcpy.FakeContext; +import com.genymobile.scrcpy.util.Ln; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +public class MediaManager { + final static byte KEY_DURATION = 0; + final static byte KEY_ALBUM = 1; + final static byte KEY_ARTIST = 2; + final static byte KEY_TITLE = 3; + + final static byte STATE_PLAYING = 0; + final static byte STATE_STOPPED = 1; + final static byte STATE_PAUSED = 2; + + MediaSessionManager sessionManager; + private MediaChange mediaChangeListener; + boolean started = false; + + public interface MediaChange { + void onMetadataChange(int id, MediaMetadata metadata); + void onPlaybackStateChange(int id, PlaybackState playbackState); + void onRemove(int id); + } + public static MediaManager create() { + MediaSessionManager manager = FakeContext.get().getSystemService(MediaSessionManager.class); + return new MediaManager(manager); + } + + int nextId = 0; + HashMap idMap = new HashMap<>(); + List mediaControllers = Collections.emptyList(); + + + private MediaManager(MediaSessionManager sessionManager) { + this.sessionManager = sessionManager; + + } + + public void setMediaChangeListener(MediaChange listener) { + this.mediaChangeListener = listener; + } + + public void start() { + if (started) { + return; + } + + sessionManager.addOnActiveSessionsChangedListener(new MediaSessionManager.OnActiveSessionsChangedListener() { + @Override + public void onActiveSessionsChanged(List controllers) { + Ln.i("MediaManager: Active Sessions changed"); + if (controllers == null) { + controllers = Collections.emptyList(); + } + // add + for(MediaController controller : controllers) { + if (!mediaControllers.contains(controller)) { + addMediaController(controller); + } + } + for(MediaController controller: mediaControllers) { + if (!controllers.contains(controller)) { + removeMediaController(controller); + } + } + + mediaControllers = new ArrayList<>(controllers); + } + }, null); + + mediaControllers = sessionManager.getActiveSessions(null); + for (MediaController controller : mediaControllers) { + addMediaController(controller); + } + + started = true; + } + + public static byte[] mediaMetadataSerialize(MediaMetadata metadata) { + ArrayList payload = new ArrayList(); + for (String key : metadata.keySet()) { + byte field_id; + byte[] field_value; + switch(key) { + case MediaMetadata.METADATA_KEY_DURATION: + field_id = KEY_DURATION; + field_value = (""+metadata.getLong(key)).getBytes(); + break; + case MediaMetadata.METADATA_KEY_ALBUM: + field_id = KEY_ALBUM; + field_value = metadata.getString(key).getBytes(); + break; + case MediaMetadata.METADATA_KEY_ARTIST: + field_id = KEY_ARTIST; + field_value = metadata.getString(key).getBytes(); + break; + case MediaMetadata.METADATA_KEY_TITLE: + field_id = KEY_TITLE; + field_value = metadata.getString(key).getBytes(); + break; + default: + field_id = 0; + field_value = null; + } + + if (field_value != null) { + payload.add(field_id); + for (byte b : field_value) { + payload.add(b); + } + payload.add((byte)0); + } + + + } + byte[] result = new byte[payload.size()]; + for (int i = 0; i < payload.size(); i++) { + result[i] = payload.get(i); + } + return result; + } + + public int playbackStateSerialize(PlaybackState state) { + switch(state.getState()) { + case PlaybackState.STATE_PLAYING: + return STATE_PLAYING; + case PlaybackState.STATE_STOPPED: + return STATE_STOPPED; + case PlaybackState.STATE_PAUSED: + return STATE_PAUSED; + default: + return -1; + } + } + + private void removeMediaController(MediaController controller) { + int controllerId = findId(controller); + Ln.i("Remove MediaController ID:" + controllerId + " pkg:" + controller.getPackageName()); + mediaChangeListener.onRemove(controllerId); + } + + private int findId(MediaController controller) { + String packageName = controller.getPackageName(); + Integer id = this.idMap.get(packageName); + if (id == null) { + id = nextId; + nextId++; + this.idMap.put(packageName, id); + } + + return id; + } + + private void addMediaController(MediaController controller) { + final int controllerId = findId(controller); + Ln.i("New MediaController ID:" + controllerId + " pkg:" + controller.getPackageName()); + controller.registerCallback(new MediaController.Callback() { + @Override + public void onMetadataChanged(MediaMetadata metadata) { + super.onMetadataChanged(metadata); + Ln.i("MediaController metadata change " + controllerId); + mediaChangeListener.onMetadataChange(controllerId, metadata); + } + + @Override + public void onPlaybackStateChanged(PlaybackState state) { + super.onPlaybackStateChanged(state); + Ln.i("MediaController playstate change " + controllerId); + mediaChangeListener.onPlaybackStateChange(controllerId, state); + } + }); + mediaChangeListener.onMetadataChange(controllerId, controller.getMetadata()); + mediaChangeListener.onPlaybackStateChange(controllerId, controller.getPlaybackState()); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java index b1123b55..cf187984 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -32,6 +32,7 @@ public final class ServiceManager { private static ClipboardManager clipboardManager; private static ActivityManager activityManager; private static CameraManager cameraManager; + private static MediaManager mediaManager; private ServiceManager() { /* not instantiable */ @@ -109,4 +110,11 @@ public final class ServiceManager { } return cameraManager; } + + public static MediaManager getMediaManager() { + if (mediaManager == null) { + mediaManager = MediaManager.create(); + } + return mediaManager; + } }