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 androidx.media.MediaPlayerInterface.BUFFERING_STATE_UNKNOWN;
20 import static androidx.media.MediaSession2.ControllerCb;
21 import static androidx.media.MediaSession2.ControllerInfo;
22 import static androidx.media.MediaSession2.ErrorCode;
23 import static androidx.media.MediaSession2.OnDataSourceMissingHelper;
24 import static androidx.media.MediaSession2.SessionCallback;
25 import static androidx.media.SessionToken2.TYPE_LIBRARY_SERVICE;
26 import static androidx.media.SessionToken2.TYPE_SESSION;
27 import static androidx.media.SessionToken2.TYPE_SESSION_SERVICE;
28 
29 import android.annotation.TargetApi;
30 import android.app.PendingIntent;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.pm.PackageManager;
34 import android.content.pm.ResolveInfo;
35 import android.media.AudioFocusRequest;
36 import android.media.AudioManager;
37 import android.os.Build;
38 import android.os.Bundle;
39 import android.os.DeadObjectException;
40 import android.os.Handler;
41 import android.os.HandlerThread;
42 import android.os.Process;
43 import android.os.RemoteException;
44 import android.os.ResultReceiver;
45 import android.support.v4.media.session.MediaSessionCompat;
46 import android.support.v4.media.session.PlaybackStateCompat;
47 import android.text.TextUtils;
48 import android.util.Log;
49 
50 import androidx.annotation.GuardedBy;
51 import androidx.annotation.NonNull;
52 import androidx.annotation.Nullable;
53 import androidx.core.util.ObjectsCompat;
54 import androidx.media.MediaController2.PlaybackInfo;
55 import androidx.media.MediaPlayerInterface.PlayerEventCallback;
56 import androidx.media.MediaPlaylistAgent.PlaylistEventCallback;
57 
58 import java.lang.ref.WeakReference;
59 import java.util.List;
60 import java.util.NoSuchElementException;
61 import java.util.concurrent.Executor;
62 import java.util.concurrent.RejectedExecutionException;
63 
64 @TargetApi(Build.VERSION_CODES.KITKAT)
65 class 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 
MediaSession2ImplBase(Context context, MediaSessionCompat sessionCompat, String id, MediaPlayerInterface player, MediaPlaylistAgent playlistAgent, VolumeProviderCompat volumeProvider, PendingIntent sessionActivity, Executor callbackExecutor, SessionCallback callback)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
updatePlayer(@onNull MediaPlayerInterface player, @Nullable MediaPlaylistAgent playlistAgent, @Nullable VolumeProviderCompat volumeProvider)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 
createPlaybackInfo(VolumeProviderCompat volumeProvider, AudioAttributesCompat attrs)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 
getLegacyStreamType(@ullable AudioAttributesCompat attrs)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
close()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
getPlayer()280     public @NonNull MediaPlayerInterface getPlayer() {
281         synchronized (mLock) {
282             return mPlayer;
283         }
284     }
285 
286     @Override
getPlaylistAgent()287     public @NonNull MediaPlaylistAgent getPlaylistAgent() {
288         synchronized (mLock) {
289             return mPlaylistAgent;
290         }
291     }
292 
293     @Override
getVolumeProvider()294     public @Nullable VolumeProviderCompat getVolumeProvider() {
295         synchronized (mLock) {
296             return mVolumeProvider;
297         }
298     }
299 
300     @Override
getToken()301     public @NonNull SessionToken2 getToken() {
302         return mSessionToken;
303     }
304 
305     @Override
getConnectedControllers()306     public @NonNull List<ControllerInfo> getConnectedControllers() {
307         return mSession2Stub.getConnectedControllers();
308     }
309 
310     @Override
setAudioFocusRequest(@ullable AudioFocusRequest afr)311     public void setAudioFocusRequest(@Nullable AudioFocusRequest afr) {
312     }
313 
314     @Override
setCustomLayout(@onNull ControllerInfo controller, @NonNull final List<MediaSession2.CommandButton> layout)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
setAllowedCommands(@onNull ControllerInfo controller, @NonNull final SessionCommandGroup2 commands)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
sendCustomCommand(@onNull final SessionCommand2 command, @Nullable final Bundle args)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
sendCustomCommand(@onNull ControllerInfo controller, @NonNull final SessionCommand2 command, @Nullable final Bundle args, @Nullable final ResultReceiver receiver)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
play()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
pause()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
reset()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
prepare()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
seekTo(long pos)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
skipForward()447     public void skipForward() {
448         // To match with KEYCODE_MEDIA_SKIP_FORWARD
449     }
450 
451     @Override
skipBackward()452     public void skipBackward() {
453         // To match with KEYCODE_MEDIA_SKIP_BACKWARD
454     }
455 
456     @Override
notifyError(@rrorCode final int errorCode, @Nullable final Bundle extras)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
notifyRoutesInfoChanged(@onNull ControllerInfo controller, @Nullable final List<Bundle> routes)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
getPlayerState()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
getCurrentPosition()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
getDuration()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
getBufferedPosition()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
getBufferingState()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
getPlaybackSpeed()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
setPlaybackSpeed(float speed)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
setOnDataSourceMissingHelper( @onNull OnDataSourceMissingHelper helper)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
clearOnDataSourceMissingHelper()591     public void clearOnDataSourceMissingHelper() {
592         synchronized (mLock) {
593             mDsmHelper = null;
594             if (mSessionPlaylistAgent != null) {
595                 mSessionPlaylistAgent.clearOnDataSourceMissingHelper();
596             }
597         }
598     }
599 
600     @Override
getPlaylist()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
setPlaylist(@onNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata)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
skipToPlaylistItem(@onNull MediaItem2 item)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
skipToPreviousItem()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
skipToNextItem()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
getPlaylistMetadata()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
addPlaylistItem(int index, @NonNull MediaItem2 item)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
removePlaylistItem(@onNull MediaItem2 item)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
replacePlaylistItem(int index, @NonNull MediaItem2 item)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
getCurrentMediaItem()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
updatePlaylistMetadata(@ullable MediaMetadata2 metadata)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
getRepeatMode()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
setRepeatMode(@ediaPlaylistAgent.RepeatMode int repeatMode)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
getShuffleMode()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
setShuffleMode(int shuffleMode)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
notifyChildrenChanged(ControllerInfo controller, final String parentId, final int itemCount, final Bundle extras, List<MediaSessionManager.RemoteUserInfo> subscribingBrowsers)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
notifySearchResultChanged(ControllerInfo controller, final String query, final int itemCount, final Bundle extras)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
createInstance()872     MediaSession2 createInstance() {
873         return new MediaSession2(this);
874     }
875 
876     @Override
getInstance()877     @NonNull MediaSession2 getInstance() {
878         return mInstance;
879     }
880 
881     @Override
getContext()882     Context getContext() {
883         return mContext;
884     }
885 
886     @Override
getCallbackExecutor()887     Executor getCallbackExecutor() {
888         return mCallbackExecutor;
889     }
890 
891     @Override
getCallback()892     SessionCallback getCallback() {
893         return mCallback;
894     }
895 
896     @Override
getSessionCompat()897     MediaSessionCompat getSessionCompat() {
898         return mSessionCompat;
899     }
900 
901     @Override
isClosed()902     boolean isClosed() {
903         return !mHandlerThread.isAlive();
904     }
905 
906     @Override
getPlaybackStateCompat()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
getPlaybackInfo()937     PlaybackInfo getPlaybackInfo() {
938         synchronized (mLock) {
939             return mPlaybackInfo;
940         }
941     }
getServiceName(Context context, String serviceAction, String id)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 
notifyAgentUpdatedNotLocked(MediaPlaylistAgent oldAgent)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 
notifyPlayerUpdatedNotLocked(MediaPlayerInterface oldPlayer)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 
notifyPlaylistChangedOnExecutor(MediaPlaylistAgent playlistAgent, final List<MediaItem2> list, final MediaMetadata2 metadata)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 
notifyPlaylistMetadataChangedOnExecutor(MediaPlaylistAgent playlistAgent, final MediaMetadata2 metadata)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 
notifyRepeatModeChangedOnExecutor(MediaPlaylistAgent playlistAgent, final int repeatMode)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 
notifyShuffleModeChangedOnExecutor(MediaPlaylistAgent playlistAgent, final int shuffleMode)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 
notifyToController(@onNull final ControllerInfo controller, @NonNull NotifyRunnable runnable)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 
notifyToAllControllers(@onNull NotifyRunnable runnable)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 {
run(ControllerCb callback)1162         void run(ControllerCb callback) throws RemoteException;
1163     }
1164 
1165     private static class MyPlayerEventCallback extends PlayerEventCallback {
1166         private final WeakReference<MediaSession2ImplBase> mSession;
1167 
MyPlayerEventCallback(MediaSession2ImplBase session)1168         private MyPlayerEventCallback(MediaSession2ImplBase session) {
1169             mSession = new WeakReference<>(session);
1170         }
1171 
1172         @Override
onCurrentDataSourceChanged(final MediaPlayerInterface player, final DataSourceDesc dsd)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
onMediaPrepared(final MediaPlayerInterface mpb, final DataSourceDesc dsd)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
onPlayerStateChanged(final MediaPlayerInterface player, final int state)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
onBufferingStateChanged(final MediaPlayerInterface mpb, final DataSourceDesc dsd, final int state)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
onPlaybackSpeedChanged(final MediaPlayerInterface mpb, final float speed)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
onSeekCompleted(final MediaPlayerInterface mpb, final long position)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 
getSession()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 
getMediaItem(MediaSession2ImplBase session, DataSourceDesc dsd)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 
MyPlaylistEventCallback(MediaSession2ImplBase session)1386         private MyPlaylistEventCallback(MediaSession2ImplBase session) {
1387             mSession = new WeakReference<>(session);
1388         }
1389 
1390         @Override
onPlaylistChanged(MediaPlaylistAgent playlistAgent, List<MediaItem2> list, MediaMetadata2 metadata)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
onPlaylistMetadataChanged(MediaPlaylistAgent playlistAgent, MediaMetadata2 metadata)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
onRepeatModeChanged(MediaPlaylistAgent playlistAgent, int repeatMode)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
onShuffleModeChanged(MediaPlaylistAgent playlistAgent, int shuffleMode)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 
BuilderBase(Context context)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 
setPlayer(@onNull MediaPlayerInterface player)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 
setPlaylistAgent(@onNull MediaPlaylistAgent playlistAgent)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 
setVolumeProvider(@ullable VolumeProviderCompat volumeProvider)1463         void setVolumeProvider(@Nullable VolumeProviderCompat volumeProvider) {
1464             mVolumeProvider = volumeProvider;
1465         }
1466 
setSessionActivity(@ullable PendingIntent pi)1467         void setSessionActivity(@Nullable PendingIntent pi) {
1468             mSessionActivity = pi;
1469         }
1470 
setId(@onNull String id)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 
setSessionCallback(@onNull Executor executor, @NonNull C callback)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 
build()1489         abstract @NonNull T build();
1490     }
1491 
1492     static final class Builder extends
1493             BuilderBase<MediaSession2, MediaSession2.SessionCallback> {
Builder(Context context)1494         Builder(Context context) {
1495             super(context);
1496         }
1497 
1498         @Override
build()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 
MainHandlerExecutor(Context context)1515         MainHandlerExecutor(Context context) {
1516             mHandler = new Handler(context.getMainLooper());
1517         }
1518 
1519         @Override
execute(Runnable command)1520         public void execute(Runnable command) {
1521             if (!mHandler.post(command)) {
1522                 throw new RejectedExecutionException(mHandler + " is shutting down");
1523             }
1524         }
1525     }
1526 }
1527