Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.media;
     18 
     19 import android.app.ActivityManager;
     20 import android.content.ComponentName;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.graphics.Bitmap;
     24 import android.media.session.MediaController;
     25 import android.media.session.MediaSession;
     26 import android.media.session.MediaSessionLegacyHelper;
     27 import android.media.session.MediaSessionManager;
     28 import android.media.session.PlaybackState;
     29 import android.os.Bundle;
     30 import android.os.Handler;
     31 import android.os.Looper;
     32 import android.os.Message;
     33 import android.os.UserHandle;
     34 import android.util.DisplayMetrics;
     35 import android.util.Log;
     36 import android.view.KeyEvent;
     37 
     38 import java.lang.ref.WeakReference;
     39 import java.util.List;
     40 
     41 /**
     42  * The RemoteController class is used to control media playback, display and update media metadata
     43  * and playback status, published by applications using the {@link RemoteControlClient} class.
     44  * <p>
     45  * A RemoteController shall be registered through
     46  * {@link AudioManager#registerRemoteController(RemoteController)} in order for the system to send
     47  * media event updates to the {@link OnClientUpdateListener} listener set in the class constructor.
     48  * Implement the methods of the interface to receive the information published by the active
     49  * {@link RemoteControlClient} instances.
     50  * <br>By default an {@link OnClientUpdateListener} implementation will not receive bitmaps for
     51  * album art. Use {@link #setArtworkConfiguration(int, int)} to receive images as well.
     52  * <p>
     53  * Registration requires the {@link OnClientUpdateListener} listener to be one of the enabled
     54  * notification listeners (see {@link android.service.notification.NotificationListenerService}).
     55  *
     56  * @deprecated Use {@link MediaController} instead.
     57  */
     58 @Deprecated public final class RemoteController
     59 {
     60     private final static int MAX_BITMAP_DIMENSION = 512;
     61     private final static String TAG = "RemoteController";
     62     private final static boolean DEBUG = false;
     63     private final static Object mInfoLock = new Object();
     64     private final Context mContext;
     65     private final int mMaxBitmapDimension;
     66     private MetadataEditor mMetadataEditor;
     67 
     68     private MediaSessionManager mSessionManager;
     69     private MediaSessionManager.OnActiveSessionsChangedListener mSessionListener;
     70     private MediaController.Callback mSessionCb = new MediaControllerCallback();
     71 
     72     /**
     73      * Synchronized on mInfoLock
     74      */
     75     private boolean mIsRegistered = false;
     76     private OnClientUpdateListener mOnClientUpdateListener;
     77     private PlaybackInfo mLastPlaybackInfo;
     78     private int mArtworkWidth = -1;
     79     private int mArtworkHeight = -1;
     80     private boolean mEnabled = true;
     81     // synchronized on mInfoLock, for USE_SESSION apis.
     82     private MediaController mCurrentSession;
     83 
     84     /**
     85      * Class constructor.
     86      * @param context the {@link Context}, must be non-null.
     87      * @param updateListener the listener to be called whenever new client information is available,
     88      *     must be non-null.
     89      * @throws IllegalArgumentException
     90      */
     91     public RemoteController(Context context, OnClientUpdateListener updateListener)
     92             throws IllegalArgumentException {
     93         this(context, updateListener, null);
     94     }
     95 
     96     /**
     97      * Class constructor.
     98      * @param context the {@link Context}, must be non-null.
     99      * @param updateListener the listener to be called whenever new client information is available,
    100      *     must be non-null.
    101      * @param looper the {@link Looper} on which to run the event loop,
    102      *     or null to use the current thread's looper.
    103      * @throws java.lang.IllegalArgumentException
    104      */
    105     public RemoteController(Context context, OnClientUpdateListener updateListener, Looper looper)
    106             throws IllegalArgumentException {
    107         if (context == null) {
    108             throw new IllegalArgumentException("Invalid null Context");
    109         }
    110         if (updateListener == null) {
    111             throw new IllegalArgumentException("Invalid null OnClientUpdateListener");
    112         }
    113         if (looper != null) {
    114             mEventHandler = new EventHandler(this, looper);
    115         } else {
    116             Looper l = Looper.myLooper();
    117             if (l != null) {
    118                 mEventHandler = new EventHandler(this, l);
    119             } else {
    120                 throw new IllegalArgumentException("Calling thread not associated with a looper");
    121             }
    122         }
    123         mOnClientUpdateListener = updateListener;
    124         mContext = context;
    125         mSessionManager = (MediaSessionManager) context
    126                 .getSystemService(Context.MEDIA_SESSION_SERVICE);
    127         mSessionListener = new TopTransportSessionListener();
    128 
    129         if (ActivityManager.isLowRamDeviceStatic()) {
    130             mMaxBitmapDimension = MAX_BITMAP_DIMENSION;
    131         } else {
    132             final DisplayMetrics dm = context.getResources().getDisplayMetrics();
    133             mMaxBitmapDimension = Math.max(dm.widthPixels, dm.heightPixels);
    134         }
    135     }
    136 
    137 
    138     /**
    139      * Interface definition for the callbacks to be invoked whenever media events, metadata
    140      * and playback status are available.
    141      */
    142     public interface OnClientUpdateListener {
    143         /**
    144          * Called whenever all information, previously received through the other
    145          * methods of the listener, is no longer valid and is about to be refreshed.
    146          * This is typically called whenever a new {@link RemoteControlClient} has been selected
    147          * by the system to have its media information published.
    148          * @param clearing true if there is no selected RemoteControlClient and no information
    149          *     is available.
    150          */
    151         public void onClientChange(boolean clearing);
    152 
    153         /**
    154          * Called whenever the playback state has changed.
    155          * It is called when no information is known about the playback progress in the media and
    156          * the playback speed.
    157          * @param state one of the playback states authorized
    158          *     in {@link RemoteControlClient#setPlaybackState(int)}.
    159          */
    160         public void onClientPlaybackStateUpdate(int state);
    161         /**
    162          * Called whenever the playback state has changed, and playback position
    163          * and speed are known.
    164          * @param state one of the playback states authorized
    165          *     in {@link RemoteControlClient#setPlaybackState(int)}.
    166          * @param stateChangeTimeMs the system time at which the state change was reported,
    167          *     expressed in ms. Based on {@link android.os.SystemClock#elapsedRealtime()}.
    168          * @param currentPosMs a positive value for the current media playback position expressed
    169          *     in ms, a negative value if the position is temporarily unknown.
    170          * @param speed  a value expressed as a ratio of 1x playback: 1.0f is normal playback,
    171          *    2.0f is 2x, 0.5f is half-speed, -2.0f is rewind at 2x speed. 0.0f means nothing is
    172          *    playing (e.g. when state is {@link RemoteControlClient#PLAYSTATE_ERROR}).
    173          */
    174         public void onClientPlaybackStateUpdate(int state, long stateChangeTimeMs,
    175                 long currentPosMs, float speed);
    176         /**
    177          * Called whenever the transport control flags have changed.
    178          * @param transportControlFlags one of the flags authorized
    179          *     in {@link RemoteControlClient#setTransportControlFlags(int)}.
    180          */
    181         public void onClientTransportControlUpdate(int transportControlFlags);
    182         /**
    183          * Called whenever new metadata is available.
    184          * See the {@link MediaMetadataEditor#putLong(int, long)},
    185          *  {@link MediaMetadataEditor#putString(int, String)},
    186          *  {@link MediaMetadataEditor#putBitmap(int, Bitmap)}, and
    187          *  {@link MediaMetadataEditor#putObject(int, Object)} methods for the various keys that
    188          *  can be queried.
    189          * @param metadataEditor the container of the new metadata.
    190          */
    191         public void onClientMetadataUpdate(MetadataEditor metadataEditor);
    192     };
    193 
    194     /**
    195      * Return the estimated playback position of the current media track or a negative value
    196      * if not available.
    197      *
    198      * <p>The value returned is estimated by the current process and may not be perfect.
    199      * The time returned by this method is calculated from the last state change time based
    200      * on the current play position at that time and the last known playback speed.
    201      * An application may call {@link #setSynchronizationMode(int)} to apply
    202      * a synchronization policy that will periodically re-sync the estimated position
    203      * with the RemoteControlClient.</p>
    204      *
    205      * @return the current estimated playback position in milliseconds or a negative value
    206      *         if not available
    207      *
    208      * @see OnClientUpdateListener#onClientPlaybackStateUpdate(int, long, long, float)
    209      */
    210     public long getEstimatedMediaPosition() {
    211         synchronized (mInfoLock) {
    212             if (mCurrentSession != null) {
    213                 PlaybackState state = mCurrentSession.getPlaybackState();
    214                 if (state != null) {
    215                     return state.getPosition();
    216                 }
    217             }
    218         }
    219         return -1;
    220     }
    221 
    222 
    223     /**
    224      * Send a simulated key event for a media button to be received by the current client.
    225      * To simulate a key press, you must first send a KeyEvent built with
    226      * a {@link KeyEvent#ACTION_DOWN} action, then another event with the {@link KeyEvent#ACTION_UP}
    227      * action.
    228      * <p>The key event will be sent to the registered receiver
    229      * (see {@link AudioManager#registerMediaButtonEventReceiver(PendingIntent)}) whose associated
    230      * {@link RemoteControlClient}'s metadata and playback state is published (there may be
    231      * none under some circumstances).
    232      * @param keyEvent a {@link KeyEvent} instance whose key code is one of
    233      *     {@link KeyEvent#KEYCODE_MUTE},
    234      *     {@link KeyEvent#KEYCODE_HEADSETHOOK},
    235      *     {@link KeyEvent#KEYCODE_MEDIA_PLAY},
    236      *     {@link KeyEvent#KEYCODE_MEDIA_PAUSE},
    237      *     {@link KeyEvent#KEYCODE_MEDIA_PLAY_PAUSE},
    238      *     {@link KeyEvent#KEYCODE_MEDIA_STOP},
    239      *     {@link KeyEvent#KEYCODE_MEDIA_NEXT},
    240      *     {@link KeyEvent#KEYCODE_MEDIA_PREVIOUS},
    241      *     {@link KeyEvent#KEYCODE_MEDIA_REWIND},
    242      *     {@link KeyEvent#KEYCODE_MEDIA_RECORD},
    243      *     {@link KeyEvent#KEYCODE_MEDIA_FAST_FORWARD},
    244      *     {@link KeyEvent#KEYCODE_MEDIA_CLOSE},
    245      *     {@link KeyEvent#KEYCODE_MEDIA_EJECT},
    246      *     or {@link KeyEvent#KEYCODE_MEDIA_AUDIO_TRACK}.
    247      * @return true if the event was successfully sent, false otherwise.
    248      * @throws IllegalArgumentException
    249      */
    250     public boolean sendMediaKeyEvent(KeyEvent keyEvent) throws IllegalArgumentException {
    251         if (!KeyEvent.isMediaKey(keyEvent.getKeyCode())) {
    252             throw new IllegalArgumentException("not a media key event");
    253         }
    254         synchronized (mInfoLock) {
    255             if (mCurrentSession != null) {
    256                 return mCurrentSession.dispatchMediaButtonEvent(keyEvent);
    257             }
    258             return false;
    259         }
    260     }
    261 
    262 
    263     /**
    264      * Sets the new playback position.
    265      * This method can only be called on a registered RemoteController.
    266      * @param timeMs a 0 or positive value for the new playback position, expressed in ms.
    267      * @return true if the command to set the playback position was successfully sent.
    268      * @throws IllegalArgumentException
    269      */
    270     public boolean seekTo(long timeMs) throws IllegalArgumentException {
    271         if (!mEnabled) {
    272             Log.e(TAG, "Cannot use seekTo() from a disabled RemoteController");
    273             return false;
    274         }
    275         if (timeMs < 0) {
    276             throw new IllegalArgumentException("illegal negative time value");
    277         }
    278         synchronized (mInfoLock) {
    279             if (mCurrentSession != null) {
    280                 mCurrentSession.getTransportControls().seekTo(timeMs);
    281             }
    282         }
    283         return true;
    284     }
    285 
    286 
    287     /**
    288      * @hide
    289      * @param wantBitmap
    290      * @param width
    291      * @param height
    292      * @return true if successful
    293      * @throws IllegalArgumentException
    294      */
    295     public boolean setArtworkConfiguration(boolean wantBitmap, int width, int height)
    296             throws IllegalArgumentException {
    297         synchronized (mInfoLock) {
    298             if (wantBitmap) {
    299                 if ((width > 0) && (height > 0)) {
    300                     if (width > mMaxBitmapDimension) { width = mMaxBitmapDimension; }
    301                     if (height > mMaxBitmapDimension) { height = mMaxBitmapDimension; }
    302                     mArtworkWidth = width;
    303                     mArtworkHeight = height;
    304                 } else {
    305                     throw new IllegalArgumentException("Invalid dimensions");
    306                 }
    307             } else {
    308                 mArtworkWidth = -1;
    309                 mArtworkHeight = -1;
    310             }
    311         }
    312         return true;
    313     }
    314 
    315     /**
    316      * Set the maximum artwork image dimensions to be received in the metadata.
    317      * No bitmaps will be received unless this has been specified.
    318      * @param width the maximum width in pixels
    319      * @param height  the maximum height in pixels
    320      * @return true if the artwork dimension was successfully set.
    321      * @throws IllegalArgumentException
    322      */
    323     public boolean setArtworkConfiguration(int width, int height) throws IllegalArgumentException {
    324         return setArtworkConfiguration(true, width, height);
    325     }
    326 
    327     /**
    328      * Prevents this RemoteController from receiving artwork images.
    329      * @return true if receiving artwork images was successfully disabled.
    330      */
    331     public boolean clearArtworkConfiguration() {
    332         return setArtworkConfiguration(false, -1, -1);
    333     }
    334 
    335 
    336     /**
    337      * Default playback position synchronization mode where the RemoteControlClient is not
    338      * asked regularly for its playback position to see if it has drifted from the estimated
    339      * position.
    340      */
    341     public static final int POSITION_SYNCHRONIZATION_NONE = 0;
    342 
    343     /**
    344      * The playback position synchronization mode where the RemoteControlClient instances which
    345      * expose their playback position to the framework, will be regularly polled to check
    346      * whether any drift has been noticed between their estimated position and the one they report.
    347      * Note that this mode should only ever be used when needing to display very accurate playback
    348      * position, as regularly polling a RemoteControlClient for its position may have an impact
    349      * on battery life (if applicable) when this query will trigger network transactions in the
    350      * case of remote playback.
    351      */
    352     public static final int POSITION_SYNCHRONIZATION_CHECK = 1;
    353 
    354     /**
    355      * Set the playback position synchronization mode.
    356      * Must be called on a registered RemoteController.
    357      * @param sync {@link #POSITION_SYNCHRONIZATION_NONE} or {@link #POSITION_SYNCHRONIZATION_CHECK}
    358      * @return true if the synchronization mode was successfully set.
    359      * @throws IllegalArgumentException
    360      */
    361     public boolean setSynchronizationMode(int sync) throws IllegalArgumentException {
    362         if ((sync != POSITION_SYNCHRONIZATION_NONE) && (sync != POSITION_SYNCHRONIZATION_CHECK)) {
    363             throw new IllegalArgumentException("Unknown synchronization mode " + sync);
    364         }
    365         if (!mIsRegistered) {
    366             Log.e(TAG, "Cannot set synchronization mode on an unregistered RemoteController");
    367             return false;
    368         }
    369         // deprecated, no-op
    370         return true;
    371     }
    372 
    373 
    374     /**
    375      * Creates a {@link MetadataEditor} for updating metadata values of the editable keys of
    376      * the current {@link RemoteControlClient}.
    377      * This method can only be called on a registered RemoteController.
    378      * @return a new MetadataEditor instance.
    379      */
    380     public MetadataEditor editMetadata() {
    381         MetadataEditor editor = new MetadataEditor();
    382         editor.mEditorMetadata = new Bundle();
    383         editor.mEditorArtwork = null;
    384         editor.mMetadataChanged = true;
    385         editor.mArtworkChanged = true;
    386         editor.mEditableKeys = 0;
    387         return editor;
    388     }
    389 
    390     /**
    391      * A class to read the metadata published by a {@link RemoteControlClient}, or send a
    392      * {@link RemoteControlClient} new values for keys that can be edited.
    393      */
    394     public class MetadataEditor extends MediaMetadataEditor {
    395         /**
    396          * @hide
    397          */
    398         protected MetadataEditor() { }
    399 
    400         /**
    401          * @hide
    402          */
    403         protected MetadataEditor(Bundle metadata, long editableKeys) {
    404             mEditorMetadata = metadata;
    405             mEditableKeys = editableKeys;
    406 
    407             mEditorArtwork = (Bitmap) metadata.getParcelable(
    408                     String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK));
    409             if (mEditorArtwork != null) {
    410                 cleanupBitmapFromBundle(MediaMetadataEditor.BITMAP_KEY_ARTWORK);
    411             }
    412 
    413             mMetadataChanged = true;
    414             mArtworkChanged = true;
    415             mApplied = false;
    416         }
    417 
    418         private void cleanupBitmapFromBundle(int key) {
    419             if (METADATA_KEYS_TYPE.get(key, METADATA_TYPE_INVALID) == METADATA_TYPE_BITMAP) {
    420                 mEditorMetadata.remove(String.valueOf(key));
    421             }
    422         }
    423 
    424         /**
    425          * Applies all of the metadata changes that have been set since the MediaMetadataEditor
    426          * instance was created with {@link RemoteController#editMetadata()}
    427          * or since {@link #clear()} was called.
    428          */
    429         public synchronized void apply() {
    430             // "applying" a metadata bundle in RemoteController is only for sending edited
    431             // key values back to the RemoteControlClient, so here we only care about the only
    432             // editable key we support: RATING_KEY_BY_USER
    433             if (!mMetadataChanged) {
    434                 return;
    435             }
    436             synchronized (mInfoLock) {
    437                 if (mCurrentSession != null) {
    438                     if (mEditorMetadata.containsKey(
    439                             String.valueOf(MediaMetadataEditor.RATING_KEY_BY_USER))) {
    440                         Rating rating = (Rating) getObject(
    441                                 MediaMetadataEditor.RATING_KEY_BY_USER, null);
    442                         if (rating != null) {
    443                             mCurrentSession.getTransportControls().setRating(rating);
    444                         }
    445                     }
    446                 }
    447             }
    448             // NOT setting mApplied to true as this type of MetadataEditor will be applied
    449             // multiple times, whenever the user of a RemoteController needs to change the
    450             // metadata (e.g. user changes the rating of a song more than once during playback)
    451             mApplied = false;
    452         }
    453 
    454     }
    455 
    456     /**
    457      * This receives updates when the current session changes. This is
    458      * registered to receive the updates on the handler thread so it can call
    459      * directly into the appropriate methods.
    460      */
    461     private class MediaControllerCallback extends MediaController.Callback {
    462         @Override
    463         public void onPlaybackStateChanged(PlaybackState state) {
    464             onNewPlaybackState(state);
    465         }
    466 
    467         @Override
    468         public void onMetadataChanged(MediaMetadata metadata) {
    469             onNewMediaMetadata(metadata);
    470         }
    471     }
    472 
    473     /**
    474      * Listens for changes to the active session stack and replaces the
    475      * currently tracked session if it has changed.
    476      */
    477     private class TopTransportSessionListener implements
    478             MediaSessionManager.OnActiveSessionsChangedListener {
    479 
    480         @Override
    481         public void onActiveSessionsChanged(List<MediaController> controllers) {
    482             int size = controllers.size();
    483             for (int i = 0; i < size; i++) {
    484                 MediaController controller = controllers.get(i);
    485                 long flags = controller.getFlags();
    486                 // We only care about sessions that handle transport controls,
    487                 // which will be true for apps using RCC
    488                 if ((flags & MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS) != 0) {
    489                     updateController(controller);
    490                     return;
    491                 }
    492             }
    493             updateController(null);
    494         }
    495 
    496     }
    497 
    498     //==================================================
    499     // Event handling
    500     private final EventHandler mEventHandler;
    501     private final static int MSG_CLIENT_CHANGE      = 0;
    502     private final static int MSG_NEW_PLAYBACK_STATE = 1;
    503     private final static int MSG_NEW_MEDIA_METADATA = 2;
    504 
    505     private class EventHandler extends Handler {
    506 
    507         public EventHandler(RemoteController rc, Looper looper) {
    508             super(looper);
    509         }
    510 
    511         @Override
    512         public void handleMessage(Message msg) {
    513             switch(msg.what) {
    514                 case MSG_CLIENT_CHANGE:
    515                     onClientChange(msg.arg2 == 1);
    516                     break;
    517                 case MSG_NEW_PLAYBACK_STATE:
    518                     onNewPlaybackState((PlaybackState) msg.obj);
    519                     break;
    520                 case MSG_NEW_MEDIA_METADATA:
    521                     onNewMediaMetadata((MediaMetadata) msg.obj);
    522                     break;
    523                 default:
    524                     Log.e(TAG, "unknown event " + msg.what);
    525             }
    526         }
    527     }
    528 
    529     /**
    530      * @hide
    531      */
    532     void startListeningToSessions() {
    533         final ComponentName listenerComponent = new ComponentName(mContext,
    534                 mOnClientUpdateListener.getClass());
    535         Handler handler = null;
    536         if (Looper.myLooper() == null) {
    537             handler = new Handler(Looper.getMainLooper());
    538         }
    539         mSessionManager.addOnActiveSessionsChangedListener(mSessionListener, listenerComponent,
    540                 UserHandle.myUserId(), handler);
    541         mSessionListener.onActiveSessionsChanged(mSessionManager
    542                 .getActiveSessions(listenerComponent));
    543         if (DEBUG) {
    544             Log.d(TAG, "Registered session listener with component " + listenerComponent
    545                     + " for user " + UserHandle.myUserId());
    546         }
    547     }
    548 
    549     /**
    550      * @hide
    551      */
    552     void stopListeningToSessions() {
    553         mSessionManager.removeOnActiveSessionsChangedListener(mSessionListener);
    554         if (DEBUG) {
    555             Log.d(TAG, "Unregistered session listener for user "
    556                     + UserHandle.myUserId());
    557         }
    558     }
    559 
    560     /** If the msg is already queued, replace it with this one. */
    561     private static final int SENDMSG_REPLACE = 0;
    562     /** If the msg is already queued, ignore this one and leave the old. */
    563     private static final int SENDMSG_NOOP = 1;
    564     /** If the msg is already queued, queue this one and leave the old. */
    565     private static final int SENDMSG_QUEUE = 2;
    566 
    567     private static void sendMsg(Handler handler, int msg, int existingMsgPolicy,
    568             int arg1, int arg2, Object obj, int delayMs) {
    569         if (handler == null) {
    570             Log.e(TAG, "null event handler, will not deliver message " + msg);
    571             return;
    572         }
    573         if (existingMsgPolicy == SENDMSG_REPLACE) {
    574             handler.removeMessages(msg);
    575         } else if (existingMsgPolicy == SENDMSG_NOOP && handler.hasMessages(msg)) {
    576             return;
    577         }
    578         handler.sendMessageDelayed(handler.obtainMessage(msg, arg1, arg2, obj), delayMs);
    579     }
    580 
    581     private void onClientChange(boolean clearing) {
    582         final OnClientUpdateListener l;
    583         synchronized(mInfoLock) {
    584             l = mOnClientUpdateListener;
    585             mMetadataEditor = null;
    586         }
    587         if (l != null) {
    588             l.onClientChange(clearing);
    589         }
    590     }
    591 
    592     private void updateController(MediaController controller) {
    593         if (DEBUG) {
    594             Log.d(TAG, "Updating controller to " + controller + " previous controller is "
    595                     + mCurrentSession);
    596         }
    597         synchronized (mInfoLock) {
    598             if (controller == null) {
    599                 if (mCurrentSession != null) {
    600                     mCurrentSession.unregisterCallback(mSessionCb);
    601                     mCurrentSession = null;
    602                     sendMsg(mEventHandler, MSG_CLIENT_CHANGE, SENDMSG_REPLACE,
    603                             0 /* arg1 ignored */, 1 /* clearing */, null /* obj */, 0 /* delay */);
    604                 }
    605             } else if (mCurrentSession == null
    606                     || !controller.getSessionToken()
    607                             .equals(mCurrentSession.getSessionToken())) {
    608                 if (mCurrentSession != null) {
    609                     mCurrentSession.unregisterCallback(mSessionCb);
    610                 }
    611                 sendMsg(mEventHandler, MSG_CLIENT_CHANGE, SENDMSG_REPLACE,
    612                         0 /* arg1 ignored */, 0 /* clearing */, null /* obj */, 0 /* delay */);
    613                 mCurrentSession = controller;
    614                 mCurrentSession.registerCallback(mSessionCb, mEventHandler);
    615 
    616                 PlaybackState state = controller.getPlaybackState();
    617                 sendMsg(mEventHandler, MSG_NEW_PLAYBACK_STATE, SENDMSG_REPLACE,
    618                         0 /* arg1 ignored */, 0 /* arg2 ignored */, state /* obj */, 0 /* delay */);
    619 
    620                 MediaMetadata metadata = controller.getMetadata();
    621                 sendMsg(mEventHandler, MSG_NEW_MEDIA_METADATA, SENDMSG_REPLACE,
    622                         0 /* arg1 ignored */, 0 /* arg2 ignored*/, metadata /* obj */, 0 /*delay*/);
    623             }
    624             // else same controller, no need to update
    625         }
    626     }
    627 
    628     private void onNewPlaybackState(PlaybackState state) {
    629         final OnClientUpdateListener l;
    630         synchronized (mInfoLock) {
    631             l = this.mOnClientUpdateListener;
    632         }
    633         if (l != null) {
    634             int playstate = state == null ? RemoteControlClient.PLAYSTATE_NONE : PlaybackState
    635                     .getRccStateFromState(state.getState());
    636             if (state == null || state.getPosition() == PlaybackState.PLAYBACK_POSITION_UNKNOWN) {
    637                 l.onClientPlaybackStateUpdate(playstate);
    638             } else {
    639                 l.onClientPlaybackStateUpdate(playstate, state.getLastPositionUpdateTime(),
    640                         state.getPosition(), state.getPlaybackSpeed());
    641             }
    642             if (state != null) {
    643                 l.onClientTransportControlUpdate(
    644                         PlaybackState.getRccControlFlagsFromActions(state.getActions()));
    645             }
    646         }
    647     }
    648 
    649     private void onNewMediaMetadata(MediaMetadata metadata) {
    650         if (metadata == null) {
    651             // RemoteController only handles non-null metadata
    652             return;
    653         }
    654         final OnClientUpdateListener l;
    655         final MetadataEditor metadataEditor;
    656         // prepare the received Bundle to be used inside a MetadataEditor
    657         synchronized(mInfoLock) {
    658             l = mOnClientUpdateListener;
    659             boolean canRate = mCurrentSession != null
    660                     && mCurrentSession.getRatingType() != Rating.RATING_NONE;
    661             long editableKeys = canRate ? MediaMetadataEditor.RATING_KEY_BY_USER : 0;
    662             Bundle legacyMetadata = MediaSessionLegacyHelper.getOldMetadata(metadata,
    663                     mArtworkWidth, mArtworkHeight);
    664             mMetadataEditor = new MetadataEditor(legacyMetadata, editableKeys);
    665             metadataEditor = mMetadataEditor;
    666         }
    667         if (l != null) {
    668             l.onClientMetadataUpdate(metadataEditor);
    669         }
    670     }
    671 
    672     //==================================================
    673     private static class PlaybackInfo {
    674         int mState;
    675         long mStateChangeTimeMs;
    676         long mCurrentPosMs;
    677         float mSpeed;
    678 
    679         PlaybackInfo(int state, long stateChangeTimeMs, long currentPosMs, float speed) {
    680             mState = state;
    681             mStateChangeTimeMs = stateChangeTimeMs;
    682             mCurrentPosMs = currentPosMs;
    683             mSpeed = speed;
    684         }
    685     }
    686 
    687     /**
    688      * @hide
    689      * Used by AudioManager to access user listener receiving the client update notifications
    690      * @return
    691      */
    692     OnClientUpdateListener getUpdateListener() {
    693         return mOnClientUpdateListener;
    694     }
    695 }
    696