| /* |
| * 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.app.PendingIntent; |
| import android.app.PendingIntent.CanceledException; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.graphics.Bitmap; |
| import android.media.IRemoteControlDisplay; |
| import android.media.MediaMetadataEditor; |
| 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.SystemClock; |
| 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. |
| * <p> |
| * 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. |
| * <br>By default an {@link OnClientUpdateListener} implementation will not receive bitmaps for |
| * album art. Use {@link #setArtworkConfiguration(int, int)} to receive images as well. |
| * <p> |
| * Registration requires the {@link OnClientUpdateListener} listener to be one of the enabled |
| * notification listeners (see {@link android.service.notification.NotificationListenerService}). |
| */ |
| public final class RemoteController |
| { |
| private final static int MAX_BITMAP_DIMENSION = 512; |
| private final static int TRANSPORT_UNKNOWN = 0; |
| private final static String TAG = "RemoteController"; |
| private final static boolean DEBUG = false; |
| private final static boolean USE_SESSIONS = true; |
| private final static Object mGenLock = new Object(); |
| private final static Object mInfoLock = new Object(); |
| private final RcDisplay mRcd; |
| private final Context mContext; |
| private final AudioManager mAudioManager; |
| private final int mMaxBitmapDimension; |
| private MetadataEditor mMetadataEditor; |
| |
| private MediaSessionManager mSessionManager; |
| private MediaSessionManager.SessionListener mSessionListener |
| = new TopTransportSessionListener(); |
| private MediaController.Callback mSessionCb = new MediaControllerCallback(); |
| |
| /** |
| * Synchronized on mGenLock |
| */ |
| private int mClientGenerationIdCurrent = 0; |
| |
| /** |
| * Synchronized on mInfoLock |
| */ |
| private boolean mIsRegistered = false; |
| private PendingIntent mClientPendingIntentCurrent; |
| 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; |
| mRcd = new RcDisplay(this); |
| mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); |
| mSessionManager = (MediaSessionManager) context |
| .getSystemService(Context.MEDIA_SESSION_SERVICE); |
| |
| 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); |
| }; |
| |
| |
| /** |
| * @hide |
| */ |
| public String getRemoteControlClientPackageName() { |
| if (USE_SESSIONS) { |
| synchronized (mInfoLock) { |
| return mCurrentSession != null ? mCurrentSession.getSessionInfo().getPackageName() |
| : null; |
| } |
| } else { |
| return mClientPendingIntentCurrent != null ? |
| mClientPendingIntentCurrent.getCreatorPackage() : null; |
| } |
| } |
| |
| /** |
| * Return the estimated playback position of the current media track or a negative value |
| * if not available. |
| * |
| * <p>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.</p> |
| * |
| * @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() { |
| if (USE_SESSIONS) { |
| synchronized (mInfoLock) { |
| if (mCurrentSession != null) { |
| PlaybackState state = mCurrentSession.getPlaybackState(); |
| if (state != null) { |
| return state.getPosition(); |
| } |
| } |
| } |
| } else { |
| final PlaybackInfo lastPlaybackInfo; |
| synchronized (mInfoLock) { |
| lastPlaybackInfo = mLastPlaybackInfo; |
| } |
| if (lastPlaybackInfo != null) { |
| if (!RemoteControlClient.playbackPositionShouldMove(lastPlaybackInfo.mState)) { |
| return lastPlaybackInfo.mCurrentPosMs; |
| } |
| |
| // Take the current position at the time of state change and |
| // estimate. |
| final long thenPos = lastPlaybackInfo.mCurrentPosMs; |
| if (thenPos < 0) { |
| return -1; |
| } |
| |
| final long now = SystemClock.elapsedRealtime(); |
| final long then = lastPlaybackInfo.mStateChangeTimeMs; |
| final long sinceThen = now - then; |
| final long scaledSinceThen = (long) (sinceThen * lastPlaybackInfo.mSpeed); |
| return thenPos + scaledSinceThen; |
| } |
| } |
| 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. |
| * <p>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"); |
| } |
| if (USE_SESSIONS) { |
| synchronized (mInfoLock) { |
| if (mCurrentSession != null) { |
| return mCurrentSession.dispatchMediaButtonEvent(keyEvent); |
| } |
| return false; |
| } |
| } else { |
| final PendingIntent pi; |
| synchronized (mInfoLock) { |
| if (!mIsRegistered) { |
| Log.e(TAG, |
| "Cannot use sendMediaKeyEvent() from an unregistered RemoteController"); |
| return false; |
| } |
| if (!mEnabled) { |
| Log.e(TAG, "Cannot use sendMediaKeyEvent() from a disabled RemoteController"); |
| return false; |
| } |
| pi = mClientPendingIntentCurrent; |
| } |
| if (pi != null) { |
| Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); |
| intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); |
| try { |
| pi.send(mContext, 0, intent); |
| } catch (CanceledException e) { |
| Log.e(TAG, "Error sending intent for media button down: ", e); |
| return false; |
| } |
| } else { |
| Log.i(TAG, "No-op when sending key click, no receiver right now"); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| |
| /** |
| * 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; |
| } |
| if (mIsRegistered) { |
| mAudioManager.remoteControlDisplayUsesBitmapSize(mRcd, |
| mArtworkWidth, mArtworkHeight); |
| } // else new values have been stored, and will be read by AudioManager with |
| // RemoteController.getArtworkSize() when AudioManager.registerRemoteController() |
| // is called. |
| } |
| 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; |
| } |
| mAudioManager.remoteControlDisplayWantsPlaybackPositionSync(mRcd, |
| POSITION_SYNCHRONIZATION_CHECK == sync); |
| 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; |
| } |
| |
| } |
| |
| |
| //================================================== |
| // Implementation of IRemoteControlDisplay interface |
| private static class RcDisplay extends IRemoteControlDisplay.Stub { |
| private final WeakReference<RemoteController> mController; |
| |
| RcDisplay(RemoteController rc) { |
| mController = new WeakReference<RemoteController>(rc); |
| } |
| |
| public void setCurrentClientId(int genId, PendingIntent clientMediaIntent, |
| boolean clearing) { |
| final RemoteController rc = mController.get(); |
| if (rc == null) { |
| return; |
| } |
| boolean isNew = false; |
| synchronized(mGenLock) { |
| if (rc.mClientGenerationIdCurrent != genId) { |
| rc.mClientGenerationIdCurrent = genId; |
| isNew = true; |
| } |
| } |
| if (clientMediaIntent != null) { |
| sendMsg(rc.mEventHandler, MSG_NEW_PENDING_INTENT, SENDMSG_REPLACE, |
| genId /*arg1*/, 0, clientMediaIntent /*obj*/, 0 /*delay*/); |
| } |
| if (isNew || clearing) { |
| sendMsg(rc.mEventHandler, MSG_CLIENT_CHANGE, SENDMSG_REPLACE, |
| genId /*arg1*/, clearing ? 1 : 0, null /*obj*/, 0 /*delay*/); |
| } |
| } |
| |
| public void setEnabled(boolean enabled) { |
| final RemoteController rc = mController.get(); |
| if (rc == null) { |
| return; |
| } |
| sendMsg(rc.mEventHandler, MSG_DISPLAY_ENABLE, SENDMSG_REPLACE, |
| enabled ? 1 : 0 /*arg1*/, 0, null /*obj*/, 0 /*delay*/); |
| } |
| |
| public void setPlaybackState(int genId, int state, |
| long stateChangeTimeMs, long currentPosMs, float speed) { |
| final RemoteController rc = mController.get(); |
| if (rc == null) { |
| return; |
| } |
| if (DEBUG) { |
| Log.d(TAG, "> new playback state: genId="+genId |
| + " state="+ state |
| + " changeTime="+ stateChangeTimeMs |
| + " pos=" + currentPosMs |
| + "ms speed=" + speed); |
| } |
| |
| synchronized(mGenLock) { |
| if (rc.mClientGenerationIdCurrent != genId) { |
| return; |
| } |
| } |
| final PlaybackInfo playbackInfo = |
| new PlaybackInfo(state, stateChangeTimeMs, currentPosMs, speed); |
| sendMsg(rc.mEventHandler, MSG_NEW_PLAYBACK_INFO, SENDMSG_REPLACE, |
| genId /*arg1*/, 0, playbackInfo /*obj*/, 0 /*delay*/); |
| |
| } |
| |
| public void setTransportControlInfo(int genId, int transportControlFlags, |
| int posCapabilities) { |
| final RemoteController rc = mController.get(); |
| if (rc == null) { |
| return; |
| } |
| synchronized(mGenLock) { |
| if (rc.mClientGenerationIdCurrent != genId) { |
| return; |
| } |
| } |
| sendMsg(rc.mEventHandler, MSG_NEW_TRANSPORT_INFO, SENDMSG_REPLACE, |
| genId /*arg1*/, transportControlFlags /*arg2*/, |
| null /*obj*/, 0 /*delay*/); |
| } |
| |
| public void setMetadata(int genId, Bundle metadata) { |
| final RemoteController rc = mController.get(); |
| if (rc == null) { |
| return; |
| } |
| if (DEBUG) { Log.e(TAG, "setMetadata("+genId+")"); } |
| if (metadata == null) { |
| return; |
| } |
| synchronized(mGenLock) { |
| if (rc.mClientGenerationIdCurrent != genId) { |
| return; |
| } |
| } |
| sendMsg(rc.mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE, |
| genId /*arg1*/, 0 /*arg2*/, |
| metadata /*obj*/, 0 /*delay*/); |
| } |
| |
| public void setArtwork(int genId, Bitmap artwork) { |
| final RemoteController rc = mController.get(); |
| if (rc == null) { |
| return; |
| } |
| if (DEBUG) { Log.v(TAG, "setArtwork("+genId+")"); } |
| synchronized(mGenLock) { |
| if (rc.mClientGenerationIdCurrent != genId) { |
| return; |
| } |
| } |
| Bundle metadata = new Bundle(1); |
| metadata.putParcelable(String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK), artwork); |
| sendMsg(rc.mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE, |
| genId /*arg1*/, 0 /*arg2*/, |
| metadata /*obj*/, 0 /*delay*/); |
| } |
| |
| public void setAllMetadata(int genId, Bundle metadata, Bitmap artwork) { |
| final RemoteController rc = mController.get(); |
| if (rc == null) { |
| return; |
| } |
| if (DEBUG) { Log.e(TAG, "setAllMetadata("+genId+")"); } |
| if ((metadata == null) && (artwork == null)) { |
| return; |
| } |
| synchronized(mGenLock) { |
| if (rc.mClientGenerationIdCurrent != genId) { |
| return; |
| } |
| } |
| if (metadata == null) { |
| metadata = new Bundle(1); |
| } |
| if (artwork != null) { |
| metadata.putParcelable(String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK), |
| artwork); |
| } |
| sendMsg(rc.mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE, |
| genId /*arg1*/, 0 /*arg2*/, |
| metadata /*obj*/, 0 /*delay*/); |
| } |
| } |
| |
| /** |
| * 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 extends MediaSessionManager.SessionListener { |
| @Override |
| public void onActiveSessionsChanged(List<MediaController> 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_NEW_PENDING_INTENT = 0; |
| private final static int MSG_NEW_PLAYBACK_INFO = 1; |
| private final static int MSG_NEW_TRANSPORT_INFO = 2; |
| private final static int MSG_NEW_METADATA = 3; // msg always has non-null obj parameter |
| private final static int MSG_CLIENT_CHANGE = 4; |
| private final static int MSG_DISPLAY_ENABLE = 5; |
| private final static int MSG_NEW_PLAYBACK_STATE = 6; |
| private final static int MSG_NEW_MEDIA_METADATA = 7; |
| |
| private class EventHandler extends Handler { |
| |
| public EventHandler(RemoteController rc, Looper looper) { |
| super(looper); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| switch(msg.what) { |
| case MSG_NEW_PENDING_INTENT: |
| onNewPendingIntent(msg.arg1, (PendingIntent) msg.obj); |
| break; |
| case MSG_NEW_PLAYBACK_INFO: |
| onNewPlaybackInfo(msg.arg1, (PlaybackInfo) msg.obj); |
| break; |
| case MSG_NEW_TRANSPORT_INFO: |
| onNewTransportInfo(msg.arg1, msg.arg2); |
| break; |
| case MSG_NEW_METADATA: |
| onNewMetadata(msg.arg1, (Bundle)msg.obj); |
| break; |
| case MSG_CLIENT_CHANGE: |
| onClientChange(msg.arg1, msg.arg2 == 1); |
| break; |
| case MSG_DISPLAY_ENABLE: |
| onDisplayEnable(msg.arg1 == 1); |
| break; |
| case MSG_NEW_PLAYBACK_STATE: |
| // same as new playback info but using new apis |
| 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()); |
| mSessionManager.addActiveSessionsListener(mSessionListener, listenerComponent, |
| UserHandle.myUserId()); |
| mSessionListener.onActiveSessionsChanged(mSessionManager |
| .getActiveSessions(listenerComponent)); |
| if (DEBUG) { |
| Log.d(TAG, "Registered session listener with component " + listenerComponent |
| + " for user " + UserHandle.myUserId()); |
| } |
| } |
| |
| /** |
| * @hide |
| */ |
| void stopListeningToSessions() { |
| mSessionManager.removeActiveSessionsListener(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); |
| } |
| |
| ///////////// These calls are used by the old APIs with RCC and RCD ////////////////////// |
| private void onNewPendingIntent(int genId, PendingIntent pi) { |
| synchronized(mGenLock) { |
| if (mClientGenerationIdCurrent != genId) { |
| return; |
| } |
| } |
| synchronized(mInfoLock) { |
| mClientPendingIntentCurrent = pi; |
| } |
| } |
| |
| private void onNewPlaybackInfo(int genId, PlaybackInfo pi) { |
| synchronized(mGenLock) { |
| if (mClientGenerationIdCurrent != genId) { |
| return; |
| } |
| } |
| final OnClientUpdateListener l; |
| synchronized(mInfoLock) { |
| l = this.mOnClientUpdateListener; |
| mLastPlaybackInfo = pi; |
| } |
| if (l != null) { |
| if (pi.mCurrentPosMs == RemoteControlClient.PLAYBACK_POSITION_ALWAYS_UNKNOWN) { |
| l.onClientPlaybackStateUpdate(pi.mState); |
| } else { |
| l.onClientPlaybackStateUpdate(pi.mState, pi.mStateChangeTimeMs, pi.mCurrentPosMs, |
| pi.mSpeed); |
| } |
| } |
| } |
| |
| private void onNewTransportInfo(int genId, int transportControlFlags) { |
| synchronized(mGenLock) { |
| if (mClientGenerationIdCurrent != genId) { |
| return; |
| } |
| } |
| final OnClientUpdateListener l; |
| synchronized(mInfoLock) { |
| l = mOnClientUpdateListener; |
| } |
| if (l != null) { |
| l.onClientTransportControlUpdate(transportControlFlags); |
| } |
| } |
| |
| /** |
| * @param genId |
| * @param metadata guaranteed to be always non-null |
| */ |
| private void onNewMetadata(int genId, Bundle metadata) { |
| synchronized(mGenLock) { |
| if (mClientGenerationIdCurrent != genId) { |
| return; |
| } |
| } |
| final OnClientUpdateListener l; |
| final MetadataEditor metadataEditor; |
| // prepare the received Bundle to be used inside a MetadataEditor |
| final long editableKeys = metadata.getLong( |
| String.valueOf(MediaMetadataEditor.KEY_EDITABLE_MASK), 0); |
| if (editableKeys != 0) { |
| metadata.remove(String.valueOf(MediaMetadataEditor.KEY_EDITABLE_MASK)); |
| } |
| synchronized(mInfoLock) { |
| l = mOnClientUpdateListener; |
| if ((mMetadataEditor != null) && (mMetadataEditor.mEditorMetadata != null)) { |
| if (mMetadataEditor.mEditorMetadata != metadata) { |
| // existing metadata, merge existing and new |
| mMetadataEditor.mEditorMetadata.putAll(metadata); |
| } |
| |
| mMetadataEditor.putBitmap(MediaMetadataEditor.BITMAP_KEY_ARTWORK, |
| (Bitmap)metadata.getParcelable( |
| String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK))); |
| mMetadataEditor.cleanupBitmapFromBundle(MediaMetadataEditor.BITMAP_KEY_ARTWORK); |
| } else { |
| mMetadataEditor = new MetadataEditor(metadata, editableKeys); |
| } |
| metadataEditor = mMetadataEditor; |
| } |
| if (l != null) { |
| l.onClientMetadataUpdate(metadataEditor); |
| } |
| } |
| |
| private void onClientChange(int genId, boolean clearing) { |
| synchronized(mGenLock) { |
| if (mClientGenerationIdCurrent != genId) { |
| return; |
| } |
| } |
| final OnClientUpdateListener l; |
| synchronized(mInfoLock) { |
| l = mOnClientUpdateListener; |
| mMetadataEditor = null; |
| } |
| if (l != null) { |
| l.onClientChange(clearing); |
| } |
| } |
| |
| private void onDisplayEnable(boolean enabled) { |
| final OnClientUpdateListener l; |
| synchronized(mInfoLock) { |
| mEnabled = enabled; |
| l = this.mOnClientUpdateListener; |
| } |
| if (!enabled) { |
| // when disabling, reset all info sent to the user |
| final int genId; |
| synchronized (mGenLock) { |
| genId = mClientGenerationIdCurrent; |
| } |
| // send "stopped" state, happened "now", playback position is 0, speed 0.0f |
| final PlaybackInfo pi = new PlaybackInfo(RemoteControlClient.PLAYSTATE_STOPPED, |
| SystemClock.elapsedRealtime() /*stateChangeTimeMs*/, |
| 0 /*currentPosMs*/, 0.0f /*speed*/); |
| sendMsg(mEventHandler, MSG_NEW_PLAYBACK_INFO, SENDMSG_REPLACE, |
| genId /*arg1*/, 0 /*arg2, ignored*/, pi /*obj*/, 0 /*delay*/); |
| // send "blank" transport control info: no controls are supported |
| sendMsg(mEventHandler, MSG_NEW_TRANSPORT_INFO, SENDMSG_REPLACE, |
| genId /*arg1*/, 0 /*arg2, no flags*/, |
| null /*obj, ignored*/, 0 /*delay*/); |
| // send dummy metadata with empty string for title and artist, duration of 0 |
| Bundle metadata = new Bundle(3); |
| metadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_TITLE), ""); |
| metadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_ARTIST), ""); |
| metadata.putLong(String.valueOf(MediaMetadataRetriever.METADATA_KEY_DURATION), 0); |
| sendMsg(mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE, |
| genId /*arg1*/, 0 /*arg2, ignored*/, metadata /*obj*/, 0 /*delay*/); |
| } |
| } |
| |
| ///////////// These calls are used by the new APIs with Sessions ////////////////////// |
| 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.removeCallback(mSessionCb); |
| mCurrentSession = null; |
| sendMsg(mEventHandler, MSG_CLIENT_CHANGE, SENDMSG_REPLACE, |
| 0 /* genId */, 1 /* clearing */, null /* obj */, 0 /* delay */); |
| } |
| } else if (mCurrentSession == null |
| || !controller.getSessionInfo().getId() |
| .equals(mCurrentSession.getSessionInfo().getId())) { |
| if (mCurrentSession != null) { |
| mCurrentSession.removeCallback(mSessionCb); |
| } |
| sendMsg(mEventHandler, MSG_CLIENT_CHANGE, SENDMSG_REPLACE, |
| 0 /* genId */, 0 /* clearing */, null /* obj */, 0 /* delay */); |
| mCurrentSession = controller; |
| mCurrentSession.addCallback(mSessionCb, mEventHandler); |
| |
| PlaybackState state = controller.getPlaybackState(); |
| sendMsg(mEventHandler, MSG_NEW_PLAYBACK_STATE, SENDMSG_REPLACE, |
| 0 /* genId */, 0, state /* obj */, 0 /* delay */); |
| |
| MediaMetadata metadata = controller.getMetadata(); |
| sendMsg(mEventHandler, MSG_NEW_MEDIA_METADATA, SENDMSG_REPLACE, |
| 0 /* arg1 */, 0 /* arg2 */, 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); |
| 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 mark this instance as registered. |
| * @param registered |
| */ |
| void setIsRegistered(boolean registered) { |
| synchronized (mInfoLock) { |
| mIsRegistered = registered; |
| } |
| } |
| |
| /** |
| * @hide |
| * Used by AudioManager to access binder to be registered/unregistered inside MediaFocusControl |
| * @return |
| */ |
| RcDisplay getRcDisplay() { |
| return mRcd; |
| } |
| |
| /** |
| * @hide |
| * Used by AudioManager to read the current artwork dimension |
| * @return array containing width (index 0) and height (index 1) of currently set artwork size |
| */ |
| int[] getArtworkSize() { |
| synchronized (mInfoLock) { |
| int[] size = { mArtworkWidth, mArtworkHeight }; |
| return size; |
| } |
| } |
| |
| /** |
| * @hide |
| * Used by AudioManager to access user listener receiving the client update notifications |
| * @return |
| */ |
| OnClientUpdateListener getUpdateListener() { |
| return mOnClientUpdateListener; |
| } |
| } |