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
17package android.media;
18
19import android.app.ActivityManager;
20import android.content.ComponentName;
21import android.content.Context;
22import android.content.Intent;
23import android.graphics.Bitmap;
24import android.media.session.MediaController;
25import android.media.session.MediaSession;
26import android.media.session.MediaSessionLegacyHelper;
27import android.media.session.MediaSessionManager;
28import android.media.session.PlaybackState;
29import android.os.Bundle;
30import android.os.Handler;
31import android.os.Looper;
32import android.os.Message;
33import android.os.UserHandle;
34import android.util.DisplayMetrics;
35import android.util.Log;
36import android.view.KeyEvent;
37
38import java.lang.ref.WeakReference;
39import 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