1/*
2 * Copyright 2018 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 androidx.media;
18
19import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION;
20
21import static androidx.media.MediaConstants2.ARGUMENT_ALLOWED_COMMANDS;
22import static androidx.media.MediaConstants2.ARGUMENT_ARGUMENTS;
23import static androidx.media.MediaConstants2.ARGUMENT_BUFFERING_STATE;
24import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_BUTTONS;
25import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_CODE;
26import static androidx.media.MediaConstants2.ARGUMENT_CUSTOM_COMMAND;
27import static androidx.media.MediaConstants2.ARGUMENT_ERROR_CODE;
28import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS;
29import static androidx.media.MediaConstants2.ARGUMENT_ICONTROLLER_CALLBACK;
30import static androidx.media.MediaConstants2.ARGUMENT_ITEM_COUNT;
31import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ID;
32import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ITEM;
33import static androidx.media.MediaConstants2.ARGUMENT_PACKAGE_NAME;
34import static androidx.media.MediaConstants2.ARGUMENT_PID;
35import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_INFO;
36import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_SPEED;
37import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_STATE_COMPAT;
38import static androidx.media.MediaConstants2.ARGUMENT_PLAYER_STATE;
39import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST;
40import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_INDEX;
41import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_METADATA;
42import static androidx.media.MediaConstants2.ARGUMENT_QUERY;
43import static androidx.media.MediaConstants2.ARGUMENT_RATING;
44import static androidx.media.MediaConstants2.ARGUMENT_REPEAT_MODE;
45import static androidx.media.MediaConstants2.ARGUMENT_RESULT_RECEIVER;
46import static androidx.media.MediaConstants2.ARGUMENT_ROUTE_BUNDLE;
47import static androidx.media.MediaConstants2.ARGUMENT_SEEK_POSITION;
48import static androidx.media.MediaConstants2.ARGUMENT_SHUFFLE_MODE;
49import static androidx.media.MediaConstants2.ARGUMENT_UID;
50import static androidx.media.MediaConstants2.ARGUMENT_URI;
51import static androidx.media.MediaConstants2.ARGUMENT_VOLUME;
52import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_DIRECTION;
53import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_FLAGS;
54import static androidx.media.MediaConstants2.CONNECT_RESULT_CONNECTED;
55import static androidx.media.MediaConstants2.CONNECT_RESULT_DISCONNECTED;
56import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_COMMAND_CODE;
57import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_CUSTOM_COMMAND;
58import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_CONNECT;
59import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_DISCONNECT;
60import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED;
61import static androidx.media.MediaConstants2.SESSION_EVENT_ON_BUFFERING_STATE_CHANGED;
62import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CHILDREN_CHANGED;
63import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED;
64import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ERROR;
65import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED;
66import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED;
67import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYER_STATE_CHANGED;
68import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_CHANGED;
69import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED;
70import static androidx.media.MediaConstants2.SESSION_EVENT_ON_REPEAT_MODE_CHANGED;
71import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ROUTES_INFO_CHANGED;
72import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SEARCH_RESULT_CHANGED;
73import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SEEK_COMPLETED;
74import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED;
75import static androidx.media.MediaConstants2.SESSION_EVENT_SEND_CUSTOM_COMMAND;
76import static androidx.media.MediaConstants2.SESSION_EVENT_SET_CUSTOM_LAYOUT;
77import static androidx.media.MediaPlayerInterface.BUFFERING_STATE_UNKNOWN;
78import static androidx.media.MediaPlayerInterface.UNKNOWN_TIME;
79import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE;
80import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY;
81import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE;
82import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_RESET;
83import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO;
84import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SET_SPEED;
85import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM;
86import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM;
87import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM;
88import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST;
89import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST_METADATA;
90import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE;
91import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE;
92import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM;
93import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM;
94import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM;
95import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_FAST_FORWARD;
96import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID;
97import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH;
98import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI;
99import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID;
100import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH;
101import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI;
102import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_REWIND;
103import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SELECT_ROUTE;
104import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SET_RATING;
105import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO;
106import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO;
107import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_ADJUST_VOLUME;
108import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_SET_VOLUME;
109
110import android.annotation.TargetApi;
111import android.app.PendingIntent;
112import android.content.Context;
113import android.net.Uri;
114import android.os.Build;
115import android.os.Bundle;
116import android.os.Handler;
117import android.os.HandlerThread;
118import android.os.IBinder;
119import android.os.Process;
120import android.os.RemoteException;
121import android.os.ResultReceiver;
122import android.os.SystemClock;
123import android.support.v4.media.MediaBrowserCompat;
124import android.support.v4.media.MediaMetadataCompat;
125import android.support.v4.media.session.MediaControllerCompat;
126import android.support.v4.media.session.MediaSessionCompat;
127import android.support.v4.media.session.PlaybackStateCompat;
128import android.util.Log;
129
130import androidx.annotation.GuardedBy;
131import androidx.annotation.NonNull;
132import androidx.annotation.Nullable;
133import androidx.core.app.BundleCompat;
134import androidx.media.MediaController2.ControllerCallback;
135import androidx.media.MediaController2.PlaybackInfo;
136import androidx.media.MediaController2.VolumeDirection;
137import androidx.media.MediaController2.VolumeFlags;
138import androidx.media.MediaPlaylistAgent.RepeatMode;
139import androidx.media.MediaPlaylistAgent.ShuffleMode;
140import androidx.media.MediaSession2.CommandButton;
141
142import java.util.List;
143import java.util.concurrent.Executor;
144
145@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
146class MediaController2ImplBase implements MediaController2.SupportLibraryImpl {
147
148    private static final String TAG = "MC2ImplBase";
149    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
150
151    // Note: Using {@code null} doesn't helpful here because MediaBrowserServiceCompat always wraps
152    //       the rootHints so it becomes non-null.
153    static final Bundle sDefaultRootExtras = new Bundle();
154    static {
155        sDefaultRootExtras.putBoolean(MediaConstants2.ROOT_EXTRA_DEFAULT, true);
156    }
157
158    private final Context mContext;
159    private final Object mLock = new Object();
160
161    private final SessionToken2 mToken;
162    private final ControllerCallback mCallback;
163    private final Executor mCallbackExecutor;
164    private final IBinder.DeathRecipient mDeathRecipient;
165
166    private final HandlerThread mHandlerThread;
167    private final Handler mHandler;
168
169    private MediaController2 mInstance;
170
171    @GuardedBy("mLock")
172    private MediaBrowserCompat mBrowserCompat;
173    @GuardedBy("mLock")
174    private boolean mIsReleased;
175    @GuardedBy("mLock")
176    private List<MediaItem2> mPlaylist;
177    @GuardedBy("mLock")
178    private MediaMetadata2 mPlaylistMetadata;
179    @GuardedBy("mLock")
180    private @RepeatMode int mRepeatMode;
181    @GuardedBy("mLock")
182    private @ShuffleMode int mShuffleMode;
183    @GuardedBy("mLock")
184    private int mPlayerState;
185    @GuardedBy("mLock")
186    private MediaItem2 mCurrentMediaItem;
187    @GuardedBy("mLock")
188    private int mBufferingState;
189    @GuardedBy("mLock")
190    private PlaybackInfo mPlaybackInfo;
191    @GuardedBy("mLock")
192    private SessionCommandGroup2 mAllowedCommands;
193
194    // Media 1.0 variables
195    @GuardedBy("mLock")
196    private MediaControllerCompat mControllerCompat;
197    @GuardedBy("mLock")
198    private ControllerCompatCallback mControllerCompatCallback;
199    @GuardedBy("mLock")
200    private PlaybackStateCompat mPlaybackStateCompat;
201    @GuardedBy("mLock")
202    private MediaMetadataCompat mMediaMetadataCompat;
203
204    // Assignment should be used with the lock hold, but should be used without a lock to prevent
205    // potential deadlock.
206    @GuardedBy("mLock")
207    private volatile boolean mConnected;
208
209    MediaController2ImplBase(@NonNull Context context, @NonNull SessionToken2 token,
210            @NonNull Executor executor, @NonNull ControllerCallback callback) {
211        super();
212        if (context == null) {
213            throw new IllegalArgumentException("context shouldn't be null");
214        }
215        if (token == null) {
216            throw new IllegalArgumentException("token shouldn't be null");
217        }
218        if (callback == null) {
219            throw new IllegalArgumentException("callback shouldn't be null");
220        }
221        if (executor == null) {
222            throw new IllegalArgumentException("executor shouldn't be null");
223        }
224        mContext = context;
225        mHandlerThread = new HandlerThread("MediaController2_Thread");
226        mHandlerThread.start();
227        mHandler = new Handler(mHandlerThread.getLooper());
228        mToken = token;
229        mCallback = callback;
230        mCallbackExecutor = executor;
231        mDeathRecipient = new IBinder.DeathRecipient() {
232            @Override
233            public void binderDied() {
234                MediaController2ImplBase.this.close();
235            }
236        };
237
238        initialize();
239    }
240
241    @Override
242    public void setInstance(MediaController2 controller) {
243        mInstance = controller;
244    }
245
246    @Override
247    public void close() {
248        if (DEBUG) {
249            //Log.d(TAG, "release from " + mToken, new IllegalStateException());
250        }
251        synchronized (mLock) {
252            if (mIsReleased) {
253                // Prevent re-enterance from the ControllerCallback.onDisconnected()
254                return;
255            }
256            mHandler.removeCallbacksAndMessages(null);
257
258            if (Build.VERSION.SDK_INT >= 18) {
259                mHandlerThread.quitSafely();
260            } else {
261                mHandlerThread.quit();
262            }
263
264            mIsReleased = true;
265
266            // Send command before the unregister callback to use mIControllerCallback in the
267            // callback.
268            sendCommand(CONTROLLER_COMMAND_DISCONNECT);
269            if (mControllerCompat != null) {
270                mControllerCompat.unregisterCallback(mControllerCompatCallback);
271            }
272            if (mBrowserCompat != null) {
273                mBrowserCompat.disconnect();
274                mBrowserCompat = null;
275            }
276            if (mControllerCompat != null) {
277                mControllerCompat.unregisterCallback(mControllerCompatCallback);
278                mControllerCompat = null;
279            }
280            mConnected = false;
281        }
282        mCallbackExecutor.execute(new Runnable() {
283            @Override
284            public void run() {
285                mCallback.onDisconnected(mInstance);
286            }
287        });
288    }
289
290    @Override
291    public @NonNull SessionToken2 getSessionToken() {
292        return mToken;
293    }
294
295    @Override
296    public boolean isConnected() {
297        synchronized (mLock) {
298            return mConnected;
299        }
300    }
301
302    @Override
303    public void play() {
304        synchronized (mLock) {
305            if (!mConnected) {
306                Log.w(TAG, "Session isn't active", new IllegalStateException());
307                return;
308            }
309            sendCommand(COMMAND_CODE_PLAYBACK_PLAY);
310        }
311    }
312
313    @Override
314    public void pause() {
315        synchronized (mLock) {
316            if (!mConnected) {
317                Log.w(TAG, "Session isn't active", new IllegalStateException());
318                return;
319            }
320            sendCommand(COMMAND_CODE_PLAYBACK_PAUSE);
321        }
322    }
323
324    @Override
325    public void reset() {
326        synchronized (mLock) {
327            if (!mConnected) {
328                Log.w(TAG, "Session isn't active", new IllegalStateException());
329                return;
330            }
331            sendCommand(COMMAND_CODE_PLAYBACK_RESET);
332        }
333    }
334
335    @Override
336    public void prepare() {
337        synchronized (mLock) {
338            if (!mConnected) {
339                Log.w(TAG, "Session isn't active", new IllegalStateException());
340                return;
341            }
342            sendCommand(COMMAND_CODE_PLAYBACK_PREPARE);
343        }
344    }
345
346    @Override
347    public void fastForward() {
348        synchronized (mLock) {
349            if (!mConnected) {
350                Log.w(TAG, "Session isn't active", new IllegalStateException());
351                return;
352            }
353            sendCommand(COMMAND_CODE_SESSION_FAST_FORWARD);
354        }
355    }
356
357    @Override
358    public void rewind() {
359        synchronized (mLock) {
360            if (!mConnected) {
361                Log.w(TAG, "Session isn't active", new IllegalStateException());
362                return;
363            }
364            sendCommand(COMMAND_CODE_SESSION_REWIND);
365        }
366    }
367
368    @Override
369    public void seekTo(long pos) {
370        synchronized (mLock) {
371            if (!mConnected) {
372                Log.w(TAG, "Session isn't active", new IllegalStateException());
373                return;
374            }
375            Bundle args = new Bundle();
376            args.putLong(ARGUMENT_SEEK_POSITION, pos);
377            sendCommand(COMMAND_CODE_PLAYBACK_SEEK_TO, args);
378        }
379    }
380
381    @Override
382    public void skipForward() {
383        // To match with KEYCODE_MEDIA_SKIP_FORWARD
384    }
385
386    @Override
387    public void skipBackward() {
388        // To match with KEYCODE_MEDIA_SKIP_BACKWARD
389    }
390
391    @Override
392    public void playFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
393        synchronized (mLock) {
394            if (!mConnected) {
395                Log.w(TAG, "Session isn't active", new IllegalStateException());
396                return;
397            }
398            Bundle args = new Bundle();
399            args.putString(ARGUMENT_MEDIA_ID, mediaId);
400            args.putBundle(ARGUMENT_EXTRAS, extras);
401            sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID, args);
402        }
403    }
404
405    @Override
406    public void playFromSearch(@NonNull String query, @Nullable Bundle extras) {
407        synchronized (mLock) {
408            if (!mConnected) {
409                Log.w(TAG, "Session isn't active", new IllegalStateException());
410                return;
411            }
412            Bundle args = new Bundle();
413            args.putString(ARGUMENT_QUERY, query);
414            args.putBundle(ARGUMENT_EXTRAS, extras);
415            sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_SEARCH, args);
416        }
417    }
418
419    @Override
420    public void playFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
421        synchronized (mLock) {
422            if (!mConnected) {
423                Log.w(TAG, "Session isn't active", new IllegalStateException());
424                return;
425            }
426            Bundle args = new Bundle();
427            args.putParcelable(ARGUMENT_URI, uri);
428            args.putBundle(ARGUMENT_EXTRAS, extras);
429            sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_URI, args);
430        }
431    }
432
433    @Override
434    public void prepareFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
435        synchronized (mLock) {
436            if (!mConnected) {
437                Log.w(TAG, "Session isn't active", new IllegalStateException());
438                return;
439            }
440            Bundle args = new Bundle();
441            args.putString(ARGUMENT_MEDIA_ID, mediaId);
442            args.putBundle(ARGUMENT_EXTRAS, extras);
443            sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID, args);
444        }
445    }
446
447    @Override
448    public void prepareFromSearch(@NonNull String query, @Nullable Bundle extras) {
449        synchronized (mLock) {
450            if (!mConnected) {
451                Log.w(TAG, "Session isn't active", new IllegalStateException());
452                return;
453            }
454            Bundle args = new Bundle();
455            args.putString(ARGUMENT_QUERY, query);
456            args.putBundle(ARGUMENT_EXTRAS, extras);
457            sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH, args);
458        }
459    }
460
461    @Override
462    public void prepareFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
463        synchronized (mLock) {
464            if (!mConnected) {
465                Log.w(TAG, "Session isn't active", new IllegalStateException());
466                return;
467            }
468            Bundle args = new Bundle();
469            args.putParcelable(ARGUMENT_URI, uri);
470            args.putBundle(ARGUMENT_EXTRAS, extras);
471            sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_URI, args);
472        }
473    }
474
475    @Override
476    public void setVolumeTo(int value, @VolumeFlags int flags) {
477        synchronized (mLock) {
478            if (!mConnected) {
479                Log.w(TAG, "Session isn't active", new IllegalStateException());
480                return;
481            }
482            Bundle args = new Bundle();
483            args.putInt(ARGUMENT_VOLUME, value);
484            args.putInt(ARGUMENT_VOLUME_FLAGS, flags);
485            sendCommand(COMMAND_CODE_VOLUME_SET_VOLUME, args);
486        }
487    }
488
489    @Override
490    public void adjustVolume(@VolumeDirection int direction, @VolumeFlags int flags) {
491        synchronized (mLock) {
492            if (!mConnected) {
493                Log.w(TAG, "Session isn't active", new IllegalStateException());
494                return;
495            }
496            Bundle args = new Bundle();
497            args.putInt(ARGUMENT_VOLUME_DIRECTION, direction);
498            args.putInt(ARGUMENT_VOLUME_FLAGS, flags);
499            sendCommand(COMMAND_CODE_VOLUME_ADJUST_VOLUME, args);
500        }
501    }
502
503    @Override
504    public @Nullable PendingIntent getSessionActivity() {
505        synchronized (mLock) {
506            if (!mConnected) {
507                Log.w(TAG, "Session isn't active", new IllegalStateException());
508                return null;
509            }
510            return mControllerCompat.getSessionActivity();
511        }
512    }
513
514    @Override
515    public int getPlayerState() {
516        synchronized (mLock) {
517            return mPlayerState;
518        }
519    }
520
521    @Override
522    public long getDuration() {
523        synchronized (mLock) {
524            if (mMediaMetadataCompat != null
525                    && mMediaMetadataCompat.containsKey(METADATA_KEY_DURATION)) {
526                return mMediaMetadataCompat.getLong(METADATA_KEY_DURATION);
527            }
528        }
529        return MediaPlayerInterface.UNKNOWN_TIME;
530    }
531
532    @Override
533    public long getCurrentPosition() {
534        synchronized (mLock) {
535            if (!mConnected) {
536                Log.w(TAG, "Session isn't active", new IllegalStateException());
537                return UNKNOWN_TIME;
538            }
539            if (mPlaybackStateCompat != null) {
540                long timeDiff = (mInstance.mTimeDiff != null) ? mInstance.mTimeDiff
541                        : SystemClock.elapsedRealtime()
542                                - mPlaybackStateCompat.getLastPositionUpdateTime();
543                long expectedPosition = mPlaybackStateCompat.getPosition()
544                        + (long) (mPlaybackStateCompat.getPlaybackSpeed() * timeDiff);
545                return Math.max(0, expectedPosition);
546            }
547            return UNKNOWN_TIME;
548        }
549    }
550
551    @Override
552    public float getPlaybackSpeed() {
553        synchronized (mLock) {
554            if (!mConnected) {
555                Log.w(TAG, "Session isn't active", new IllegalStateException());
556                return 0f;
557            }
558            return (mPlaybackStateCompat == null) ? 0f : mPlaybackStateCompat.getPlaybackSpeed();
559        }
560    }
561
562    @Override
563    public void setPlaybackSpeed(float speed) {
564        synchronized (mLock) {
565            if (!mConnected) {
566                Log.w(TAG, "Session isn't active", new IllegalStateException());
567                return;
568            }
569            Bundle args = new Bundle();
570            args.putFloat(ARGUMENT_PLAYBACK_SPEED, speed);
571            sendCommand(COMMAND_CODE_PLAYBACK_SET_SPEED, args);
572        }
573    }
574
575    @Override
576    public @MediaPlayerInterface.BuffState int getBufferingState() {
577        synchronized (mLock) {
578            if (!mConnected) {
579                Log.w(TAG, "Session isn't active", new IllegalStateException());
580                return BUFFERING_STATE_UNKNOWN;
581            }
582            return mBufferingState;
583        }
584    }
585
586    @Override
587    public long getBufferedPosition() {
588        synchronized (mLock) {
589            if (!mConnected) {
590                Log.w(TAG, "Session isn't active", new IllegalStateException());
591                return UNKNOWN_TIME;
592            }
593            return (mPlaybackStateCompat == null) ? UNKNOWN_TIME
594                    : mPlaybackStateCompat.getBufferedPosition();
595        }
596    }
597
598    @Override
599    public @Nullable PlaybackInfo getPlaybackInfo() {
600        synchronized (mLock) {
601            return mPlaybackInfo;
602        }
603    }
604
605    @Override
606    public void setRating(@NonNull String mediaId, @NonNull Rating2 rating) {
607        synchronized (mLock) {
608            if (!mConnected) {
609                Log.w(TAG, "Session isn't active", new IllegalStateException());
610                return;
611            }
612            Bundle args = new Bundle();
613            args.putString(ARGUMENT_MEDIA_ID, mediaId);
614            args.putBundle(ARGUMENT_RATING, rating.toBundle());
615            sendCommand(COMMAND_CODE_SESSION_SET_RATING, args);
616        }
617    }
618
619    @Override
620    public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args,
621            @Nullable ResultReceiver cb) {
622        synchronized (mLock) {
623            if (!mConnected) {
624                Log.w(TAG, "Session isn't active", new IllegalStateException());
625                return;
626            }
627            Bundle bundle = new Bundle();
628            bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle());
629            bundle.putBundle(ARGUMENT_ARGUMENTS, args);
630            sendCommand(CONTROLLER_COMMAND_BY_CUSTOM_COMMAND, bundle, cb);
631        }
632    }
633
634    @Override
635    public @Nullable List<MediaItem2> getPlaylist() {
636        synchronized (mLock) {
637            return mPlaylist;
638        }
639    }
640
641    @Override
642    public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
643        if (list == null) {
644            throw new IllegalArgumentException("list shouldn't be null");
645        }
646        Bundle args = new Bundle();
647        args.putParcelableArray(ARGUMENT_PLAYLIST, MediaUtils2.toMediaItem2ParcelableArray(list));
648        args.putBundle(ARGUMENT_PLAYLIST_METADATA, metadata == null ? null : metadata.toBundle());
649        sendCommand(COMMAND_CODE_PLAYLIST_SET_LIST, args);
650    }
651
652    @Override
653    public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
654        Bundle args = new Bundle();
655        args.putBundle(ARGUMENT_PLAYLIST_METADATA, metadata == null ? null : metadata.toBundle());
656        sendCommand(COMMAND_CODE_PLAYLIST_SET_LIST_METADATA, args);
657    }
658
659    @Override
660    public @Nullable MediaMetadata2 getPlaylistMetadata() {
661        synchronized (mLock) {
662            return mPlaylistMetadata;
663        }
664    }
665
666    @Override
667    public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
668        Bundle args = new Bundle();
669        args.putInt(ARGUMENT_PLAYLIST_INDEX, index);
670        args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
671        sendCommand(COMMAND_CODE_PLAYLIST_ADD_ITEM, args);
672    }
673
674    @Override
675    public void removePlaylistItem(@NonNull MediaItem2 item) {
676        Bundle args = new Bundle();
677        args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
678        sendCommand(COMMAND_CODE_PLAYLIST_REMOVE_ITEM, args);
679    }
680
681    @Override
682    public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
683        Bundle args = new Bundle();
684        args.putInt(ARGUMENT_PLAYLIST_INDEX, index);
685        args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
686        sendCommand(COMMAND_CODE_PLAYLIST_REPLACE_ITEM, args);
687    }
688
689    @Override
690    public MediaItem2 getCurrentMediaItem() {
691        synchronized (mLock) {
692            return mCurrentMediaItem;
693        }
694    }
695
696    @Override
697    public void skipToPreviousItem() {
698        sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM);
699    }
700
701    @Override
702    public void skipToNextItem() {
703        sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM);
704    }
705
706    @Override
707    public void skipToPlaylistItem(@NonNull MediaItem2 item) {
708        Bundle args = new Bundle();
709        args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
710        sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM, args);
711    }
712
713    @Override
714    public @RepeatMode int getRepeatMode() {
715        synchronized (mLock) {
716            return mRepeatMode;
717        }
718    }
719
720    @Override
721    public void setRepeatMode(@RepeatMode int repeatMode) {
722        Bundle args = new Bundle();
723        args.putInt(ARGUMENT_REPEAT_MODE, repeatMode);
724        sendCommand(COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE, args);
725    }
726
727    @Override
728    public @ShuffleMode int getShuffleMode() {
729        synchronized (mLock) {
730            return mShuffleMode;
731        }
732    }
733
734    @Override
735    public void setShuffleMode(@ShuffleMode int shuffleMode) {
736        Bundle args = new Bundle();
737        args.putInt(ARGUMENT_SHUFFLE_MODE, shuffleMode);
738        sendCommand(COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE, args);
739    }
740
741    @Override
742    public void subscribeRoutesInfo() {
743        sendCommand(COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO);
744    }
745
746    @Override
747    public void unsubscribeRoutesInfo() {
748        sendCommand(COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO);
749    }
750
751    @Override
752    public void selectRoute(@NonNull Bundle route) {
753        if (route == null) {
754            throw new IllegalArgumentException("route shouldn't be null");
755        }
756        Bundle args = new Bundle();
757        args.putBundle(ARGUMENT_ROUTE_BUNDLE, route);
758        sendCommand(COMMAND_CODE_SESSION_SELECT_ROUTE, args);
759    }
760
761    @Override
762    public @NonNull Context getContext() {
763        return mContext;
764    }
765
766    @Override
767    public @NonNull ControllerCallback getCallback() {
768        return mCallback;
769    }
770
771    @Override
772    public @NonNull Executor getCallbackExecutor() {
773        return mCallbackExecutor;
774    }
775
776    @Override
777    public @Nullable MediaBrowserCompat getBrowserCompat() {
778        synchronized (mLock) {
779            return mBrowserCompat;
780        }
781    }
782
783    // Should be used without a lock to prevent potential deadlock.
784    void onConnectedNotLocked(Bundle data) {
785        data.setClassLoader(MediaSession2.class.getClassLoader());
786        // is enough or should we pass it while connecting?
787        final SessionCommandGroup2 allowedCommands = SessionCommandGroup2.fromBundle(
788                data.getBundle(ARGUMENT_ALLOWED_COMMANDS));
789        final int playerState = data.getInt(ARGUMENT_PLAYER_STATE);
790        final int bufferingState = data.getInt(ARGUMENT_BUFFERING_STATE);
791        final PlaybackStateCompat playbackStateCompat = data.getParcelable(
792                ARGUMENT_PLAYBACK_STATE_COMPAT);
793        final int repeatMode = data.getInt(ARGUMENT_REPEAT_MODE);
794        final int shuffleMode = data.getInt(ARGUMENT_SHUFFLE_MODE);
795        final List<MediaItem2> playlist = MediaUtils2.fromMediaItem2ParcelableArray(
796                data.getParcelableArray(ARGUMENT_PLAYLIST));
797        final MediaItem2 currentMediaItem = MediaItem2.fromBundle(
798                data.getBundle(ARGUMENT_MEDIA_ITEM));
799        final PlaybackInfo playbackInfo =
800                PlaybackInfo.fromBundle(data.getBundle(ARGUMENT_PLAYBACK_INFO));
801        final MediaMetadata2 metadata = MediaMetadata2.fromBundle(
802                data.getBundle(ARGUMENT_PLAYLIST_METADATA));
803        if (DEBUG) {
804            Log.d(TAG, "onConnectedNotLocked sessionCompatToken=" + mToken.getSessionCompatToken()
805                    + ", allowedCommands=" + allowedCommands);
806        }
807        boolean close = false;
808        try {
809            synchronized (mLock) {
810                if (mIsReleased) {
811                    return;
812                }
813                if (mConnected) {
814                    Log.e(TAG, "Cannot be notified about the connection result many times."
815                            + " Probably a bug or malicious app.");
816                    close = true;
817                    return;
818                }
819                mAllowedCommands = allowedCommands;
820                mPlayerState = playerState;
821                mBufferingState = bufferingState;
822                mPlaybackStateCompat = playbackStateCompat;
823                mRepeatMode = repeatMode;
824                mShuffleMode = shuffleMode;
825                mPlaylist = playlist;
826                mCurrentMediaItem = currentMediaItem;
827                mPlaylistMetadata = metadata;
828                mConnected = true;
829                mPlaybackInfo = playbackInfo;
830            }
831            mCallbackExecutor.execute(new Runnable() {
832                @Override
833                public void run() {
834                    // Note: We may trigger ControllerCallbacks with the initial values
835                    // But it's hard to define the order of the controller callbacks
836                    // Only notify about the
837                    mCallback.onConnected(mInstance, allowedCommands);
838                }
839            });
840        } finally {
841            if (close) {
842                // Trick to call release() without holding the lock, to prevent potential deadlock
843                // with the developer's custom lock within the ControllerCallback.onDisconnected().
844                close();
845            }
846        }
847    }
848
849    private void initialize() {
850        if (mToken.getType() == SessionToken2.TYPE_SESSION) {
851            synchronized (mLock) {
852                mBrowserCompat = null;
853            }
854            connectToSession(mToken.getSessionCompatToken());
855        } else {
856            connectToService();
857        }
858    }
859
860    private void connectToSession(MediaSessionCompat.Token sessionCompatToken) {
861        MediaControllerCompat controllerCompat = null;
862        try {
863            controllerCompat = new MediaControllerCompat(mContext, sessionCompatToken);
864        } catch (RemoteException e) {
865            e.printStackTrace();
866        }
867        synchronized (mLock) {
868            mControllerCompat = controllerCompat;
869            mControllerCompatCallback = new ControllerCompatCallback();
870            mControllerCompat.registerCallback(mControllerCompatCallback, mHandler);
871        }
872
873        if (controllerCompat.isSessionReady()) {
874            sendCommand(CONTROLLER_COMMAND_CONNECT, new ResultReceiver(mHandler) {
875                @Override
876                protected void onReceiveResult(int resultCode, Bundle resultData) {
877                    if (!mHandlerThread.isAlive()) {
878                        return;
879                    }
880                    switch (resultCode) {
881                        case CONNECT_RESULT_CONNECTED:
882                            onConnectedNotLocked(resultData);
883                            break;
884                        case CONNECT_RESULT_DISCONNECTED:
885                            mCallbackExecutor.execute(new Runnable() {
886                                @Override
887                                public void run() {
888                                    mCallback.onDisconnected(mInstance);
889                                }
890                            });
891                            close();
892                            break;
893                    }
894                }
895            });
896        }
897    }
898
899    private void connectToService() {
900        mCallbackExecutor.execute(new Runnable() {
901            @Override
902            public void run() {
903                synchronized (mLock) {
904                    mBrowserCompat = new MediaBrowserCompat(mContext, mToken.getComponentName(),
905                            new ConnectionCallback(), sDefaultRootExtras);
906                    mBrowserCompat.connect();
907                }
908            }
909        });
910    }
911
912    private void sendCommand(int commandCode) {
913        sendCommand(commandCode, null);
914    }
915
916    private void sendCommand(int commandCode, Bundle args) {
917        if (args == null) {
918            args = new Bundle();
919        }
920        args.putInt(ARGUMENT_COMMAND_CODE, commandCode);
921        sendCommand(CONTROLLER_COMMAND_BY_COMMAND_CODE, args, null);
922    }
923
924    private void sendCommand(String command) {
925        sendCommand(command, null, null);
926    }
927
928    private void sendCommand(String command, ResultReceiver receiver) {
929        sendCommand(command, null, receiver);
930    }
931
932    private void sendCommand(String command, Bundle args, ResultReceiver receiver) {
933        if (args == null) {
934            args = new Bundle();
935        }
936        MediaControllerCompat controller;
937        ControllerCompatCallback callback;
938        synchronized (mLock) {
939            controller = mControllerCompat;
940            callback = mControllerCompatCallback;
941        }
942        BundleCompat.putBinder(args, ARGUMENT_ICONTROLLER_CALLBACK,
943                callback.getIControllerCallback().asBinder());
944        args.putString(ARGUMENT_PACKAGE_NAME, mContext.getPackageName());
945        args.putInt(ARGUMENT_UID, Process.myUid());
946        args.putInt(ARGUMENT_PID, Process.myPid());
947        controller.sendCommand(command, args, receiver);
948    }
949
950    private class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
951        @Override
952        public void onConnected() {
953            MediaBrowserCompat browser = getBrowserCompat();
954            if (browser != null) {
955                connectToSession(browser.getSessionToken());
956            } else if (DEBUG) {
957                Log.d(TAG, "Controller is closed prematually", new IllegalStateException());
958            }
959        }
960
961        @Override
962        public void onConnectionSuspended() {
963            close();
964        }
965
966        @Override
967        public void onConnectionFailed() {
968            close();
969        }
970    }
971
972    private final class ControllerCompatCallback extends MediaControllerCompat.Callback {
973        @Override
974        public void onSessionReady() {
975            sendCommand(CONTROLLER_COMMAND_CONNECT, new ResultReceiver(mHandler) {
976                @Override
977                protected void onReceiveResult(int resultCode, Bundle resultData) {
978                    if (!mHandlerThread.isAlive()) {
979                        return;
980                    }
981                    switch (resultCode) {
982                        case CONNECT_RESULT_CONNECTED:
983                            onConnectedNotLocked(resultData);
984                            break;
985                        case CONNECT_RESULT_DISCONNECTED:
986                            mCallbackExecutor.execute(new Runnable() {
987                                @Override
988                                public void run() {
989                                    mCallback.onDisconnected(mInstance);
990                                }
991                            });
992                            close();
993                            break;
994                    }
995                }
996            });
997        }
998
999        @Override
1000        public void onSessionDestroyed() {
1001            close();
1002        }
1003
1004        @Override
1005        public void onPlaybackStateChanged(PlaybackStateCompat state) {
1006            synchronized (mLock) {
1007                mPlaybackStateCompat = state;
1008            }
1009        }
1010
1011        @Override
1012        public void onMetadataChanged(MediaMetadataCompat metadata) {
1013            synchronized (mLock) {
1014                mMediaMetadataCompat = metadata;
1015            }
1016        }
1017
1018        @Override
1019        public void onSessionEvent(String event, Bundle extras) {
1020            if (extras != null) {
1021                extras.setClassLoader(MediaSession2.class.getClassLoader());
1022            }
1023            switch (event) {
1024                case SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED: {
1025                    final SessionCommandGroup2 allowedCommands = SessionCommandGroup2.fromBundle(
1026                            extras.getBundle(ARGUMENT_ALLOWED_COMMANDS));
1027                    synchronized (mLock) {
1028                        mAllowedCommands = allowedCommands;
1029                    }
1030                    mCallbackExecutor.execute(new Runnable() {
1031                        @Override
1032                        public void run() {
1033                            mCallback.onAllowedCommandsChanged(mInstance, allowedCommands);
1034                        }
1035                    });
1036                    break;
1037                }
1038                case SESSION_EVENT_ON_PLAYER_STATE_CHANGED: {
1039                    final int playerState = extras.getInt(ARGUMENT_PLAYER_STATE);
1040                    PlaybackStateCompat state =
1041                            extras.getParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT);
1042                    if (state == null) {
1043                        return;
1044                    }
1045                    synchronized (mLock) {
1046                        mPlayerState = playerState;
1047                        mPlaybackStateCompat = state;
1048                    }
1049                    mCallbackExecutor.execute(new Runnable() {
1050                        @Override
1051                        public void run() {
1052                            mCallback.onPlayerStateChanged(mInstance, playerState);
1053                        }
1054                    });
1055                    break;
1056                }
1057                case SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED: {
1058                    final MediaItem2 item = MediaItem2.fromBundle(
1059                            extras.getBundle(ARGUMENT_MEDIA_ITEM));
1060                    synchronized (mLock) {
1061                        mCurrentMediaItem = item;
1062                    }
1063                    mCallbackExecutor.execute(new Runnable() {
1064                        @Override
1065                        public void run() {
1066                            mCallback.onCurrentMediaItemChanged(mInstance, item);
1067                        }
1068                    });
1069                    break;
1070                }
1071                case SESSION_EVENT_ON_ERROR: {
1072                    final int errorCode = extras.getInt(ARGUMENT_ERROR_CODE);
1073                    final Bundle errorExtras = extras.getBundle(ARGUMENT_EXTRAS);
1074                    mCallbackExecutor.execute(new Runnable() {
1075                        @Override
1076                        public void run() {
1077                            mCallback.onError(mInstance, errorCode, errorExtras);
1078                        }
1079                    });
1080                    break;
1081                }
1082                case SESSION_EVENT_ON_ROUTES_INFO_CHANGED: {
1083                    final List<Bundle> routes = MediaUtils2.toBundleList(
1084                            extras.getParcelableArray(ARGUMENT_ROUTE_BUNDLE));
1085                    mCallbackExecutor.execute(new Runnable() {
1086                        @Override
1087                        public void run() {
1088                            mCallback.onRoutesInfoChanged(mInstance, routes);
1089                        }
1090                    });
1091                    break;
1092                }
1093                case SESSION_EVENT_ON_PLAYLIST_CHANGED: {
1094                    final MediaMetadata2 playlistMetadata = MediaMetadata2.fromBundle(
1095                            extras.getBundle(ARGUMENT_PLAYLIST_METADATA));
1096                    final List<MediaItem2> playlist = MediaUtils2.fromMediaItem2ParcelableArray(
1097                            extras.getParcelableArray(ARGUMENT_PLAYLIST));
1098                    synchronized (mLock) {
1099                        mPlaylist = playlist;
1100                        mPlaylistMetadata = playlistMetadata;
1101                    }
1102                    mCallbackExecutor.execute(new Runnable() {
1103                        @Override
1104                        public void run() {
1105                            mCallback.onPlaylistChanged(mInstance, playlist, playlistMetadata);
1106                        }
1107                    });
1108                    break;
1109                }
1110                case SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED: {
1111                    final MediaMetadata2 playlistMetadata = MediaMetadata2.fromBundle(
1112                            extras.getBundle(ARGUMENT_PLAYLIST_METADATA));
1113                    synchronized (mLock) {
1114                        mPlaylistMetadata = playlistMetadata;
1115                    }
1116                    mCallbackExecutor.execute(new Runnable() {
1117                        @Override
1118                        public void run() {
1119                            mCallback.onPlaylistMetadataChanged(mInstance, playlistMetadata);
1120                        }
1121                    });
1122                    break;
1123                }
1124                case SESSION_EVENT_ON_REPEAT_MODE_CHANGED: {
1125                    final int repeatMode = extras.getInt(ARGUMENT_REPEAT_MODE);
1126                    synchronized (mLock) {
1127                        mRepeatMode = repeatMode;
1128                    }
1129                    mCallbackExecutor.execute(new Runnable() {
1130                        @Override
1131                        public void run() {
1132                            mCallback.onRepeatModeChanged(mInstance, repeatMode);
1133                        }
1134                    });
1135                    break;
1136                }
1137                case SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED: {
1138                    final int shuffleMode = extras.getInt(ARGUMENT_SHUFFLE_MODE);
1139                    synchronized (mLock) {
1140                        mShuffleMode = shuffleMode;
1141                    }
1142                    mCallbackExecutor.execute(new Runnable() {
1143                        @Override
1144                        public void run() {
1145                            mCallback.onShuffleModeChanged(mInstance, shuffleMode);
1146                        }
1147                    });
1148                    break;
1149                }
1150                case SESSION_EVENT_SEND_CUSTOM_COMMAND: {
1151                    Bundle commandBundle = extras.getBundle(ARGUMENT_CUSTOM_COMMAND);
1152                    if (commandBundle == null) {
1153                        return;
1154                    }
1155                    final SessionCommand2 command = SessionCommand2.fromBundle(commandBundle);
1156                    final Bundle args = extras.getBundle(ARGUMENT_ARGUMENTS);
1157                    final ResultReceiver receiver = extras.getParcelable(ARGUMENT_RESULT_RECEIVER);
1158                    mCallbackExecutor.execute(new Runnable() {
1159                        @Override
1160                        public void run() {
1161                            mCallback.onCustomCommand(mInstance, command, args, receiver);
1162                        }
1163                    });
1164                    break;
1165                }
1166                case SESSION_EVENT_SET_CUSTOM_LAYOUT: {
1167                    final List<CommandButton> layout = MediaUtils2.fromCommandButtonParcelableArray(
1168                            extras.getParcelableArray(ARGUMENT_COMMAND_BUTTONS));
1169                    if (layout == null) {
1170                        return;
1171                    }
1172                    mCallbackExecutor.execute(new Runnable() {
1173                        @Override
1174                        public void run() {
1175                            mCallback.onCustomLayoutChanged(mInstance, layout);
1176                        }
1177                    });
1178                    break;
1179                }
1180                case SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED: {
1181                    final PlaybackInfo info = PlaybackInfo.fromBundle(
1182                            extras.getBundle(ARGUMENT_PLAYBACK_INFO));
1183                    if (info == null) {
1184                        return;
1185                    }
1186                    synchronized (mLock) {
1187                        mPlaybackInfo = info;
1188                    }
1189                    mCallbackExecutor.execute(new Runnable() {
1190                        @Override
1191                        public void run() {
1192                            mCallback.onPlaybackInfoChanged(mInstance, info);
1193                        }
1194                    });
1195                    break;
1196                }
1197                case SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED: {
1198                    final PlaybackStateCompat state =
1199                            extras.getParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT);
1200                    if (state == null) {
1201                        return;
1202                    }
1203                    synchronized (mLock) {
1204                        mPlaybackStateCompat = state;
1205                    }
1206                    mCallbackExecutor.execute(new Runnable() {
1207                        @Override
1208                        public void run() {
1209                            mCallback.onPlaybackSpeedChanged(mInstance, state.getPlaybackSpeed());
1210                        }
1211                    });
1212                    break;
1213                }
1214                case SESSION_EVENT_ON_BUFFERING_STATE_CHANGED: {
1215                    final MediaItem2 item = MediaItem2.fromBundle(
1216                            extras.getBundle(ARGUMENT_MEDIA_ITEM));
1217                    final int bufferingState = extras.getInt(ARGUMENT_BUFFERING_STATE);
1218                    PlaybackStateCompat state =
1219                            extras.getParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT);
1220                    if (item == null || state == null) {
1221                        return;
1222                    }
1223                    synchronized (mLock) {
1224                        mBufferingState = bufferingState;
1225                        mPlaybackStateCompat = state;
1226                    }
1227                    mCallbackExecutor.execute(new Runnable() {
1228                        @Override
1229                        public void run() {
1230                            mCallback.onBufferingStateChanged(mInstance, item, bufferingState);
1231                        }
1232                    });
1233                    break;
1234                }
1235                case SESSION_EVENT_ON_SEEK_COMPLETED: {
1236                    final long position = extras.getLong(ARGUMENT_SEEK_POSITION);
1237                    PlaybackStateCompat state =
1238                            extras.getParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT);
1239                    if (state == null) {
1240                        return;
1241                    }
1242                    synchronized (mLock) {
1243                        mPlaybackStateCompat = state;
1244                    }
1245                    mCallbackExecutor.execute(new Runnable() {
1246                        @Override
1247                        public void run() {
1248                            mCallback.onSeekCompleted(mInstance, position);
1249                        }
1250                    });
1251                    break;
1252                }
1253                case SESSION_EVENT_ON_CHILDREN_CHANGED: {
1254                    String parentId = extras.getString(ARGUMENT_MEDIA_ID);
1255                    if (parentId == null || !(mInstance instanceof MediaBrowser2)) {
1256                        return;
1257                    }
1258                    int itemCount = extras.getInt(ARGUMENT_ITEM_COUNT, -1);
1259                    Bundle childrenExtras = extras.getBundle(ARGUMENT_EXTRAS);
1260                    ((MediaBrowser2.BrowserCallback) mCallback).onChildrenChanged(
1261                            (MediaBrowser2) mInstance, parentId, itemCount, childrenExtras);
1262                    break;
1263                }
1264                case SESSION_EVENT_ON_SEARCH_RESULT_CHANGED: {
1265                    final String query = extras.getString(ARGUMENT_QUERY);
1266                    if (query == null || !(mInstance instanceof MediaBrowser2)) {
1267                        return;
1268                    }
1269                    final int itemCount = extras.getInt(ARGUMENT_ITEM_COUNT, -1);
1270                    final Bundle searchExtras = extras.getBundle(ARGUMENT_EXTRAS);
1271                    mCallbackExecutor.execute(new Runnable() {
1272                        @Override
1273                        public void run() {
1274                            ((MediaBrowser2.BrowserCallback) mCallback).onSearchResultChanged(
1275                                    (MediaBrowser2) mInstance, query, itemCount, searchExtras);
1276                        }
1277                    });
1278                    break;
1279                }
1280            }
1281        }
1282    }
1283}
1284