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 
17 package androidx.media;
18 
19 import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION;
20 
21 import static androidx.media.MediaConstants2.ARGUMENT_ALLOWED_COMMANDS;
22 import static androidx.media.MediaConstants2.ARGUMENT_ARGUMENTS;
23 import static androidx.media.MediaConstants2.ARGUMENT_BUFFERING_STATE;
24 import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_BUTTONS;
25 import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_CODE;
26 import static androidx.media.MediaConstants2.ARGUMENT_CUSTOM_COMMAND;
27 import static androidx.media.MediaConstants2.ARGUMENT_ERROR_CODE;
28 import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS;
29 import static androidx.media.MediaConstants2.ARGUMENT_ICONTROLLER_CALLBACK;
30 import static androidx.media.MediaConstants2.ARGUMENT_ITEM_COUNT;
31 import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ID;
32 import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ITEM;
33 import static androidx.media.MediaConstants2.ARGUMENT_PACKAGE_NAME;
34 import static androidx.media.MediaConstants2.ARGUMENT_PID;
35 import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_INFO;
36 import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_SPEED;
37 import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_STATE_COMPAT;
38 import static androidx.media.MediaConstants2.ARGUMENT_PLAYER_STATE;
39 import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST;
40 import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_INDEX;
41 import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_METADATA;
42 import static androidx.media.MediaConstants2.ARGUMENT_QUERY;
43 import static androidx.media.MediaConstants2.ARGUMENT_RATING;
44 import static androidx.media.MediaConstants2.ARGUMENT_REPEAT_MODE;
45 import static androidx.media.MediaConstants2.ARGUMENT_RESULT_RECEIVER;
46 import static androidx.media.MediaConstants2.ARGUMENT_ROUTE_BUNDLE;
47 import static androidx.media.MediaConstants2.ARGUMENT_SEEK_POSITION;
48 import static androidx.media.MediaConstants2.ARGUMENT_SHUFFLE_MODE;
49 import static androidx.media.MediaConstants2.ARGUMENT_UID;
50 import static androidx.media.MediaConstants2.ARGUMENT_URI;
51 import static androidx.media.MediaConstants2.ARGUMENT_VOLUME;
52 import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_DIRECTION;
53 import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_FLAGS;
54 import static androidx.media.MediaConstants2.CONNECT_RESULT_CONNECTED;
55 import static androidx.media.MediaConstants2.CONNECT_RESULT_DISCONNECTED;
56 import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_COMMAND_CODE;
57 import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_CUSTOM_COMMAND;
58 import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_CONNECT;
59 import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_DISCONNECT;
60 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED;
61 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_BUFFERING_STATE_CHANGED;
62 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CHILDREN_CHANGED;
63 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED;
64 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ERROR;
65 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED;
66 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED;
67 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYER_STATE_CHANGED;
68 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_CHANGED;
69 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED;
70 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_REPEAT_MODE_CHANGED;
71 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ROUTES_INFO_CHANGED;
72 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SEARCH_RESULT_CHANGED;
73 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SEEK_COMPLETED;
74 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED;
75 import static androidx.media.MediaConstants2.SESSION_EVENT_SEND_CUSTOM_COMMAND;
76 import static androidx.media.MediaConstants2.SESSION_EVENT_SET_CUSTOM_LAYOUT;
77 import static androidx.media.MediaPlayerInterface.BUFFERING_STATE_UNKNOWN;
78 import static androidx.media.MediaPlayerInterface.UNKNOWN_TIME;
79 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE;
80 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY;
81 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE;
82 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_RESET;
83 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO;
84 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SET_SPEED;
85 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM;
86 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM;
87 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM;
88 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST;
89 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST_METADATA;
90 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE;
91 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE;
92 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM;
93 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM;
94 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM;
95 import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_FAST_FORWARD;
96 import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID;
97 import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH;
98 import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI;
99 import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID;
100 import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH;
101 import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI;
102 import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_REWIND;
103 import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SELECT_ROUTE;
104 import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SET_RATING;
105 import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO;
106 import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO;
107 import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_ADJUST_VOLUME;
108 import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_SET_VOLUME;
109 
110 import android.annotation.TargetApi;
111 import android.app.PendingIntent;
112 import android.content.Context;
113 import android.net.Uri;
114 import android.os.Build;
115 import android.os.Bundle;
116 import android.os.Handler;
117 import android.os.HandlerThread;
118 import android.os.IBinder;
119 import android.os.Process;
120 import android.os.RemoteException;
121 import android.os.ResultReceiver;
122 import android.os.SystemClock;
123 import android.support.v4.media.MediaBrowserCompat;
124 import android.support.v4.media.MediaMetadataCompat;
125 import android.support.v4.media.session.MediaControllerCompat;
126 import android.support.v4.media.session.MediaSessionCompat;
127 import android.support.v4.media.session.PlaybackStateCompat;
128 import android.util.Log;
129 
130 import androidx.annotation.GuardedBy;
131 import androidx.annotation.NonNull;
132 import androidx.annotation.Nullable;
133 import androidx.core.app.BundleCompat;
134 import androidx.media.MediaController2.ControllerCallback;
135 import androidx.media.MediaController2.PlaybackInfo;
136 import androidx.media.MediaController2.VolumeDirection;
137 import androidx.media.MediaController2.VolumeFlags;
138 import androidx.media.MediaPlaylistAgent.RepeatMode;
139 import androidx.media.MediaPlaylistAgent.ShuffleMode;
140 import androidx.media.MediaSession2.CommandButton;
141 
142 import java.util.List;
143 import java.util.concurrent.Executor;
144 
145 @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
146 class 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 {
sDefaultRootExtras.putBoolean(MediaConstants2.ROOT_EXTRA_DEFAULT, true)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 
MediaController2ImplBase(@onNull Context context, @NonNull SessionToken2 token, @NonNull Executor executor, @NonNull ControllerCallback callback)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
setInstance(MediaController2 controller)242     public void setInstance(MediaController2 controller) {
243         mInstance = controller;
244     }
245 
246     @Override
close()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
getSessionToken()291     public @NonNull SessionToken2 getSessionToken() {
292         return mToken;
293     }
294 
295     @Override
isConnected()296     public boolean isConnected() {
297         synchronized (mLock) {
298             return mConnected;
299         }
300     }
301 
302     @Override
play()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
pause()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
reset()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
prepare()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
fastForward()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
rewind()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
seekTo(long pos)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
skipForward()382     public void skipForward() {
383         // To match with KEYCODE_MEDIA_SKIP_FORWARD
384     }
385 
386     @Override
skipBackward()387     public void skipBackward() {
388         // To match with KEYCODE_MEDIA_SKIP_BACKWARD
389     }
390 
391     @Override
playFromMediaId(@onNull String mediaId, @Nullable Bundle extras)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
playFromSearch(@onNull String query, @Nullable Bundle extras)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
playFromUri(@onNull Uri uri, @Nullable Bundle extras)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
prepareFromMediaId(@onNull String mediaId, @Nullable Bundle extras)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
prepareFromSearch(@onNull String query, @Nullable Bundle extras)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
prepareFromUri(@onNull Uri uri, @Nullable Bundle extras)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
setVolumeTo(int value, @VolumeFlags int flags)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
adjustVolume(@olumeDirection int direction, @VolumeFlags int flags)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
getSessionActivity()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
getPlayerState()515     public int getPlayerState() {
516         synchronized (mLock) {
517             return mPlayerState;
518         }
519     }
520 
521     @Override
getDuration()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
getCurrentPosition()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
getPlaybackSpeed()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
setPlaybackSpeed(float speed)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
getBufferingState()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
getBufferedPosition()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
getPlaybackInfo()599     public @Nullable PlaybackInfo getPlaybackInfo() {
600         synchronized (mLock) {
601             return mPlaybackInfo;
602         }
603     }
604 
605     @Override
setRating(@onNull String mediaId, @NonNull Rating2 rating)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
sendCustomCommand(@onNull SessionCommand2 command, @Nullable Bundle args, @Nullable ResultReceiver cb)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
getPlaylist()635     public @Nullable List<MediaItem2> getPlaylist() {
636         synchronized (mLock) {
637             return mPlaylist;
638         }
639     }
640 
641     @Override
setPlaylist(@onNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata)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
updatePlaylistMetadata(@ullable MediaMetadata2 metadata)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
getPlaylistMetadata()660     public @Nullable MediaMetadata2 getPlaylistMetadata() {
661         synchronized (mLock) {
662             return mPlaylistMetadata;
663         }
664     }
665 
666     @Override
addPlaylistItem(int index, @NonNull MediaItem2 item)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
removePlaylistItem(@onNull MediaItem2 item)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
replacePlaylistItem(int index, @NonNull MediaItem2 item)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
getCurrentMediaItem()690     public MediaItem2 getCurrentMediaItem() {
691         synchronized (mLock) {
692             return mCurrentMediaItem;
693         }
694     }
695 
696     @Override
skipToPreviousItem()697     public void skipToPreviousItem() {
698         sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM);
699     }
700 
701     @Override
skipToNextItem()702     public void skipToNextItem() {
703         sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM);
704     }
705 
706     @Override
skipToPlaylistItem(@onNull MediaItem2 item)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
getRepeatMode()714     public @RepeatMode int getRepeatMode() {
715         synchronized (mLock) {
716             return mRepeatMode;
717         }
718     }
719 
720     @Override
setRepeatMode(@epeatMode int repeatMode)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
getShuffleMode()728     public @ShuffleMode int getShuffleMode() {
729         synchronized (mLock) {
730             return mShuffleMode;
731         }
732     }
733 
734     @Override
setShuffleMode(@huffleMode int shuffleMode)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
subscribeRoutesInfo()742     public void subscribeRoutesInfo() {
743         sendCommand(COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO);
744     }
745 
746     @Override
unsubscribeRoutesInfo()747     public void unsubscribeRoutesInfo() {
748         sendCommand(COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO);
749     }
750 
751     @Override
selectRoute(@onNull Bundle route)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
getContext()762     public @NonNull Context getContext() {
763         return mContext;
764     }
765 
766     @Override
getCallback()767     public @NonNull ControllerCallback getCallback() {
768         return mCallback;
769     }
770 
771     @Override
getCallbackExecutor()772     public @NonNull Executor getCallbackExecutor() {
773         return mCallbackExecutor;
774     }
775 
776     @Override
getBrowserCompat()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.
onConnectedNotLocked(Bundle data)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 
initialize()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 
connectToSession(MediaSessionCompat.Token sessionCompatToken)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 
connectToService()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 
sendCommand(int commandCode)912     private void sendCommand(int commandCode) {
913         sendCommand(commandCode, null);
914     }
915 
sendCommand(int commandCode, Bundle args)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 
sendCommand(String command)924     private void sendCommand(String command) {
925         sendCommand(command, null, null);
926     }
927 
sendCommand(String command, ResultReceiver receiver)928     private void sendCommand(String command, ResultReceiver receiver) {
929         sendCommand(command, null, receiver);
930     }
931 
sendCommand(String command, Bundle args, ResultReceiver receiver)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
onConnected()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
onConnectionSuspended()962         public void onConnectionSuspended() {
963             close();
964         }
965 
966         @Override
onConnectionFailed()967         public void onConnectionFailed() {
968             close();
969         }
970     }
971 
972     private final class ControllerCompatCallback extends MediaControllerCompat.Callback {
973         @Override
onSessionReady()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
onSessionDestroyed()1000         public void onSessionDestroyed() {
1001             close();
1002         }
1003 
1004         @Override
onPlaybackStateChanged(PlaybackStateCompat state)1005         public void onPlaybackStateChanged(PlaybackStateCompat state) {
1006             synchronized (mLock) {
1007                 mPlaybackStateCompat = state;
1008             }
1009         }
1010 
1011         @Override
onMetadataChanged(MediaMetadataCompat metadata)1012         public void onMetadataChanged(MediaMetadataCompat metadata) {
1013             synchronized (mLock) {
1014                 mMediaMetadataCompat = metadata;
1015             }
1016         }
1017 
1018         @Override
onSessionEvent(String event, Bundle extras)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