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 androidx.media.MediaPlayerInterface.BUFFERING_STATE_UNKNOWN;
20import static androidx.media.MediaSession2.ControllerCb;
21import static androidx.media.MediaSession2.ControllerInfo;
22import static androidx.media.MediaSession2.ErrorCode;
23import static androidx.media.MediaSession2.OnDataSourceMissingHelper;
24import static androidx.media.MediaSession2.SessionCallback;
25import static androidx.media.SessionToken2.TYPE_LIBRARY_SERVICE;
26import static androidx.media.SessionToken2.TYPE_SESSION;
27import static androidx.media.SessionToken2.TYPE_SESSION_SERVICE;
28
29import android.annotation.TargetApi;
30import android.app.PendingIntent;
31import android.content.Context;
32import android.content.Intent;
33import android.content.pm.PackageManager;
34import android.content.pm.ResolveInfo;
35import android.media.AudioFocusRequest;
36import android.media.AudioManager;
37import android.os.Build;
38import android.os.Bundle;
39import android.os.DeadObjectException;
40import android.os.Handler;
41import android.os.HandlerThread;
42import android.os.Process;
43import android.os.RemoteException;
44import android.os.ResultReceiver;
45import android.support.v4.media.session.MediaSessionCompat;
46import android.support.v4.media.session.PlaybackStateCompat;
47import android.text.TextUtils;
48import android.util.Log;
49
50import androidx.annotation.GuardedBy;
51import androidx.annotation.NonNull;
52import androidx.annotation.Nullable;
53import androidx.core.util.ObjectsCompat;
54import androidx.media.MediaController2.PlaybackInfo;
55import androidx.media.MediaPlayerInterface.PlayerEventCallback;
56import androidx.media.MediaPlaylistAgent.PlaylistEventCallback;
57
58import java.lang.ref.WeakReference;
59import java.util.List;
60import java.util.NoSuchElementException;
61import java.util.concurrent.Executor;
62import java.util.concurrent.RejectedExecutionException;
63
64@TargetApi(Build.VERSION_CODES.KITKAT)
65class MediaSession2ImplBase extends MediaSession2.SupportLibraryImpl {
66    static final String TAG = "MS2ImplBase";
67    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
68
69    private final Object mLock = new Object();
70
71    private final Context mContext;
72    private final HandlerThread mHandlerThread;
73    private final Handler mHandler;
74    private final MediaSessionCompat mSessionCompat;
75    private final MediaSession2Stub mSession2Stub;
76    private final MediaSessionLegacyStub mSessionLegacyStub;
77    private final String mId;
78    private final Executor mCallbackExecutor;
79    private final SessionCallback mCallback;
80    private final SessionToken2 mSessionToken;
81    private final AudioManager mAudioManager;
82    private final MediaPlayerInterface.PlayerEventCallback mPlayerEventCallback;
83    private final MediaPlaylistAgent.PlaylistEventCallback mPlaylistEventCallback;
84    private final MediaSession2 mInstance;
85
86    @GuardedBy("mLock")
87    private MediaPlayerInterface mPlayer;
88    @GuardedBy("mLock")
89    private MediaPlaylistAgent mPlaylistAgent;
90    @GuardedBy("mLock")
91    private SessionPlaylistAgentImplBase mSessionPlaylistAgent;
92    @GuardedBy("mLock")
93    private VolumeProviderCompat mVolumeProvider;
94    @GuardedBy("mLock")
95    private OnDataSourceMissingHelper mDsmHelper;
96    @GuardedBy("mLock")
97    private PlaybackInfo mPlaybackInfo;
98
99    MediaSession2ImplBase(Context context, MediaSessionCompat sessionCompat, String id,
100            MediaPlayerInterface player, MediaPlaylistAgent playlistAgent,
101            VolumeProviderCompat volumeProvider, PendingIntent sessionActivity,
102            Executor callbackExecutor, SessionCallback callback) {
103        mContext = context;
104        mInstance = createInstance();
105        mHandlerThread = new HandlerThread("MediaController2_Thread");
106        mHandlerThread.start();
107        mHandler = new Handler(mHandlerThread.getLooper());
108
109        mSessionCompat = sessionCompat;
110        mSession2Stub = new MediaSession2Stub(this);
111        mSessionLegacyStub = new MediaSessionLegacyStub(this);
112        mSessionCompat.setCallback(mSession2Stub, mHandler);
113        mSessionCompat.setSessionActivity(sessionActivity);
114
115        mId = id;
116        mCallback = callback;
117        mCallbackExecutor = callbackExecutor;
118        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
119
120        mPlayerEventCallback = new MyPlayerEventCallback(this);
121        mPlaylistEventCallback = new MyPlaylistEventCallback(this);
122
123        // Infer type from the id and package name.
124        String libraryService = getServiceName(context, MediaLibraryService2.SERVICE_INTERFACE, id);
125        String sessionService = getServiceName(context, MediaSessionService2.SERVICE_INTERFACE, id);
126        if (sessionService != null && libraryService != null) {
127            throw new IllegalArgumentException("Ambiguous session type. Multiple"
128                    + " session services define the same id=" + id);
129        } else if (libraryService != null) {
130            mSessionToken = new SessionToken2(Process.myUid(), TYPE_LIBRARY_SERVICE,
131                    context.getPackageName(), libraryService, id, mSessionCompat.getSessionToken());
132        } else if (sessionService != null) {
133            mSessionToken = new SessionToken2(Process.myUid(), TYPE_SESSION_SERVICE,
134                    context.getPackageName(), sessionService, id, mSessionCompat.getSessionToken());
135        } else {
136            mSessionToken = new SessionToken2(Process.myUid(), TYPE_SESSION,
137                    context.getPackageName(), null, id, mSessionCompat.getSessionToken());
138        }
139        updatePlayer(player, playlistAgent, volumeProvider);
140    }
141
142    @Override
143    public void updatePlayer(@NonNull MediaPlayerInterface player,
144            @Nullable MediaPlaylistAgent playlistAgent,
145            @Nullable VolumeProviderCompat volumeProvider) {
146        if (player == null) {
147            throw new IllegalArgumentException("player shouldn't be null");
148        }
149        final boolean hasPlayerChanged;
150        final boolean hasAgentChanged;
151        final boolean hasPlaybackInfoChanged;
152        final MediaPlayerInterface oldPlayer;
153        final MediaPlaylistAgent oldAgent;
154        final PlaybackInfo info = createPlaybackInfo(volumeProvider, player.getAudioAttributes());
155        synchronized (mLock) {
156            hasPlayerChanged = (mPlayer != player);
157            hasAgentChanged = (mPlaylistAgent != playlistAgent);
158            hasPlaybackInfoChanged = (mPlaybackInfo != info);
159            oldPlayer = mPlayer;
160            oldAgent = mPlaylistAgent;
161            mPlayer = player;
162            if (playlistAgent == null) {
163                mSessionPlaylistAgent = new SessionPlaylistAgentImplBase(this, mPlayer);
164                if (mDsmHelper != null) {
165                    mSessionPlaylistAgent.setOnDataSourceMissingHelper(mDsmHelper);
166                }
167                playlistAgent = mSessionPlaylistAgent;
168            }
169            mPlaylistAgent = playlistAgent;
170            mVolumeProvider = volumeProvider;
171            mPlaybackInfo = info;
172        }
173        if (volumeProvider == null) {
174            int stream = getLegacyStreamType(player.getAudioAttributes());
175            mSessionCompat.setPlaybackToLocal(stream);
176        }
177        if (player != oldPlayer) {
178            player.registerPlayerEventCallback(mCallbackExecutor, mPlayerEventCallback);
179            if (oldPlayer != null) {
180                // Warning: Poorly implement player may ignore this
181                oldPlayer.unregisterPlayerEventCallback(mPlayerEventCallback);
182            }
183        }
184        if (playlistAgent != oldAgent) {
185            playlistAgent.registerPlaylistEventCallback(mCallbackExecutor, mPlaylistEventCallback);
186            if (oldAgent != null) {
187                // Warning: Poorly implement agent may ignore this
188                oldAgent.unregisterPlaylistEventCallback(mPlaylistEventCallback);
189            }
190        }
191
192        if (oldPlayer != null) {
193            // If it's not the first updatePlayer(), tell changes in the player, agent, and playback
194            // info.
195            if (hasAgentChanged) {
196                // Update agent first. Otherwise current position may be changed off the current
197                // media item's duration, and controller may consider it as a bug.
198                notifyAgentUpdatedNotLocked(oldAgent);
199            }
200            if (hasPlayerChanged) {
201                notifyPlayerUpdatedNotLocked(oldPlayer);
202            }
203            if (hasPlaybackInfoChanged) {
204                // Currently hasPlaybackInfo is always true, but check this in case that we're
205                // adding PlaybackInfo#equals().
206                notifyToAllControllers(new NotifyRunnable() {
207                    @Override
208                    public void run(ControllerCb callback) throws RemoteException {
209                        callback.onPlaybackInfoChanged(info);
210                    }
211                });
212            }
213        }
214    }
215
216    private PlaybackInfo createPlaybackInfo(VolumeProviderCompat volumeProvider,
217            AudioAttributesCompat attrs) {
218        PlaybackInfo info;
219        if (volumeProvider == null) {
220            int stream = getLegacyStreamType(attrs);
221            int controlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
222            if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
223                controlType = VolumeProviderCompat.VOLUME_CONTROL_FIXED;
224            }
225            info = PlaybackInfo.createPlaybackInfo(
226                    PlaybackInfo.PLAYBACK_TYPE_LOCAL,
227                    attrs,
228                    controlType,
229                    mAudioManager.getStreamMaxVolume(stream),
230                    mAudioManager.getStreamVolume(stream));
231        } else {
232            info = PlaybackInfo.createPlaybackInfo(
233                    PlaybackInfo.PLAYBACK_TYPE_REMOTE,
234                    attrs,
235                    volumeProvider.getVolumeControl(),
236                    volumeProvider.getMaxVolume(),
237                    volumeProvider.getCurrentVolume());
238        }
239        return info;
240    }
241
242    private int getLegacyStreamType(@Nullable AudioAttributesCompat attrs) {
243        int stream;
244        if (attrs == null) {
245            stream = AudioManager.STREAM_MUSIC;
246        } else {
247            stream = attrs.getLegacyStreamType();
248            if (stream == AudioManager.USE_DEFAULT_STREAM_TYPE) {
249                // Usually, AudioAttributesCompat#getLegacyStreamType() does not return
250                // USE_DEFAULT_STREAM_TYPE unless the developer sets it with
251                // AudioAttributesCompat.Builder#setLegacyStreamType().
252                // But for safety, let's convert USE_DEFAULT_STREAM_TYPE to STREAM_MUSIC here.
253                stream = AudioManager.STREAM_MUSIC;
254            }
255        }
256        return stream;
257    }
258
259    @Override
260    public void close() {
261        synchronized (mLock) {
262            if (mPlayer == null) {
263                return;
264            }
265            mPlayer.unregisterPlayerEventCallback(mPlayerEventCallback);
266            mPlayer = null;
267            mSessionCompat.release();
268            mHandler.removeCallbacksAndMessages(null);
269            if (mHandlerThread.isAlive()) {
270                if (Build.VERSION.SDK_INT >= 18) {
271                    mHandlerThread.quitSafely();
272                } else {
273                    mHandlerThread.quit();
274                }
275            }
276        }
277    }
278
279    @Override
280    public @NonNull MediaPlayerInterface getPlayer() {
281        synchronized (mLock) {
282            return mPlayer;
283        }
284    }
285
286    @Override
287    public @NonNull MediaPlaylistAgent getPlaylistAgent() {
288        synchronized (mLock) {
289            return mPlaylistAgent;
290        }
291    }
292
293    @Override
294    public @Nullable VolumeProviderCompat getVolumeProvider() {
295        synchronized (mLock) {
296            return mVolumeProvider;
297        }
298    }
299
300    @Override
301    public @NonNull SessionToken2 getToken() {
302        return mSessionToken;
303    }
304
305    @Override
306    public @NonNull List<ControllerInfo> getConnectedControllers() {
307        return mSession2Stub.getConnectedControllers();
308    }
309
310    @Override
311    public void setAudioFocusRequest(@Nullable AudioFocusRequest afr) {
312    }
313
314    @Override
315    public void setCustomLayout(@NonNull ControllerInfo controller,
316            @NonNull final List<MediaSession2.CommandButton> layout) {
317        if (controller == null) {
318            throw new IllegalArgumentException("controller shouldn't be null");
319        }
320        if (layout == null) {
321            throw new IllegalArgumentException("layout shouldn't be null");
322        }
323        notifyToController(controller, new NotifyRunnable() {
324            @Override
325            public void run(ControllerCb callback) throws RemoteException {
326                callback.onCustomLayoutChanged(layout);
327            }
328        });
329    }
330
331    @Override
332    public void setAllowedCommands(@NonNull ControllerInfo controller,
333            @NonNull final SessionCommandGroup2 commands) {
334        if (controller == null) {
335            throw new IllegalArgumentException("controller shouldn't be null");
336        }
337        if (commands == null) {
338            throw new IllegalArgumentException("commands shouldn't be null");
339        }
340        mSession2Stub.setAllowedCommands(controller, commands);
341        notifyToController(controller, new NotifyRunnable() {
342            @Override
343            public void run(ControllerCb callback) throws RemoteException {
344                callback.onAllowedCommandsChanged(commands);
345            }
346        });
347    }
348
349    @Override
350    public void sendCustomCommand(@NonNull final SessionCommand2 command,
351            @Nullable final Bundle args) {
352        if (command == null) {
353            throw new IllegalArgumentException("command shouldn't be null");
354        }
355        notifyToAllControllers(new NotifyRunnable() {
356            @Override
357            public void run(ControllerCb callback) throws RemoteException {
358                callback.onCustomCommand(command, args, null);
359            }
360        });
361    }
362
363    @Override
364    public void sendCustomCommand(@NonNull ControllerInfo controller,
365            @NonNull final SessionCommand2 command, @Nullable final Bundle args,
366            @Nullable final ResultReceiver receiver) {
367        if (controller == null) {
368            throw new IllegalArgumentException("controller shouldn't be null");
369        }
370        if (command == null) {
371            throw new IllegalArgumentException("command shouldn't be null");
372        }
373        notifyToController(controller, new NotifyRunnable() {
374            @Override
375            public void run(ControllerCb callback) throws RemoteException {
376                callback.onCustomCommand(command, args, receiver);
377            }
378        });
379    }
380
381    @Override
382    public void play() {
383        MediaPlayerInterface player;
384        synchronized (mLock) {
385            player = mPlayer;
386        }
387        if (player != null) {
388            player.play();
389        } else if (DEBUG) {
390            Log.d(TAG, "API calls after the close()", new IllegalStateException());
391        }
392    }
393
394    @Override
395    public void pause() {
396        MediaPlayerInterface player;
397        synchronized (mLock) {
398            player = mPlayer;
399        }
400        if (player != null) {
401            player.pause();
402        } else if (DEBUG) {
403            Log.d(TAG, "API calls after the close()", new IllegalStateException());
404        }
405    }
406
407    @Override
408    public void reset() {
409        MediaPlayerInterface player;
410        synchronized (mLock) {
411            player = mPlayer;
412        }
413        if (player != null) {
414            player.reset();
415        } else if (DEBUG) {
416            Log.d(TAG, "API calls after the close()", new IllegalStateException());
417        }
418    }
419
420    @Override
421    public void prepare() {
422        MediaPlayerInterface player;
423        synchronized (mLock) {
424            player = mPlayer;
425        }
426        if (player != null) {
427            player.prepare();
428        } else if (DEBUG) {
429            Log.d(TAG, "API calls after the close()", new IllegalStateException());
430        }
431    }
432
433    @Override
434    public void seekTo(long pos) {
435        MediaPlayerInterface player;
436        synchronized (mLock) {
437            player = mPlayer;
438        }
439        if (player != null) {
440            player.seekTo(pos);
441        } else if (DEBUG) {
442            Log.d(TAG, "API calls after the close()", new IllegalStateException());
443        }
444    }
445
446    @Override
447    public void skipForward() {
448        // To match with KEYCODE_MEDIA_SKIP_FORWARD
449    }
450
451    @Override
452    public void skipBackward() {
453        // To match with KEYCODE_MEDIA_SKIP_BACKWARD
454    }
455
456    @Override
457    public void notifyError(@ErrorCode final int errorCode, @Nullable final Bundle extras) {
458        notifyToAllControllers(new NotifyRunnable() {
459            @Override
460            public void run(ControllerCb callback) throws RemoteException {
461                callback.onError(errorCode, extras);
462            }
463        });
464    }
465
466    @Override
467    public void notifyRoutesInfoChanged(@NonNull ControllerInfo controller,
468            @Nullable final List<Bundle> routes) {
469        notifyToController(controller, new NotifyRunnable() {
470            @Override
471            public void run(ControllerCb callback) throws RemoteException {
472                callback.onRoutesInfoChanged(routes);
473            }
474        });
475    }
476
477    @Override
478    public @MediaPlayerInterface.PlayerState int getPlayerState() {
479        MediaPlayerInterface player;
480        synchronized (mLock) {
481            player = mPlayer;
482        }
483        if (player != null) {
484            return player.getPlayerState();
485        } else if (DEBUG) {
486            Log.d(TAG, "API calls after the close()", new IllegalStateException());
487        }
488        return MediaPlayerInterface.PLAYER_STATE_ERROR;
489    }
490
491    @Override
492    public long getCurrentPosition() {
493        MediaPlayerInterface player;
494        synchronized (mLock) {
495            player = mPlayer;
496        }
497        if (player != null) {
498            return player.getCurrentPosition();
499        } else if (DEBUG) {
500            Log.d(TAG, "API calls after the close()", new IllegalStateException());
501        }
502        return MediaPlayerInterface.UNKNOWN_TIME;
503    }
504
505    @Override
506    public long getDuration() {
507        MediaPlayerInterface player;
508        synchronized (mLock) {
509            player = mPlayer;
510        }
511        if (player != null) {
512            // Note: This should be the same as
513            // getCurrentMediaItem().getMetadata().getLong(METADATA_KEY_DURATION)
514            return player.getDuration();
515        } else if (DEBUG) {
516            Log.d(TAG, "API calls after the close()", new IllegalStateException());
517        }
518        return MediaPlayerInterface.UNKNOWN_TIME;
519    }
520
521    @Override
522    public long getBufferedPosition() {
523        MediaPlayerInterface player;
524        synchronized (mLock) {
525            player = mPlayer;
526        }
527        if (player != null) {
528            return player.getBufferedPosition();
529        } else if (DEBUG) {
530            Log.d(TAG, "API calls after the close()", new IllegalStateException());
531        }
532        return MediaPlayerInterface.UNKNOWN_TIME;
533    }
534
535    @Override
536    public @MediaPlayerInterface.BuffState int getBufferingState() {
537        MediaPlayerInterface player;
538        synchronized (mLock) {
539            player = mPlayer;
540        }
541        if (player != null) {
542            return player.getBufferingState();
543        } else if (DEBUG) {
544            Log.d(TAG, "API calls after the close()", new IllegalStateException());
545        }
546        return BUFFERING_STATE_UNKNOWN;
547    }
548
549    @Override
550    public float getPlaybackSpeed() {
551        MediaPlayerInterface player;
552        synchronized (mLock) {
553            player = mPlayer;
554        }
555        if (player != null) {
556            return player.getPlaybackSpeed();
557        } else if (DEBUG) {
558            Log.d(TAG, "API calls after the close()", new IllegalStateException());
559        }
560        return 1.0f;
561    }
562
563    @Override
564    public void setPlaybackSpeed(float speed) {
565        MediaPlayerInterface player;
566        synchronized (mLock) {
567            player = mPlayer;
568        }
569        if (player != null) {
570            player.setPlaybackSpeed(speed);
571        } else if (DEBUG) {
572            Log.d(TAG, "API calls after the close()", new IllegalStateException());
573        }
574    }
575
576    @Override
577    public void setOnDataSourceMissingHelper(
578            @NonNull OnDataSourceMissingHelper helper) {
579        if (helper == null) {
580            throw new IllegalArgumentException("helper shouldn't be null");
581        }
582        synchronized (mLock) {
583            mDsmHelper = helper;
584            if (mSessionPlaylistAgent != null) {
585                mSessionPlaylistAgent.setOnDataSourceMissingHelper(helper);
586            }
587        }
588    }
589
590    @Override
591    public void clearOnDataSourceMissingHelper() {
592        synchronized (mLock) {
593            mDsmHelper = null;
594            if (mSessionPlaylistAgent != null) {
595                mSessionPlaylistAgent.clearOnDataSourceMissingHelper();
596            }
597        }
598    }
599
600    @Override
601    public List<MediaItem2> getPlaylist() {
602        MediaPlaylistAgent agent;
603        synchronized (mLock) {
604            agent = mPlaylistAgent;
605        }
606        if (agent != null) {
607            return agent.getPlaylist();
608        } else if (DEBUG) {
609            Log.d(TAG, "API calls after the close()", new IllegalStateException());
610        }
611        return null;
612    }
613
614    @Override
615    public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
616        if (list == null) {
617            throw new IllegalArgumentException("list shouldn't be null");
618        }
619        MediaPlaylistAgent agent;
620        synchronized (mLock) {
621            agent = mPlaylistAgent;
622        }
623        if (agent != null) {
624            agent.setPlaylist(list, metadata);
625        } else if (DEBUG) {
626            Log.d(TAG, "API calls after the close()", new IllegalStateException());
627        }
628    }
629
630    @Override
631    public void skipToPlaylistItem(@NonNull MediaItem2 item) {
632        if (item == null) {
633            throw new IllegalArgumentException("item shouldn't be null");
634        }
635        MediaPlaylistAgent agent;
636        synchronized (mLock) {
637            agent = mPlaylistAgent;
638        }
639        if (agent != null) {
640            agent.skipToPlaylistItem(item);
641        } else if (DEBUG) {
642            Log.d(TAG, "API calls after the close()", new IllegalStateException());
643        }
644    }
645
646    @Override
647    public void skipToPreviousItem() {
648        MediaPlaylistAgent agent;
649        synchronized (mLock) {
650            agent = mPlaylistAgent;
651        }
652        if (agent != null) {
653            agent.skipToPreviousItem();
654        } else if (DEBUG) {
655            Log.d(TAG, "API calls after the close()", new IllegalStateException());
656        }
657    }
658
659    @Override
660    public void skipToNextItem() {
661        MediaPlaylistAgent agent;
662        synchronized (mLock) {
663            agent = mPlaylistAgent;
664        }
665        if (agent != null) {
666            agent.skipToNextItem();
667        } else if (DEBUG) {
668            Log.d(TAG, "API calls after the close()", new IllegalStateException());
669        }
670    }
671
672    @Override
673    public MediaMetadata2 getPlaylistMetadata() {
674        MediaPlaylistAgent agent;
675        synchronized (mLock) {
676            agent = mPlaylistAgent;
677        }
678        if (agent != null) {
679            return agent.getPlaylistMetadata();
680        } else if (DEBUG) {
681            Log.d(TAG, "API calls after the close()", new IllegalStateException());
682        }
683        return null;
684    }
685
686    @Override
687    public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
688        if (index < 0) {
689            throw new IllegalArgumentException("index shouldn't be negative");
690        }
691        if (item == null) {
692            throw new IllegalArgumentException("item shouldn't be null");
693        }
694        MediaPlaylistAgent agent;
695        synchronized (mLock) {
696            agent = mPlaylistAgent;
697        }
698        if (agent != null) {
699            agent.addPlaylistItem(index, item);
700        } else if (DEBUG) {
701            Log.d(TAG, "API calls after the close()", new IllegalStateException());
702        }
703    }
704
705    @Override
706    public void removePlaylistItem(@NonNull MediaItem2 item) {
707        if (item == null) {
708            throw new IllegalArgumentException("item shouldn't be null");
709        }
710        MediaPlaylistAgent agent;
711        synchronized (mLock) {
712            agent = mPlaylistAgent;
713        }
714        if (agent != null) {
715            agent.removePlaylistItem(item);
716        } else if (DEBUG) {
717            Log.d(TAG, "API calls after the close()", new IllegalStateException());
718        }
719    }
720
721    @Override
722    public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
723        if (index < 0) {
724            throw new IllegalArgumentException("index shouldn't be negative");
725        }
726        if (item == null) {
727            throw new IllegalArgumentException("item shouldn't be null");
728        }
729        MediaPlaylistAgent agent;
730        synchronized (mLock) {
731            agent = mPlaylistAgent;
732        }
733        if (agent != null) {
734            agent.replacePlaylistItem(index, item);
735        } else if (DEBUG) {
736            Log.d(TAG, "API calls after the close()", new IllegalStateException());
737        }
738    }
739
740    @Override
741    public MediaItem2 getCurrentMediaItem() {
742        MediaPlaylistAgent agent;
743        synchronized (mLock) {
744            agent = mPlaylistAgent;
745        }
746        if (agent != null) {
747            return agent.getCurrentMediaItem();
748        } else if (DEBUG) {
749            Log.d(TAG, "API calls after the close()", new IllegalStateException());
750        }
751        return null;
752    }
753
754    @Override
755    public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
756        MediaPlaylistAgent agent;
757        synchronized (mLock) {
758            agent = mPlaylistAgent;
759        }
760        if (agent != null) {
761            agent.updatePlaylistMetadata(metadata);
762        } else if (DEBUG) {
763            Log.d(TAG, "API calls after the close()", new IllegalStateException());
764        }
765    }
766
767    @Override
768    public @MediaPlaylistAgent.RepeatMode int getRepeatMode() {
769        MediaPlaylistAgent agent;
770        synchronized (mLock) {
771            agent = mPlaylistAgent;
772        }
773        if (agent != null) {
774            return agent.getRepeatMode();
775        } else if (DEBUG) {
776            Log.d(TAG, "API calls after the close()", new IllegalStateException());
777        }
778        return MediaPlaylistAgent.REPEAT_MODE_NONE;
779    }
780
781    @Override
782    public void setRepeatMode(@MediaPlaylistAgent.RepeatMode int repeatMode) {
783        MediaPlaylistAgent agent;
784        synchronized (mLock) {
785            agent = mPlaylistAgent;
786        }
787        if (agent != null) {
788            agent.setRepeatMode(repeatMode);
789        } else if (DEBUG) {
790            Log.d(TAG, "API calls after the close()", new IllegalStateException());
791        }
792    }
793
794    @Override
795    public @MediaPlaylistAgent.ShuffleMode int getShuffleMode() {
796        MediaPlaylistAgent agent;
797        synchronized (mLock) {
798            agent = mPlaylistAgent;
799        }
800        if (agent != null) {
801            return agent.getShuffleMode();
802        } else if (DEBUG) {
803            Log.d(TAG, "API calls after the close()", new IllegalStateException());
804        }
805        return MediaPlaylistAgent.SHUFFLE_MODE_NONE;
806    }
807
808    @Override
809    public void setShuffleMode(int shuffleMode) {
810        MediaPlaylistAgent agent;
811        synchronized (mLock) {
812            agent = mPlaylistAgent;
813        }
814        if (agent != null) {
815            agent.setShuffleMode(shuffleMode);
816        } else if (DEBUG) {
817            Log.d(TAG, "API calls after the close()", new IllegalStateException());
818        }
819    }
820
821    ///////////////////////////////////////////////////
822    // LibrarySession Methods
823    ///////////////////////////////////////////////////
824
825    @Override
826    void notifyChildrenChanged(ControllerInfo controller, final String parentId,
827            final int itemCount, final Bundle extras,
828            List<MediaSessionManager.RemoteUserInfo> subscribingBrowsers) {
829        if (controller == null) {
830            throw new IllegalArgumentException("controller shouldn't be null");
831        }
832        if (TextUtils.isEmpty(parentId)) {
833            throw new IllegalArgumentException("query shouldn't be empty");
834        }
835
836        // Notify controller only if it has subscribed the parentId.
837        for (MediaSessionManager.RemoteUserInfo info : subscribingBrowsers) {
838            if (info.getPackageName().equals(controller.getPackageName())
839                    && info.getUid() == controller.getUid()) {
840                notifyToController(controller, new NotifyRunnable() {
841                    @Override
842                    public void run(ControllerCb callback) throws RemoteException {
843                        callback.onChildrenChanged(parentId, itemCount, extras);
844                    }
845                });
846                return;
847            }
848        }
849    }
850
851    @Override
852    void notifySearchResultChanged(ControllerInfo controller, final String query,
853            final int itemCount, final Bundle extras) {
854        if (controller == null) {
855            throw new IllegalArgumentException("controller shouldn't be null");
856        }
857        if (TextUtils.isEmpty(query)) {
858            throw new IllegalArgumentException("query shouldn't be empty");
859        }
860        notifyToController(controller, new NotifyRunnable() {
861            @Override
862            public void run(ControllerCb callback) throws RemoteException {
863                callback.onSearchResultChanged(query, itemCount, extras);
864            }
865        });
866    }
867
868    ///////////////////////////////////////////////////
869    // package private and private methods
870    ///////////////////////////////////////////////////
871    @Override
872    MediaSession2 createInstance() {
873        return new MediaSession2(this);
874    }
875
876    @Override
877    @NonNull MediaSession2 getInstance() {
878        return mInstance;
879    }
880
881    @Override
882    Context getContext() {
883        return mContext;
884    }
885
886    @Override
887    Executor getCallbackExecutor() {
888        return mCallbackExecutor;
889    }
890
891    @Override
892    SessionCallback getCallback() {
893        return mCallback;
894    }
895
896    @Override
897    MediaSessionCompat getSessionCompat() {
898        return mSessionCompat;
899    }
900
901    @Override
902    boolean isClosed() {
903        return !mHandlerThread.isAlive();
904    }
905
906    @Override
907    PlaybackStateCompat getPlaybackStateCompat() {
908        synchronized (mLock) {
909            int state = MediaUtils2.createPlaybackStateCompatState(getPlayerState(),
910                    getBufferingState());
911            long allActions = PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_PAUSE
912                    | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_REWIND
913                    | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
914                    | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
915                    | PlaybackStateCompat.ACTION_FAST_FORWARD
916                    | PlaybackStateCompat.ACTION_SET_RATING
917                    | PlaybackStateCompat.ACTION_SEEK_TO | PlaybackStateCompat.ACTION_PLAY_PAUSE
918                    | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
919                    | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
920                    | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
921                    | PlaybackStateCompat.ACTION_PLAY_FROM_URI | PlaybackStateCompat.ACTION_PREPARE
922                    | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID
923                    | PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH
924                    | PlaybackStateCompat.ACTION_PREPARE_FROM_URI
925                    | PlaybackStateCompat.ACTION_SET_REPEAT_MODE
926                    | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
927                    | PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED;
928            return new PlaybackStateCompat.Builder()
929                    .setState(state, getCurrentPosition(), getPlaybackSpeed())
930                    .setActions(allActions)
931                    .setBufferedPosition(getBufferedPosition())
932                    .build();
933        }
934    }
935
936    @Override
937    PlaybackInfo getPlaybackInfo() {
938        synchronized (mLock) {
939            return mPlaybackInfo;
940        }
941    }
942    private static String getServiceName(Context context, String serviceAction, String id) {
943        PackageManager manager = context.getPackageManager();
944        Intent serviceIntent = new Intent(serviceAction);
945        serviceIntent.setPackage(context.getPackageName());
946        List<ResolveInfo> services = manager.queryIntentServices(serviceIntent,
947                PackageManager.GET_META_DATA);
948        String serviceName = null;
949        if (services != null) {
950            for (int i = 0; i < services.size(); i++) {
951                String serviceId = SessionToken2.getSessionId(services.get(i));
952                if (serviceId != null && TextUtils.equals(id, serviceId)) {
953                    if (services.get(i).serviceInfo == null) {
954                        continue;
955                    }
956                    if (serviceName != null) {
957                        throw new IllegalArgumentException("Ambiguous session type. Multiple"
958                                + " session services define the same id=" + id);
959                    }
960                    serviceName = services.get(i).serviceInfo.name;
961                }
962            }
963        }
964        return serviceName;
965    }
966
967    private void notifyAgentUpdatedNotLocked(MediaPlaylistAgent oldAgent) {
968        // Tells the playlist change first, to current item can change be notified with an item
969        // within the playlist.
970        List<MediaItem2> oldPlaylist = oldAgent.getPlaylist();
971        final List<MediaItem2> newPlaylist = getPlaylist();
972        if (!ObjectsCompat.equals(oldPlaylist, newPlaylist)) {
973            notifyToAllControllers(new NotifyRunnable() {
974                @Override
975                public void run(ControllerCb callback) throws RemoteException {
976                    callback.onPlaylistChanged(
977                            newPlaylist, getPlaylistMetadata());
978                }
979            });
980        } else {
981            MediaMetadata2 oldMetadata = oldAgent.getPlaylistMetadata();
982            final MediaMetadata2 newMetadata = getPlaylistMetadata();
983            if (!ObjectsCompat.equals(oldMetadata, newMetadata)) {
984                notifyToAllControllers(new NotifyRunnable() {
985                    @Override
986                    public void run(ControllerCb callback) throws RemoteException {
987                        callback.onPlaylistMetadataChanged(newMetadata);
988                    }
989                });
990            }
991        }
992        MediaItem2 oldCurrentItem = oldAgent.getCurrentMediaItem();
993        final MediaItem2 newCurrentItem = getCurrentMediaItem();
994        if (!ObjectsCompat.equals(oldCurrentItem, newCurrentItem)) {
995            notifyToAllControllers(new NotifyRunnable() {
996                @Override
997                public void run(ControllerCb callback) throws RemoteException {
998                    callback.onCurrentMediaItemChanged(newCurrentItem);
999                }
1000            });
1001        }
1002        final int repeatMode = getRepeatMode();
1003        if (oldAgent.getRepeatMode() != repeatMode) {
1004            notifyToAllControllers(new NotifyRunnable() {
1005                @Override
1006                public void run(ControllerCb callback) throws RemoteException {
1007                    callback.onRepeatModeChanged(repeatMode);
1008                }
1009            });
1010        }
1011        final int shuffleMode = getShuffleMode();
1012        if (oldAgent.getShuffleMode() != shuffleMode) {
1013            notifyToAllControllers(new NotifyRunnable() {
1014                @Override
1015                public void run(ControllerCb callback) throws RemoteException {
1016                    callback.onShuffleModeChanged(shuffleMode);
1017                }
1018            });
1019        }
1020    }
1021
1022    private void notifyPlayerUpdatedNotLocked(MediaPlayerInterface oldPlayer) {
1023        // Always forcefully send the player state and buffered state to send the current position
1024        // and buffered position.
1025        final int playerState = getPlayerState();
1026        notifyToAllControllers(new NotifyRunnable() {
1027            @Override
1028            public void run(ControllerCb callback) throws RemoteException {
1029                callback.onPlayerStateChanged(playerState);
1030            }
1031        });
1032        final MediaItem2 item = getCurrentMediaItem();
1033        if (item != null) {
1034            final int bufferingState = getBufferingState();
1035            notifyToAllControllers(new NotifyRunnable() {
1036                @Override
1037                public void run(ControllerCb callback) throws RemoteException {
1038                    callback.onBufferingStateChanged(item, bufferingState);
1039                }
1040            });
1041        }
1042        final float speed = getPlaybackSpeed();
1043        if (speed != oldPlayer.getPlaybackSpeed()) {
1044            notifyToAllControllers(new NotifyRunnable() {
1045                @Override
1046                public void run(ControllerCb callback) throws RemoteException {
1047                    callback.onPlaybackSpeedChanged(speed);
1048                }
1049            });
1050        }
1051        // Note: AudioInfo is updated outside of this API.
1052    }
1053
1054    private void notifyPlaylistChangedOnExecutor(MediaPlaylistAgent playlistAgent,
1055            final List<MediaItem2> list, final MediaMetadata2 metadata) {
1056        synchronized (mLock) {
1057            if (playlistAgent != mPlaylistAgent) {
1058                // Ignore calls from the old agent.
1059                return;
1060            }
1061        }
1062        mCallback.onPlaylistChanged(mInstance, playlistAgent, list, metadata);
1063        notifyToAllControllers(new NotifyRunnable() {
1064            @Override
1065            public void run(ControllerCb callback) throws RemoteException {
1066                callback.onPlaylistChanged(list, metadata);
1067            }
1068        });
1069    }
1070
1071    private void notifyPlaylistMetadataChangedOnExecutor(MediaPlaylistAgent playlistAgent,
1072            final MediaMetadata2 metadata) {
1073        synchronized (mLock) {
1074            if (playlistAgent != mPlaylistAgent) {
1075                // Ignore calls from the old agent.
1076                return;
1077            }
1078        }
1079        mCallback.onPlaylistMetadataChanged(mInstance, playlistAgent, metadata);
1080        notifyToAllControllers(new NotifyRunnable() {
1081            @Override
1082            public void run(ControllerCb callback) throws RemoteException {
1083                callback.onPlaylistMetadataChanged(metadata);
1084            }
1085        });
1086    }
1087
1088    private void notifyRepeatModeChangedOnExecutor(MediaPlaylistAgent playlistAgent,
1089            final int repeatMode) {
1090        synchronized (mLock) {
1091            if (playlistAgent != mPlaylistAgent) {
1092                // Ignore calls from the old agent.
1093                return;
1094            }
1095        }
1096        mCallback.onRepeatModeChanged(mInstance, playlistAgent, repeatMode);
1097        notifyToAllControllers(new NotifyRunnable() {
1098            @Override
1099            public void run(ControllerCb callback) throws RemoteException {
1100                callback.onRepeatModeChanged(repeatMode);
1101            }
1102        });
1103    }
1104
1105    private void notifyShuffleModeChangedOnExecutor(MediaPlaylistAgent playlistAgent,
1106            final int shuffleMode) {
1107        synchronized (mLock) {
1108            if (playlistAgent != mPlaylistAgent) {
1109                // Ignore calls from the old agent.
1110                return;
1111            }
1112        }
1113        mCallback.onShuffleModeChanged(mInstance, playlistAgent, shuffleMode);
1114        notifyToAllControllers(new NotifyRunnable() {
1115            @Override
1116            public void run(ControllerCb callback) throws RemoteException {
1117                callback.onShuffleModeChanged(shuffleMode);
1118            }
1119        });
1120    }
1121
1122    private void notifyToController(@NonNull final ControllerInfo controller,
1123            @NonNull NotifyRunnable runnable) {
1124        if (controller == null) {
1125            return;
1126        }
1127        try {
1128            runnable.run(controller.getControllerCb());
1129        } catch (DeadObjectException e) {
1130            if (DEBUG) {
1131                Log.d(TAG, controller.toString() + " is gone", e);
1132            }
1133            mSession2Stub.removeControllerInfo(controller);
1134            mCallbackExecutor.execute(new Runnable() {
1135                @Override
1136                public void run() {
1137                    mCallback.onDisconnected(MediaSession2ImplBase.this.getInstance(), controller);
1138                }
1139            });
1140        } catch (RemoteException e) {
1141            // Currently it's TransactionTooLargeException or DeadSystemException.
1142            // We'd better to leave log for those cases because
1143            //   - TransactionTooLargeException means that we may need to fix our code.
1144            //     (e.g. add pagination or special way to deliver Bitmap)
1145            //   - DeadSystemException means that errors around it can be ignored.
1146            Log.w(TAG, "Exception in " + controller.toString(), e);
1147        }
1148    }
1149
1150    private void notifyToAllControllers(@NonNull NotifyRunnable runnable) {
1151        List<ControllerInfo> controllers = getConnectedControllers();
1152        for (int i = 0; i < controllers.size(); i++) {
1153            notifyToController(controllers.get(i), runnable);
1154        }
1155    }
1156
1157    ///////////////////////////////////////////////////
1158    // Inner classes
1159    ///////////////////////////////////////////////////
1160    @FunctionalInterface
1161    private interface NotifyRunnable {
1162        void run(ControllerCb callback) throws RemoteException;
1163    }
1164
1165    private static class MyPlayerEventCallback extends PlayerEventCallback {
1166        private final WeakReference<MediaSession2ImplBase> mSession;
1167
1168        private MyPlayerEventCallback(MediaSession2ImplBase session) {
1169            mSession = new WeakReference<>(session);
1170        }
1171
1172        @Override
1173        public void onCurrentDataSourceChanged(final MediaPlayerInterface player,
1174                final DataSourceDesc dsd) {
1175            final MediaSession2ImplBase session = getSession();
1176            if (session == null) {
1177                return;
1178            }
1179            session.getCallbackExecutor().execute(new Runnable() {
1180                @Override
1181                public void run() {
1182                    final MediaItem2 item;
1183                    if (dsd == null) {
1184                        // This is OK because onCurrentDataSourceChanged() can be called with the
1185                        // null dsd, so onCurrentMediaItemChanged() can be as well.
1186                        item = null;
1187                    } else {
1188                        item = MyPlayerEventCallback.this.getMediaItem(session, dsd);
1189                        if (item == null) {
1190                            Log.w(TAG, "Cannot obtain media item from the dsd=" + dsd);
1191                            return;
1192                        }
1193                    }
1194                    session.getCallback().onCurrentMediaItemChanged(session.getInstance(), player,
1195                            item);
1196                    session.notifyToAllControllers(new NotifyRunnable() {
1197                        @Override
1198                        public void run(ControllerCb callback) throws RemoteException {
1199                            callback.onCurrentMediaItemChanged(item);
1200                        }
1201                    });
1202                }
1203            });
1204        }
1205
1206        @Override
1207        public void onMediaPrepared(final MediaPlayerInterface mpb, final DataSourceDesc dsd) {
1208            final MediaSession2ImplBase session = getSession();
1209            if (session == null || dsd == null) {
1210                return;
1211            }
1212            session.getCallbackExecutor().execute(new Runnable() {
1213                @Override
1214                public void run() {
1215                    MediaItem2 item = MyPlayerEventCallback.this.getMediaItem(session, dsd);
1216                    if (item == null) {
1217                        return;
1218                    }
1219                    if (item.equals(session.getCurrentMediaItem())) {
1220                        long duration = session.getDuration();
1221                        if (duration < 0) {
1222                            return;
1223                        }
1224                        MediaMetadata2 metadata = item.getMetadata();
1225                        if (metadata != null) {
1226                            if (!metadata.containsKey(MediaMetadata2.METADATA_KEY_DURATION)) {
1227                                metadata = new MediaMetadata2.Builder(metadata).putLong(
1228                                        MediaMetadata2.METADATA_KEY_DURATION, duration).build();
1229                            } else {
1230                                long durationFromMetadata =
1231                                        metadata.getLong(MediaMetadata2.METADATA_KEY_DURATION);
1232                                if (duration != durationFromMetadata) {
1233                                    // Warns developers about the mismatch. Don't log media item
1234                                    // here to keep metadata secure.
1235                                    Log.w(TAG, "duration mismatch for an item."
1236                                            + " duration from player=" + duration
1237                                            + " duration from metadata=" + durationFromMetadata
1238                                            + ". May be a timing issue?");
1239                                }
1240                                // Trust duration in the metadata set by developer.
1241                                // In theory, duration may differ if the current item has been
1242                                // changed before the getDuration(). So it's better not touch
1243                                // duration set by developer.
1244                                metadata = null;
1245                            }
1246                        } else {
1247                            metadata = new MediaMetadata2.Builder()
1248                                    .putLong(MediaMetadata2.METADATA_KEY_DURATION, duration)
1249                                    .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID,
1250                                            item.getMediaId())
1251                                    .build();
1252                        }
1253                        if (metadata != null) {
1254                            item.setMetadata(metadata);
1255                            session.notifyToAllControllers(new NotifyRunnable() {
1256                                @Override
1257                                public void run(ControllerCb callback) throws RemoteException {
1258                                    callback.onPlaylistChanged(
1259                                            session.getPlaylist(), session.getPlaylistMetadata());
1260                                }
1261                            });
1262                        }
1263                    }
1264                    session.getCallback().onMediaPrepared(session.getInstance(), mpb, item);
1265                }
1266            });
1267        }
1268
1269        @Override
1270        public void onPlayerStateChanged(final MediaPlayerInterface player, final int state) {
1271            final MediaSession2ImplBase session = getSession();
1272            if (session == null) {
1273                return;
1274            }
1275            session.getCallbackExecutor().execute(new Runnable() {
1276                @Override
1277                public void run() {
1278                    session.getCallback().onPlayerStateChanged(
1279                            session.getInstance(), player, state);
1280                    session.notifyToAllControllers(new NotifyRunnable() {
1281                        @Override
1282                        public void run(ControllerCb callback) throws RemoteException {
1283                            callback.onPlayerStateChanged(state);
1284                        }
1285                    });
1286                }
1287            });
1288        }
1289
1290        @Override
1291        public void onBufferingStateChanged(final MediaPlayerInterface mpb,
1292                final DataSourceDesc dsd, final int state) {
1293            final MediaSession2ImplBase session = getSession();
1294            if (session == null || dsd == null) {
1295                return;
1296            }
1297            session.getCallbackExecutor().execute(new Runnable() {
1298                @Override
1299                public void run() {
1300                    final MediaItem2 item = MyPlayerEventCallback.this.getMediaItem(session, dsd);
1301                    if (item == null) {
1302                        return;
1303                    }
1304                    session.getCallback().onBufferingStateChanged(
1305                            session.getInstance(), mpb, item, state);
1306                    session.notifyToAllControllers(new NotifyRunnable() {
1307                        @Override
1308                        public void run(ControllerCb callback) throws RemoteException {
1309                            callback.onBufferingStateChanged(item, state);
1310                        }
1311                    });
1312                }
1313            });
1314        }
1315
1316        @Override
1317        public void onPlaybackSpeedChanged(final MediaPlayerInterface mpb, final float speed) {
1318            final MediaSession2ImplBase session = getSession();
1319            if (session == null) {
1320                return;
1321            }
1322            session.getCallbackExecutor().execute(new Runnable() {
1323                @Override
1324                public void run() {
1325                    session.getCallback().onPlaybackSpeedChanged(session.getInstance(), mpb, speed);
1326                    session.notifyToAllControllers(new NotifyRunnable() {
1327                        @Override
1328                        public void run(ControllerCb callback) throws RemoteException {
1329                            callback.onPlaybackSpeedChanged(speed);
1330                        }
1331                    });
1332                }
1333            });
1334        }
1335
1336        @Override
1337        public void onSeekCompleted(final MediaPlayerInterface mpb, final long position) {
1338            final MediaSession2ImplBase session = getSession();
1339            if (session == null) {
1340                return;
1341            }
1342            session.getCallbackExecutor().execute(new Runnable() {
1343                @Override
1344                public void run() {
1345                    session.getCallback().onSeekCompleted(session.getInstance(), mpb, position);
1346                    session.notifyToAllControllers(new NotifyRunnable() {
1347                        @Override
1348                        public void run(ControllerCb callback) throws RemoteException {
1349                            callback.onSeekCompleted(position);
1350                        }
1351                    });
1352                }
1353            });
1354        }
1355
1356        private MediaSession2ImplBase getSession() {
1357            final MediaSession2ImplBase session = mSession.get();
1358            if (session == null && DEBUG) {
1359                Log.d(TAG, "Session is closed", new IllegalStateException());
1360            }
1361            return session;
1362        }
1363
1364        private MediaItem2 getMediaItem(MediaSession2ImplBase session, DataSourceDesc dsd) {
1365            MediaPlaylistAgent agent = session.getPlaylistAgent();
1366            if (agent == null) {
1367                if (DEBUG) {
1368                    Log.d(TAG, "Session is closed", new IllegalStateException());
1369                }
1370                return null;
1371            }
1372            MediaItem2 item = agent.getMediaItem(dsd);
1373            if (item == null) {
1374                if (DEBUG) {
1375                    Log.d(TAG, "Could not find matching item for dsd=" + dsd,
1376                            new NoSuchElementException());
1377                }
1378            }
1379            return item;
1380        }
1381    }
1382
1383    private static class MyPlaylistEventCallback extends PlaylistEventCallback {
1384        private final WeakReference<MediaSession2ImplBase> mSession;
1385
1386        private MyPlaylistEventCallback(MediaSession2ImplBase session) {
1387            mSession = new WeakReference<>(session);
1388        }
1389
1390        @Override
1391        public void onPlaylistChanged(MediaPlaylistAgent playlistAgent, List<MediaItem2> list,
1392                MediaMetadata2 metadata) {
1393            final MediaSession2ImplBase session = mSession.get();
1394            if (session == null) {
1395                return;
1396            }
1397            session.notifyPlaylistChangedOnExecutor(playlistAgent, list, metadata);
1398        }
1399
1400        @Override
1401        public void onPlaylistMetadataChanged(MediaPlaylistAgent playlistAgent,
1402                MediaMetadata2 metadata) {
1403            final MediaSession2ImplBase session = mSession.get();
1404            if (session == null) {
1405                return;
1406            }
1407            session.notifyPlaylistMetadataChangedOnExecutor(playlistAgent, metadata);
1408        }
1409
1410        @Override
1411        public void onRepeatModeChanged(MediaPlaylistAgent playlistAgent, int repeatMode) {
1412            final MediaSession2ImplBase session = mSession.get();
1413            if (session == null) {
1414                return;
1415            }
1416            session.notifyRepeatModeChangedOnExecutor(playlistAgent, repeatMode);
1417        }
1418
1419        @Override
1420        public void onShuffleModeChanged(MediaPlaylistAgent playlistAgent, int shuffleMode) {
1421            final MediaSession2ImplBase session = mSession.get();
1422            if (session == null) {
1423                return;
1424            }
1425            session.notifyShuffleModeChangedOnExecutor(playlistAgent, shuffleMode);
1426        }
1427    }
1428
1429    abstract static class BuilderBase
1430            <T extends MediaSession2, C extends SessionCallback> {
1431        final Context mContext;
1432        MediaPlayerInterface mPlayer;
1433        String mId;
1434        Executor mCallbackExecutor;
1435        C mCallback;
1436        MediaPlaylistAgent mPlaylistAgent;
1437        VolumeProviderCompat mVolumeProvider;
1438        PendingIntent mSessionActivity;
1439
1440        BuilderBase(Context context) {
1441            if (context == null) {
1442                throw new IllegalArgumentException("context shouldn't be null");
1443            }
1444            mContext = context;
1445            // Ensure MediaSessionCompat non-null or empty
1446            mId = TAG;
1447        }
1448
1449        void setPlayer(@NonNull MediaPlayerInterface player) {
1450            if (player == null) {
1451                throw new IllegalArgumentException("player shouldn't be null");
1452            }
1453            mPlayer = player;
1454        }
1455
1456        void setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) {
1457            if (playlistAgent == null) {
1458                throw new IllegalArgumentException("playlistAgent shouldn't be null");
1459            }
1460            mPlaylistAgent = playlistAgent;
1461        }
1462
1463        void setVolumeProvider(@Nullable VolumeProviderCompat volumeProvider) {
1464            mVolumeProvider = volumeProvider;
1465        }
1466
1467        void setSessionActivity(@Nullable PendingIntent pi) {
1468            mSessionActivity = pi;
1469        }
1470
1471        void setId(@NonNull String id) {
1472            if (id == null) {
1473                throw new IllegalArgumentException("id shouldn't be null");
1474            }
1475            mId = id;
1476        }
1477
1478        void setSessionCallback(@NonNull Executor executor, @NonNull C callback) {
1479            if (executor == null) {
1480                throw new IllegalArgumentException("executor shouldn't be null");
1481            }
1482            if (callback == null) {
1483                throw new IllegalArgumentException("callback shouldn't be null");
1484            }
1485            mCallbackExecutor = executor;
1486            mCallback = callback;
1487        }
1488
1489        abstract @NonNull T build();
1490    }
1491
1492    static final class Builder extends
1493            BuilderBase<MediaSession2, MediaSession2.SessionCallback> {
1494        Builder(Context context) {
1495            super(context);
1496        }
1497
1498        @Override
1499        public @NonNull MediaSession2 build() {
1500            if (mCallbackExecutor == null) {
1501                mCallbackExecutor = new MainHandlerExecutor(mContext);
1502            }
1503            if (mCallback == null) {
1504                mCallback = new SessionCallback() {};
1505            }
1506            return new MediaSession2(new MediaSession2ImplBase(mContext,
1507                    new MediaSessionCompat(mContext, mId), mId, mPlayer, mPlaylistAgent,
1508                    mVolumeProvider, mSessionActivity, mCallbackExecutor, mCallback));
1509        }
1510    }
1511
1512    static class MainHandlerExecutor implements Executor {
1513        private final Handler mHandler;
1514
1515        MainHandlerExecutor(Context context) {
1516            mHandler = new Handler(context.getMainLooper());
1517        }
1518
1519        @Override
1520        public void execute(Runnable command) {
1521            if (!mHandler.post(command)) {
1522                throw new RejectedExecutionException(mHandler + " is shutting down");
1523            }
1524        }
1525    }
1526}
1527