/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.media; import android.app.ActivityManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.media.session.MediaController; import android.media.session.MediaSession; import android.media.session.MediaSessionLegacyHelper; import android.media.session.MediaSessionManager; import android.media.session.PlaybackState; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.UserHandle; import android.util.DisplayMetrics; import android.util.Log; import android.view.KeyEvent; import java.lang.ref.WeakReference; import java.util.List; /** * The RemoteController class is used to control media playback, display and update media metadata * and playback status, published by applications using the {@link RemoteControlClient} class. *

* A RemoteController shall be registered through * {@link AudioManager#registerRemoteController(RemoteController)} in order for the system to send * media event updates to the {@link OnClientUpdateListener} listener set in the class constructor. * Implement the methods of the interface to receive the information published by the active * {@link RemoteControlClient} instances. *
By default an {@link OnClientUpdateListener} implementation will not receive bitmaps for * album art. Use {@link #setArtworkConfiguration(int, int)} to receive images as well. *

* Registration requires the {@link OnClientUpdateListener} listener to be one of the enabled * notification listeners (see {@link android.service.notification.NotificationListenerService}). * * @deprecated Use {@link MediaController} instead. */ @Deprecated public final class RemoteController { private final static int MAX_BITMAP_DIMENSION = 512; private final static String TAG = "RemoteController"; private final static boolean DEBUG = false; private final static Object mInfoLock = new Object(); private final Context mContext; private final int mMaxBitmapDimension; private MetadataEditor mMetadataEditor; private MediaSessionManager mSessionManager; private MediaSessionManager.OnActiveSessionsChangedListener mSessionListener; private MediaController.Callback mSessionCb = new MediaControllerCallback(); /** * Synchronized on mInfoLock */ private boolean mIsRegistered = false; private OnClientUpdateListener mOnClientUpdateListener; private PlaybackInfo mLastPlaybackInfo; private int mArtworkWidth = -1; private int mArtworkHeight = -1; private boolean mEnabled = true; // synchronized on mInfoLock, for USE_SESSION apis. private MediaController mCurrentSession; /** * Class constructor. * @param context the {@link Context}, must be non-null. * @param updateListener the listener to be called whenever new client information is available, * must be non-null. * @throws IllegalArgumentException */ public RemoteController(Context context, OnClientUpdateListener updateListener) throws IllegalArgumentException { this(context, updateListener, null); } /** * Class constructor. * @param context the {@link Context}, must be non-null. * @param updateListener the listener to be called whenever new client information is available, * must be non-null. * @param looper the {@link Looper} on which to run the event loop, * or null to use the current thread's looper. * @throws java.lang.IllegalArgumentException */ public RemoteController(Context context, OnClientUpdateListener updateListener, Looper looper) throws IllegalArgumentException { if (context == null) { throw new IllegalArgumentException("Invalid null Context"); } if (updateListener == null) { throw new IllegalArgumentException("Invalid null OnClientUpdateListener"); } if (looper != null) { mEventHandler = new EventHandler(this, looper); } else { Looper l = Looper.myLooper(); if (l != null) { mEventHandler = new EventHandler(this, l); } else { throw new IllegalArgumentException("Calling thread not associated with a looper"); } } mOnClientUpdateListener = updateListener; mContext = context; mSessionManager = (MediaSessionManager) context .getSystemService(Context.MEDIA_SESSION_SERVICE); mSessionListener = new TopTransportSessionListener(); if (ActivityManager.isLowRamDeviceStatic()) { mMaxBitmapDimension = MAX_BITMAP_DIMENSION; } else { final DisplayMetrics dm = context.getResources().getDisplayMetrics(); mMaxBitmapDimension = Math.max(dm.widthPixels, dm.heightPixels); } } /** * Interface definition for the callbacks to be invoked whenever media events, metadata * and playback status are available. */ public interface OnClientUpdateListener { /** * Called whenever all information, previously received through the other * methods of the listener, is no longer valid and is about to be refreshed. * This is typically called whenever a new {@link RemoteControlClient} has been selected * by the system to have its media information published. * @param clearing true if there is no selected RemoteControlClient and no information * is available. */ public void onClientChange(boolean clearing); /** * Called whenever the playback state has changed. * It is called when no information is known about the playback progress in the media and * the playback speed. * @param state one of the playback states authorized * in {@link RemoteControlClient#setPlaybackState(int)}. */ public void onClientPlaybackStateUpdate(int state); /** * Called whenever the playback state has changed, and playback position * and speed are known. * @param state one of the playback states authorized * in {@link RemoteControlClient#setPlaybackState(int)}. * @param stateChangeTimeMs the system time at which the state change was reported, * expressed in ms. Based on {@link android.os.SystemClock#elapsedRealtime()}. * @param currentPosMs a positive value for the current media playback position expressed * in ms, a negative value if the position is temporarily unknown. * @param speed a value expressed as a ratio of 1x playback: 1.0f is normal playback, * 2.0f is 2x, 0.5f is half-speed, -2.0f is rewind at 2x speed. 0.0f means nothing is * playing (e.g. when state is {@link RemoteControlClient#PLAYSTATE_ERROR}). */ public void onClientPlaybackStateUpdate(int state, long stateChangeTimeMs, long currentPosMs, float speed); /** * Called whenever the transport control flags have changed. * @param transportControlFlags one of the flags authorized * in {@link RemoteControlClient#setTransportControlFlags(int)}. */ public void onClientTransportControlUpdate(int transportControlFlags); /** * Called whenever new metadata is available. * See the {@link MediaMetadataEditor#putLong(int, long)}, * {@link MediaMetadataEditor#putString(int, String)}, * {@link MediaMetadataEditor#putBitmap(int, Bitmap)}, and * {@link MediaMetadataEditor#putObject(int, Object)} methods for the various keys that * can be queried. * @param metadataEditor the container of the new metadata. */ public void onClientMetadataUpdate(MetadataEditor metadataEditor); }; /** * Return the estimated playback position of the current media track or a negative value * if not available. * *

The value returned is estimated by the current process and may not be perfect. * The time returned by this method is calculated from the last state change time based * on the current play position at that time and the last known playback speed. * An application may call {@link #setSynchronizationMode(int)} to apply * a synchronization policy that will periodically re-sync the estimated position * with the RemoteControlClient.

* * @return the current estimated playback position in milliseconds or a negative value * if not available * * @see OnClientUpdateListener#onClientPlaybackStateUpdate(int, long, long, float) */ public long getEstimatedMediaPosition() { synchronized (mInfoLock) { if (mCurrentSession != null) { PlaybackState state = mCurrentSession.getPlaybackState(); if (state != null) { return state.getPosition(); } } } return -1; } /** * Send a simulated key event for a media button to be received by the current client. * To simulate a key press, you must first send a KeyEvent built with * a {@link KeyEvent#ACTION_DOWN} action, then another event with the {@link KeyEvent#ACTION_UP} * action. *

The key event will be sent to the registered receiver * (see {@link AudioManager#registerMediaButtonEventReceiver(PendingIntent)}) whose associated * {@link RemoteControlClient}'s metadata and playback state is published (there may be * none under some circumstances). * @param keyEvent a {@link KeyEvent} instance whose key code is one of * {@link KeyEvent#KEYCODE_MUTE}, * {@link KeyEvent#KEYCODE_HEADSETHOOK}, * {@link KeyEvent#KEYCODE_MEDIA_PLAY}, * {@link KeyEvent#KEYCODE_MEDIA_PAUSE}, * {@link KeyEvent#KEYCODE_MEDIA_PLAY_PAUSE}, * {@link KeyEvent#KEYCODE_MEDIA_STOP}, * {@link KeyEvent#KEYCODE_MEDIA_NEXT}, * {@link KeyEvent#KEYCODE_MEDIA_PREVIOUS}, * {@link KeyEvent#KEYCODE_MEDIA_REWIND}, * {@link KeyEvent#KEYCODE_MEDIA_RECORD}, * {@link KeyEvent#KEYCODE_MEDIA_FAST_FORWARD}, * {@link KeyEvent#KEYCODE_MEDIA_CLOSE}, * {@link KeyEvent#KEYCODE_MEDIA_EJECT}, * or {@link KeyEvent#KEYCODE_MEDIA_AUDIO_TRACK}. * @return true if the event was successfully sent, false otherwise. * @throws IllegalArgumentException */ public boolean sendMediaKeyEvent(KeyEvent keyEvent) throws IllegalArgumentException { if (!KeyEvent.isMediaKey(keyEvent.getKeyCode())) { throw new IllegalArgumentException("not a media key event"); } synchronized (mInfoLock) { if (mCurrentSession != null) { return mCurrentSession.dispatchMediaButtonEvent(keyEvent); } return false; } } /** * Sets the new playback position. * This method can only be called on a registered RemoteController. * @param timeMs a 0 or positive value for the new playback position, expressed in ms. * @return true if the command to set the playback position was successfully sent. * @throws IllegalArgumentException */ public boolean seekTo(long timeMs) throws IllegalArgumentException { if (!mEnabled) { Log.e(TAG, "Cannot use seekTo() from a disabled RemoteController"); return false; } if (timeMs < 0) { throw new IllegalArgumentException("illegal negative time value"); } synchronized (mInfoLock) { if (mCurrentSession != null) { mCurrentSession.getTransportControls().seekTo(timeMs); } } return true; } /** * @hide * @param wantBitmap * @param width * @param height * @return true if successful * @throws IllegalArgumentException */ public boolean setArtworkConfiguration(boolean wantBitmap, int width, int height) throws IllegalArgumentException { synchronized (mInfoLock) { if (wantBitmap) { if ((width > 0) && (height > 0)) { if (width > mMaxBitmapDimension) { width = mMaxBitmapDimension; } if (height > mMaxBitmapDimension) { height = mMaxBitmapDimension; } mArtworkWidth = width; mArtworkHeight = height; } else { throw new IllegalArgumentException("Invalid dimensions"); } } else { mArtworkWidth = -1; mArtworkHeight = -1; } } return true; } /** * Set the maximum artwork image dimensions to be received in the metadata. * No bitmaps will be received unless this has been specified. * @param width the maximum width in pixels * @param height the maximum height in pixels * @return true if the artwork dimension was successfully set. * @throws IllegalArgumentException */ public boolean setArtworkConfiguration(int width, int height) throws IllegalArgumentException { return setArtworkConfiguration(true, width, height); } /** * Prevents this RemoteController from receiving artwork images. * @return true if receiving artwork images was successfully disabled. */ public boolean clearArtworkConfiguration() { return setArtworkConfiguration(false, -1, -1); } /** * Default playback position synchronization mode where the RemoteControlClient is not * asked regularly for its playback position to see if it has drifted from the estimated * position. */ public static final int POSITION_SYNCHRONIZATION_NONE = 0; /** * The playback position synchronization mode where the RemoteControlClient instances which * expose their playback position to the framework, will be regularly polled to check * whether any drift has been noticed between their estimated position and the one they report. * Note that this mode should only ever be used when needing to display very accurate playback * position, as regularly polling a RemoteControlClient for its position may have an impact * on battery life (if applicable) when this query will trigger network transactions in the * case of remote playback. */ public static final int POSITION_SYNCHRONIZATION_CHECK = 1; /** * Set the playback position synchronization mode. * Must be called on a registered RemoteController. * @param sync {@link #POSITION_SYNCHRONIZATION_NONE} or {@link #POSITION_SYNCHRONIZATION_CHECK} * @return true if the synchronization mode was successfully set. * @throws IllegalArgumentException */ public boolean setSynchronizationMode(int sync) throws IllegalArgumentException { if ((sync != POSITION_SYNCHRONIZATION_NONE) && (sync != POSITION_SYNCHRONIZATION_CHECK)) { throw new IllegalArgumentException("Unknown synchronization mode " + sync); } if (!mIsRegistered) { Log.e(TAG, "Cannot set synchronization mode on an unregistered RemoteController"); return false; } // deprecated, no-op return true; } /** * Creates a {@link MetadataEditor} for updating metadata values of the editable keys of * the current {@link RemoteControlClient}. * This method can only be called on a registered RemoteController. * @return a new MetadataEditor instance. */ public MetadataEditor editMetadata() { MetadataEditor editor = new MetadataEditor(); editor.mEditorMetadata = new Bundle(); editor.mEditorArtwork = null; editor.mMetadataChanged = true; editor.mArtworkChanged = true; editor.mEditableKeys = 0; return editor; } /** * A class to read the metadata published by a {@link RemoteControlClient}, or send a * {@link RemoteControlClient} new values for keys that can be edited. */ public class MetadataEditor extends MediaMetadataEditor { /** * @hide */ protected MetadataEditor() { } /** * @hide */ protected MetadataEditor(Bundle metadata, long editableKeys) { mEditorMetadata = metadata; mEditableKeys = editableKeys; mEditorArtwork = (Bitmap) metadata.getParcelable( String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK)); if (mEditorArtwork != null) { cleanupBitmapFromBundle(MediaMetadataEditor.BITMAP_KEY_ARTWORK); } mMetadataChanged = true; mArtworkChanged = true; mApplied = false; } private void cleanupBitmapFromBundle(int key) { if (METADATA_KEYS_TYPE.get(key, METADATA_TYPE_INVALID) == METADATA_TYPE_BITMAP) { mEditorMetadata.remove(String.valueOf(key)); } } /** * Applies all of the metadata changes that have been set since the MediaMetadataEditor * instance was created with {@link RemoteController#editMetadata()} * or since {@link #clear()} was called. */ public synchronized void apply() { // "applying" a metadata bundle in RemoteController is only for sending edited // key values back to the RemoteControlClient, so here we only care about the only // editable key we support: RATING_KEY_BY_USER if (!mMetadataChanged) { return; } synchronized (mInfoLock) { if (mCurrentSession != null) { if (mEditorMetadata.containsKey( String.valueOf(MediaMetadataEditor.RATING_KEY_BY_USER))) { Rating rating = (Rating) getObject( MediaMetadataEditor.RATING_KEY_BY_USER, null); if (rating != null) { mCurrentSession.getTransportControls().setRating(rating); } } } } // NOT setting mApplied to true as this type of MetadataEditor will be applied // multiple times, whenever the user of a RemoteController needs to change the // metadata (e.g. user changes the rating of a song more than once during playback) mApplied = false; } } /** * This receives updates when the current session changes. This is * registered to receive the updates on the handler thread so it can call * directly into the appropriate methods. */ private class MediaControllerCallback extends MediaController.Callback { @Override public void onPlaybackStateChanged(PlaybackState state) { onNewPlaybackState(state); } @Override public void onMetadataChanged(MediaMetadata metadata) { onNewMediaMetadata(metadata); } } /** * Listens for changes to the active session stack and replaces the * currently tracked session if it has changed. */ private class TopTransportSessionListener implements MediaSessionManager.OnActiveSessionsChangedListener { @Override public void onActiveSessionsChanged(List controllers) { int size = controllers.size(); for (int i = 0; i < size; i++) { MediaController controller = controllers.get(i); long flags = controller.getFlags(); // We only care about sessions that handle transport controls, // which will be true for apps using RCC if ((flags & MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS) != 0) { updateController(controller); return; } } updateController(null); } } //================================================== // Event handling private final EventHandler mEventHandler; private final static int MSG_CLIENT_CHANGE = 0; private final static int MSG_NEW_PLAYBACK_STATE = 1; private final static int MSG_NEW_MEDIA_METADATA = 2; private class EventHandler extends Handler { public EventHandler(RemoteController rc, Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { switch(msg.what) { case MSG_CLIENT_CHANGE: onClientChange(msg.arg2 == 1); break; case MSG_NEW_PLAYBACK_STATE: onNewPlaybackState((PlaybackState) msg.obj); break; case MSG_NEW_MEDIA_METADATA: onNewMediaMetadata((MediaMetadata) msg.obj); break; default: Log.e(TAG, "unknown event " + msg.what); } } } /** * @hide */ void startListeningToSessions() { final ComponentName listenerComponent = new ComponentName(mContext, mOnClientUpdateListener.getClass()); Handler handler = null; if (Looper.myLooper() == null) { handler = new Handler(Looper.getMainLooper()); } mSessionManager.addOnActiveSessionsChangedListener(mSessionListener, listenerComponent, UserHandle.myUserId(), handler); mSessionListener.onActiveSessionsChanged(mSessionManager .getActiveSessions(listenerComponent)); if (DEBUG) { Log.d(TAG, "Registered session listener with component " + listenerComponent + " for user " + UserHandle.myUserId()); } } /** * @hide */ void stopListeningToSessions() { mSessionManager.removeOnActiveSessionsChangedListener(mSessionListener); if (DEBUG) { Log.d(TAG, "Unregistered session listener for user " + UserHandle.myUserId()); } } /** If the msg is already queued, replace it with this one. */ private static final int SENDMSG_REPLACE = 0; /** If the msg is already queued, ignore this one and leave the old. */ private static final int SENDMSG_NOOP = 1; /** If the msg is already queued, queue this one and leave the old. */ private static final int SENDMSG_QUEUE = 2; private static void sendMsg(Handler handler, int msg, int existingMsgPolicy, int arg1, int arg2, Object obj, int delayMs) { if (handler == null) { Log.e(TAG, "null event handler, will not deliver message " + msg); return; } if (existingMsgPolicy == SENDMSG_REPLACE) { handler.removeMessages(msg); } else if (existingMsgPolicy == SENDMSG_NOOP && handler.hasMessages(msg)) { return; } handler.sendMessageDelayed(handler.obtainMessage(msg, arg1, arg2, obj), delayMs); } private void onClientChange(boolean clearing) { final OnClientUpdateListener l; synchronized(mInfoLock) { l = mOnClientUpdateListener; mMetadataEditor = null; } if (l != null) { l.onClientChange(clearing); } } private void updateController(MediaController controller) { if (DEBUG) { Log.d(TAG, "Updating controller to " + controller + " previous controller is " + mCurrentSession); } synchronized (mInfoLock) { if (controller == null) { if (mCurrentSession != null) { mCurrentSession.unregisterCallback(mSessionCb); mCurrentSession = null; sendMsg(mEventHandler, MSG_CLIENT_CHANGE, SENDMSG_REPLACE, 0 /* arg1 ignored */, 1 /* clearing */, null /* obj */, 0 /* delay */); } } else if (mCurrentSession == null || !controller.getSessionToken() .equals(mCurrentSession.getSessionToken())) { if (mCurrentSession != null) { mCurrentSession.unregisterCallback(mSessionCb); } sendMsg(mEventHandler, MSG_CLIENT_CHANGE, SENDMSG_REPLACE, 0 /* arg1 ignored */, 0 /* clearing */, null /* obj */, 0 /* delay */); mCurrentSession = controller; mCurrentSession.registerCallback(mSessionCb, mEventHandler); PlaybackState state = controller.getPlaybackState(); sendMsg(mEventHandler, MSG_NEW_PLAYBACK_STATE, SENDMSG_REPLACE, 0 /* arg1 ignored */, 0 /* arg2 ignored */, state /* obj */, 0 /* delay */); MediaMetadata metadata = controller.getMetadata(); sendMsg(mEventHandler, MSG_NEW_MEDIA_METADATA, SENDMSG_REPLACE, 0 /* arg1 ignored */, 0 /* arg2 ignored*/, metadata /* obj */, 0 /*delay*/); } // else same controller, no need to update } } private void onNewPlaybackState(PlaybackState state) { final OnClientUpdateListener l; synchronized (mInfoLock) { l = this.mOnClientUpdateListener; } if (l != null) { int playstate = state == null ? RemoteControlClient.PLAYSTATE_NONE : PlaybackState .getRccStateFromState(state.getState()); if (state == null || state.getPosition() == PlaybackState.PLAYBACK_POSITION_UNKNOWN) { l.onClientPlaybackStateUpdate(playstate); } else { l.onClientPlaybackStateUpdate(playstate, state.getLastPositionUpdateTime(), state.getPosition(), state.getPlaybackSpeed()); } if (state != null) { l.onClientTransportControlUpdate( PlaybackState.getRccControlFlagsFromActions(state.getActions())); } } } private void onNewMediaMetadata(MediaMetadata metadata) { if (metadata == null) { // RemoteController only handles non-null metadata return; } final OnClientUpdateListener l; final MetadataEditor metadataEditor; // prepare the received Bundle to be used inside a MetadataEditor synchronized(mInfoLock) { l = mOnClientUpdateListener; boolean canRate = mCurrentSession != null && mCurrentSession.getRatingType() != Rating.RATING_NONE; long editableKeys = canRate ? MediaMetadataEditor.RATING_KEY_BY_USER : 0; Bundle legacyMetadata = MediaSessionLegacyHelper.getOldMetadata(metadata, mArtworkWidth, mArtworkHeight); mMetadataEditor = new MetadataEditor(legacyMetadata, editableKeys); metadataEditor = mMetadataEditor; } if (l != null) { l.onClientMetadataUpdate(metadataEditor); } } //================================================== private static class PlaybackInfo { int mState; long mStateChangeTimeMs; long mCurrentPosMs; float mSpeed; PlaybackInfo(int state, long stateChangeTimeMs, long currentPosMs, float speed) { mState = state; mStateChangeTimeMs = stateChangeTimeMs; mCurrentPosMs = currentPosMs; mSpeed = speed; } } /** * @hide * Used by AudioManager to access user listener receiving the client update notifications * @return */ OnClientUpdateListener getUpdateListener() { return mOnClientUpdateListener; } }