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 com.android.media;
18 
19 import static android.media.SessionCommand2.COMMAND_CODE_CUSTOM;
20 import static android.media.SessionToken2.TYPE_LIBRARY_SERVICE;
21 import static android.media.SessionToken2.TYPE_SESSION;
22 import static android.media.SessionToken2.TYPE_SESSION_SERVICE;
23 
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.app.PendingIntent;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.pm.PackageManager;
30 import android.content.pm.ResolveInfo;
31 import android.media.AudioAttributes;
32 import android.media.AudioFocusRequest;
33 import android.media.AudioManager;
34 import android.media.DataSourceDesc;
35 import android.media.MediaController2;
36 import android.media.MediaController2.PlaybackInfo;
37 import android.media.MediaItem2;
38 import android.media.MediaLibraryService2;
39 import android.media.MediaMetadata2;
40 import android.media.MediaPlayerBase;
41 import android.media.MediaPlayerBase.PlayerEventCallback;
42 import android.media.MediaPlayerBase.PlayerState;
43 import android.media.MediaPlaylistAgent;
44 import android.media.MediaPlaylistAgent.PlaylistEventCallback;
45 import android.media.MediaSession2;
46 import android.media.MediaSession2.Builder;
47 import android.media.SessionCommand2;
48 import android.media.MediaSession2.CommandButton;
49 import android.media.SessionCommandGroup2;
50 import android.media.MediaSession2.ControllerInfo;
51 import android.media.MediaSession2.OnDataSourceMissingHelper;
52 import android.media.MediaSession2.SessionCallback;
53 import android.media.MediaSessionService2;
54 import android.media.SessionToken2;
55 import android.media.VolumeProvider2;
56 import android.media.session.MediaSessionManager;
57 import android.media.update.MediaSession2Provider;
58 import android.os.Bundle;
59 import android.os.IBinder;
60 import android.os.Parcelable;
61 import android.os.Process;
62 import android.os.ResultReceiver;
63 import android.support.annotation.GuardedBy;
64 import android.text.TextUtils;
65 import android.util.Log;
66 
67 import java.lang.ref.WeakReference;
68 import java.lang.reflect.Field;
69 import java.util.ArrayList;
70 import java.util.Collections;
71 import java.util.HashSet;
72 import java.util.List;
73 import java.util.NoSuchElementException;
74 import java.util.Set;
75 import java.util.concurrent.Executor;
76 
77 public class MediaSession2Impl implements MediaSession2Provider {
78     private static final String TAG = "MediaSession2";
79     private static final boolean DEBUG = true;//Log.isLoggable(TAG, Log.DEBUG);
80 
81     private final Object mLock = new Object();
82 
83     private final MediaSession2 mInstance;
84     private final Context mContext;
85     private final String mId;
86     private final Executor mCallbackExecutor;
87     private final SessionCallback mCallback;
88     private final MediaSession2Stub mSessionStub;
89     private final SessionToken2 mSessionToken;
90     private final AudioManager mAudioManager;
91     private final PendingIntent mSessionActivity;
92     private final PlayerEventCallback mPlayerEventCallback;
93     private final PlaylistEventCallback mPlaylistEventCallback;
94 
95     // mPlayer is set to null when the session is closed, and we shouldn't throw an exception
96     // nor leave log always for using mPlayer when it's null. Here's the reason.
97     // When a MediaSession2 is closed, there could be a pended operation in the session callback
98     // executor that may want to access the player. Here's the sample code snippet for that.
99     //
100     //   public void onFoo() {
101     //     if (mPlayer == null) return; // first check
102     //     mSessionCallbackExecutor.executor(() -> {
103     //       // Error. Session may be closed and mPlayer can be null here.
104     //       mPlayer.foo();
105     //     });
106     //   }
107     //
108     // By adding protective code, we can also protect APIs from being called after the close()
109     //
110     // TODO(jaewan): Should we put volatile here?
111     @GuardedBy("mLock")
112     private MediaPlayerBase mPlayer;
113     @GuardedBy("mLock")
114     private MediaPlaylistAgent mPlaylistAgent;
115     @GuardedBy("mLock")
116     private SessionPlaylistAgent mSessionPlaylistAgent;
117     @GuardedBy("mLock")
118     private VolumeProvider2 mVolumeProvider;
119     @GuardedBy("mLock")
120     private PlaybackInfo mPlaybackInfo;
121     @GuardedBy("mLock")
122     private OnDataSourceMissingHelper mDsmHelper;
123 
124     /**
125      * Can be only called by the {@link Builder#build()}.
126      * @param context
127      * @param player
128      * @param id
129      * @param playlistAgent
130      * @param volumeProvider
131      * @param sessionActivity
132      * @param callbackExecutor
133      * @param callback
134      */
MediaSession2Impl(Context context, MediaPlayerBase player, String id, MediaPlaylistAgent playlistAgent, VolumeProvider2 volumeProvider, PendingIntent sessionActivity, Executor callbackExecutor, SessionCallback callback)135     public MediaSession2Impl(Context context, MediaPlayerBase player, String id,
136             MediaPlaylistAgent playlistAgent, VolumeProvider2 volumeProvider,
137             PendingIntent sessionActivity,
138             Executor callbackExecutor, SessionCallback callback) {
139         // TODO(jaewan): Keep other params.
140         mInstance = createInstance();
141 
142         // Argument checks are done by builder already.
143         // Initialize finals first.
144         mContext = context;
145         mId = id;
146         mCallback = callback;
147         mCallbackExecutor = callbackExecutor;
148         mSessionActivity = sessionActivity;
149         mSessionStub = new MediaSession2Stub(this);
150         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
151         mPlayerEventCallback = new MyPlayerEventCallback(this);
152         mPlaylistEventCallback = new MyPlaylistEventCallback(this);
153 
154         // Infer type from the id and package name.
155         String libraryService = getServiceName(context, MediaLibraryService2.SERVICE_INTERFACE, id);
156         String sessionService = getServiceName(context, MediaSessionService2.SERVICE_INTERFACE, id);
157         if (sessionService != null && libraryService != null) {
158             throw new IllegalArgumentException("Ambiguous session type. Multiple"
159                     + " session services define the same id=" + id);
160         } else if (libraryService != null) {
161             mSessionToken = new SessionToken2Impl(Process.myUid(), TYPE_LIBRARY_SERVICE,
162                     mContext.getPackageName(), libraryService, id, mSessionStub).getInstance();
163         } else if (sessionService != null) {
164             mSessionToken = new SessionToken2Impl(Process.myUid(), TYPE_SESSION_SERVICE,
165                     mContext.getPackageName(), sessionService, id, mSessionStub).getInstance();
166         } else {
167             mSessionToken = new SessionToken2Impl(Process.myUid(), TYPE_SESSION,
168                     mContext.getPackageName(), null, id, mSessionStub).getInstance();
169         }
170 
171         updatePlayer(player, playlistAgent, volumeProvider);
172 
173         // Ask server for the sanity check, and starts
174         // Sanity check for making session ID unique 'per package' cannot be done in here.
175         // Server can only know if the package has another process and has another session with the
176         // same id. Note that 'ID is unique per package' is important for controller to distinguish
177         // a session in another package.
178         MediaSessionManager manager =
179                 (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
180         if (!manager.createSession2(mSessionToken)) {
181             throw new IllegalStateException("Session with the same id is already used by"
182                     + " another process. Use MediaController2 instead.");
183         }
184     }
185 
createInstance()186     MediaSession2 createInstance() {
187         return new MediaSession2(this);
188     }
189 
getServiceName(Context context, String serviceAction, String id)190     private static String getServiceName(Context context, String serviceAction, String id) {
191         PackageManager manager = context.getPackageManager();
192         Intent serviceIntent = new Intent(serviceAction);
193         serviceIntent.setPackage(context.getPackageName());
194         List<ResolveInfo> services = manager.queryIntentServices(serviceIntent,
195                 PackageManager.GET_META_DATA);
196         String serviceName = null;
197         if (services != null) {
198             for (int i = 0; i < services.size(); i++) {
199                 String serviceId = SessionToken2Impl.getSessionId(services.get(i));
200                 if (serviceId != null && TextUtils.equals(id, serviceId)) {
201                     if (services.get(i).serviceInfo == null) {
202                         continue;
203                     }
204                     if (serviceName != null) {
205                         throw new IllegalArgumentException("Ambiguous session type. Multiple"
206                                 + " session services define the same id=" + id);
207                     }
208                     serviceName = services.get(i).serviceInfo.name;
209                 }
210             }
211         }
212         return serviceName;
213     }
214 
215     @Override
updatePlayer_impl(@onNull MediaPlayerBase player, MediaPlaylistAgent playlistAgent, VolumeProvider2 volumeProvider)216     public void updatePlayer_impl(@NonNull MediaPlayerBase player, MediaPlaylistAgent playlistAgent,
217             VolumeProvider2 volumeProvider) throws IllegalArgumentException {
218         ensureCallingThread();
219         if (player == null) {
220             throw new IllegalArgumentException("player shouldn't be null");
221         }
222         updatePlayer(player, playlistAgent, volumeProvider);
223     }
224 
updatePlayer(MediaPlayerBase player, MediaPlaylistAgent agent, VolumeProvider2 volumeProvider)225     private void updatePlayer(MediaPlayerBase player, MediaPlaylistAgent agent,
226             VolumeProvider2 volumeProvider) {
227         final MediaPlayerBase oldPlayer;
228         final MediaPlaylistAgent oldAgent;
229         final PlaybackInfo info = createPlaybackInfo(volumeProvider, player.getAudioAttributes());
230         synchronized (mLock) {
231             oldPlayer = mPlayer;
232             oldAgent = mPlaylistAgent;
233             mPlayer = player;
234             if (agent == null) {
235                 mSessionPlaylistAgent = new SessionPlaylistAgent(this, mPlayer);
236                 if (mDsmHelper != null) {
237                     mSessionPlaylistAgent.setOnDataSourceMissingHelper(mDsmHelper);
238                 }
239                 agent = mSessionPlaylistAgent;
240             }
241             mPlaylistAgent = agent;
242             mVolumeProvider = volumeProvider;
243             mPlaybackInfo = info;
244         }
245         if (player != oldPlayer) {
246             player.registerPlayerEventCallback(mCallbackExecutor, mPlayerEventCallback);
247             if (oldPlayer != null) {
248                 // Warning: Poorly implement player may ignore this
249                 oldPlayer.unregisterPlayerEventCallback(mPlayerEventCallback);
250             }
251         }
252         if (agent != oldAgent) {
253             agent.registerPlaylistEventCallback(mCallbackExecutor, mPlaylistEventCallback);
254             if (oldAgent != null) {
255                 // Warning: Poorly implement player may ignore this
256                 oldAgent.unregisterPlaylistEventCallback(mPlaylistEventCallback);
257             }
258         }
259 
260         if (oldPlayer != null) {
261             mSessionStub.notifyPlaybackInfoChanged(info);
262             notifyPlayerUpdatedNotLocked(oldPlayer);
263         }
264         // TODO(jaewan): Repeat the same thing for the playlist agent.
265     }
266 
createPlaybackInfo(VolumeProvider2 volumeProvider, AudioAttributes attrs)267     private PlaybackInfo createPlaybackInfo(VolumeProvider2 volumeProvider, AudioAttributes attrs) {
268         PlaybackInfo info;
269         if (volumeProvider == null) {
270             int stream;
271             if (attrs == null) {
272                 stream = AudioManager.STREAM_MUSIC;
273             } else {
274                 stream = attrs.getVolumeControlStream();
275                 if (stream == AudioManager.USE_DEFAULT_STREAM_TYPE) {
276                     // It may happen if the AudioAttributes doesn't have usage.
277                     // Change it to the STREAM_MUSIC because it's not supported by audio manager
278                     // for querying volume level.
279                     stream = AudioManager.STREAM_MUSIC;
280                 }
281             }
282             info = MediaController2Impl.PlaybackInfoImpl.createPlaybackInfo(
283                     PlaybackInfo.PLAYBACK_TYPE_LOCAL,
284                     attrs,
285                     mAudioManager.isVolumeFixed()
286                             ? VolumeProvider2.VOLUME_CONTROL_FIXED
287                             : VolumeProvider2.VOLUME_CONTROL_ABSOLUTE,
288                     mAudioManager.getStreamMaxVolume(stream),
289                     mAudioManager.getStreamVolume(stream));
290         } else {
291             info = MediaController2Impl.PlaybackInfoImpl.createPlaybackInfo(
292                     PlaybackInfo.PLAYBACK_TYPE_REMOTE /* ControlType */,
293                     attrs,
294                     volumeProvider.getControlType(),
295                     volumeProvider.getMaxVolume(),
296                     volumeProvider.getCurrentVolume());
297         }
298         return info;
299     }
300 
301     @Override
close_impl()302     public void close_impl() {
303         // Stop system service from listening this session first.
304         MediaSessionManager manager =
305                 (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
306         manager.destroySession2(mSessionToken);
307 
308         if (mSessionStub != null) {
309             if (DEBUG) {
310                 Log.d(TAG, "session is now unavailable, id=" + mId);
311             }
312             // Invalidate previously published session stub.
313             mSessionStub.destroyNotLocked();
314         }
315         final MediaPlayerBase player;
316         final MediaPlaylistAgent agent;
317         synchronized (mLock) {
318             player = mPlayer;
319             mPlayer = null;
320             agent = mPlaylistAgent;
321             mPlaylistAgent = null;
322             mSessionPlaylistAgent = null;
323         }
324         if (player != null) {
325             player.unregisterPlayerEventCallback(mPlayerEventCallback);
326         }
327         if (agent != null) {
328             agent.unregisterPlaylistEventCallback(mPlaylistEventCallback);
329         }
330     }
331 
332     @Override
getPlayer_impl()333     public MediaPlayerBase getPlayer_impl() {
334         return getPlayer();
335     }
336 
337     @Override
getPlaylistAgent_impl()338     public MediaPlaylistAgent getPlaylistAgent_impl() {
339         return mPlaylistAgent;
340     }
341 
342     @Override
getVolumeProvider_impl()343     public VolumeProvider2 getVolumeProvider_impl() {
344         return mVolumeProvider;
345     }
346 
347     @Override
getToken_impl()348     public SessionToken2 getToken_impl() {
349         return mSessionToken;
350     }
351 
352     @Override
getConnectedControllers_impl()353     public List<ControllerInfo> getConnectedControllers_impl() {
354         return mSessionStub.getControllers();
355     }
356 
357     @Override
setAudioFocusRequest_impl(AudioFocusRequest afr)358     public void setAudioFocusRequest_impl(AudioFocusRequest afr) {
359         // implement
360     }
361 
362     @Override
play_impl()363     public void play_impl() {
364         ensureCallingThread();
365         final MediaPlayerBase player = mPlayer;
366         if (player != null) {
367             player.play();
368         } else if (DEBUG) {
369             Log.d(TAG, "API calls after the close()", new IllegalStateException());
370         }
371     }
372 
373     @Override
pause_impl()374     public void pause_impl() {
375         ensureCallingThread();
376         final MediaPlayerBase player = mPlayer;
377         if (player != null) {
378             player.pause();
379         } else if (DEBUG) {
380             Log.d(TAG, "API calls after the close()", new IllegalStateException());
381         }
382     }
383 
384     @Override
stop_impl()385     public void stop_impl() {
386         ensureCallingThread();
387         final MediaPlayerBase player = mPlayer;
388         if (player != null) {
389             player.reset();
390         } else if (DEBUG) {
391             Log.d(TAG, "API calls after the close()", new IllegalStateException());
392         }
393     }
394 
395     @Override
skipToPlaylistItem_impl(@onNull MediaItem2 item)396     public void skipToPlaylistItem_impl(@NonNull MediaItem2 item) {
397         if (item == null) {
398             throw new IllegalArgumentException("item shouldn't be null");
399         }
400         final MediaPlaylistAgent agent = mPlaylistAgent;
401         if (agent != null) {
402             agent.skipToPlaylistItem(item);
403         } else if (DEBUG) {
404             Log.d(TAG, "API calls after the close()", new IllegalStateException());
405         }
406     }
407 
408     @Override
skipToPreviousItem_impl()409     public void skipToPreviousItem_impl() {
410         final MediaPlaylistAgent agent = mPlaylistAgent;
411         if (agent != null) {
412             agent.skipToPreviousItem();
413         } else if (DEBUG) {
414             Log.d(TAG, "API calls after the close()", new IllegalStateException());
415         }
416     }
417 
418     @Override
skipToNextItem_impl()419     public void skipToNextItem_impl() {
420         final MediaPlaylistAgent agent = mPlaylistAgent;
421         if (agent != null) {
422             agent.skipToNextItem();
423         } else if (DEBUG) {
424             Log.d(TAG, "API calls after the close()", new IllegalStateException());
425         }
426     }
427 
428     @Override
setCustomLayout_impl(@onNull ControllerInfo controller, @NonNull List<CommandButton> layout)429     public void setCustomLayout_impl(@NonNull ControllerInfo controller,
430             @NonNull List<CommandButton> layout) {
431         ensureCallingThread();
432         if (controller == null) {
433             throw new IllegalArgumentException("controller shouldn't be null");
434         }
435         if (layout == null) {
436             throw new IllegalArgumentException("layout shouldn't be null");
437         }
438         mSessionStub.notifyCustomLayoutNotLocked(controller, layout);
439     }
440 
441     //////////////////////////////////////////////////////////////////////////////////////
442     // TODO(jaewan): Implement follows
443     //////////////////////////////////////////////////////////////////////////////////////
444 
445     @Override
setAllowedCommands_impl(@onNull ControllerInfo controller, @NonNull SessionCommandGroup2 commands)446     public void setAllowedCommands_impl(@NonNull ControllerInfo controller,
447             @NonNull SessionCommandGroup2 commands) {
448         if (controller == null) {
449             throw new IllegalArgumentException("controller shouldn't be null");
450         }
451         if (commands == null) {
452             throw new IllegalArgumentException("commands shouldn't be null");
453         }
454         mSessionStub.setAllowedCommands(controller, commands);
455     }
456 
457     @Override
sendCustomCommand_impl(@onNull ControllerInfo controller, @NonNull SessionCommand2 command, Bundle args, ResultReceiver receiver)458     public void sendCustomCommand_impl(@NonNull ControllerInfo controller,
459             @NonNull SessionCommand2 command, Bundle args, ResultReceiver receiver) {
460         if (controller == null) {
461             throw new IllegalArgumentException("controller shouldn't be null");
462         }
463         if (command == null) {
464             throw new IllegalArgumentException("command shouldn't be null");
465         }
466         mSessionStub.sendCustomCommand(controller, command, args, receiver);
467     }
468 
469     @Override
sendCustomCommand_impl(@onNull SessionCommand2 command, Bundle args)470     public void sendCustomCommand_impl(@NonNull SessionCommand2 command, Bundle args) {
471         if (command == null) {
472             throw new IllegalArgumentException("command shouldn't be null");
473         }
474         mSessionStub.sendCustomCommand(command, args);
475     }
476 
477     @Override
setPlaylist_impl(@onNull List<MediaItem2> list, MediaMetadata2 metadata)478     public void setPlaylist_impl(@NonNull List<MediaItem2> list, MediaMetadata2 metadata) {
479         if (list == null) {
480             throw new IllegalArgumentException("list shouldn't be null");
481         }
482         ensureCallingThread();
483         final MediaPlaylistAgent agent = mPlaylistAgent;
484         if (agent != null) {
485             agent.setPlaylist(list, metadata);
486         } else if (DEBUG) {
487             Log.d(TAG, "API calls after the close()", new IllegalStateException());
488         }
489     }
490 
491     @Override
updatePlaylistMetadata_impl(MediaMetadata2 metadata)492     public void updatePlaylistMetadata_impl(MediaMetadata2 metadata) {
493         final MediaPlaylistAgent agent = mPlaylistAgent;
494         if (agent != null) {
495             agent.updatePlaylistMetadata(metadata);
496         } else if (DEBUG) {
497             Log.d(TAG, "API calls after the close()", new IllegalStateException());
498         }
499     }
500 
501     @Override
addPlaylistItem_impl(int index, @NonNull MediaItem2 item)502     public void addPlaylistItem_impl(int index, @NonNull MediaItem2 item) {
503         if (index < 0) {
504             throw new IllegalArgumentException("index shouldn't be negative");
505         }
506         if (item == null) {
507             throw new IllegalArgumentException("item shouldn't be null");
508         }
509         final MediaPlaylistAgent agent = mPlaylistAgent;
510         if (agent != null) {
511             agent.addPlaylistItem(index, item);
512         } else if (DEBUG) {
513             Log.d(TAG, "API calls after the close()", new IllegalStateException());
514         }
515     }
516 
517     @Override
removePlaylistItem_impl(@onNull MediaItem2 item)518     public void removePlaylistItem_impl(@NonNull MediaItem2 item) {
519         if (item == null) {
520             throw new IllegalArgumentException("item shouldn't be null");
521         }
522         final MediaPlaylistAgent agent = mPlaylistAgent;
523         if (agent != null) {
524             agent.removePlaylistItem(item);
525         } else if (DEBUG) {
526             Log.d(TAG, "API calls after the close()", new IllegalStateException());
527         }
528     }
529 
530     @Override
replacePlaylistItem_impl(int index, @NonNull MediaItem2 item)531     public void replacePlaylistItem_impl(int index, @NonNull MediaItem2 item) {
532         if (index < 0) {
533             throw new IllegalArgumentException("index shouldn't be negative");
534         }
535         if (item == null) {
536             throw new IllegalArgumentException("item shouldn't be null");
537         }
538         final MediaPlaylistAgent agent = mPlaylistAgent;
539         if (agent != null) {
540             agent.replacePlaylistItem(index, item);
541         } else if (DEBUG) {
542             Log.d(TAG, "API calls after the close()", new IllegalStateException());
543         }
544     }
545 
546     @Override
getPlaylist_impl()547     public List<MediaItem2> getPlaylist_impl() {
548         final MediaPlaylistAgent agent = mPlaylistAgent;
549         if (agent != null) {
550             return agent.getPlaylist();
551         } else if (DEBUG) {
552             Log.d(TAG, "API calls after the close()", new IllegalStateException());
553         }
554         return null;
555     }
556 
557     @Override
getPlaylistMetadata_impl()558     public MediaMetadata2 getPlaylistMetadata_impl() {
559         final MediaPlaylistAgent agent = mPlaylistAgent;
560         if (agent != null) {
561             return agent.getPlaylistMetadata();
562         } else if (DEBUG) {
563             Log.d(TAG, "API calls after the close()", new IllegalStateException());
564         }
565         return null;
566     }
567 
568     @Override
getCurrentPlaylistItem_impl()569     public MediaItem2 getCurrentPlaylistItem_impl() {
570         // TODO(jaewan): Implement
571         return null;
572     }
573 
574     @Override
getRepeatMode_impl()575     public int getRepeatMode_impl() {
576         final MediaPlaylistAgent agent = mPlaylistAgent;
577         if (agent != null) {
578             return agent.getRepeatMode();
579         } else if (DEBUG) {
580             Log.d(TAG, "API calls after the close()", new IllegalStateException());
581         }
582         return MediaPlaylistAgent.REPEAT_MODE_NONE;
583     }
584 
585     @Override
setRepeatMode_impl(int repeatMode)586     public void setRepeatMode_impl(int repeatMode) {
587         final MediaPlaylistAgent agent = mPlaylistAgent;
588         if (agent != null) {
589             agent.setRepeatMode(repeatMode);
590         } else if (DEBUG) {
591             Log.d(TAG, "API calls after the close()", new IllegalStateException());
592         }
593     }
594 
595     @Override
getShuffleMode_impl()596     public int getShuffleMode_impl() {
597         final MediaPlaylistAgent agent = mPlaylistAgent;
598         if (agent != null) {
599             return agent.getShuffleMode();
600         } else if (DEBUG) {
601             Log.d(TAG, "API calls after the close()", new IllegalStateException());
602         }
603         return MediaPlaylistAgent.SHUFFLE_MODE_NONE;
604     }
605 
606     @Override
setShuffleMode_impl(int shuffleMode)607     public void setShuffleMode_impl(int shuffleMode) {
608         final MediaPlaylistAgent agent = mPlaylistAgent;
609         if (agent != null) {
610             agent.setShuffleMode(shuffleMode);
611         } else if (DEBUG) {
612             Log.d(TAG, "API calls after the close()", new IllegalStateException());
613         }
614     }
615 
616     @Override
prepare_impl()617     public void prepare_impl() {
618         ensureCallingThread();
619         final MediaPlayerBase player = mPlayer;
620         if (player != null) {
621             player.prepare();
622         } else if (DEBUG) {
623             Log.d(TAG, "API calls after the close()", new IllegalStateException());
624         }
625     }
626 
627     @Override
seekTo_impl(long pos)628     public void seekTo_impl(long pos) {
629         ensureCallingThread();
630         final MediaPlayerBase player = mPlayer;
631         if (player != null) {
632             player.seekTo(pos);
633         } else if (DEBUG) {
634             Log.d(TAG, "API calls after the close()", new IllegalStateException());
635         }
636     }
637 
638     @Override
getPlayerState_impl()639     public @PlayerState int getPlayerState_impl() {
640         final MediaPlayerBase player = mPlayer;
641         if (player != null) {
642             return mPlayer.getPlayerState();
643         } else if (DEBUG) {
644             Log.d(TAG, "API calls after the close()", new IllegalStateException());
645         }
646         return MediaPlayerBase.PLAYER_STATE_ERROR;
647     }
648 
649     @Override
getCurrentPosition_impl()650     public long getCurrentPosition_impl() {
651         final MediaPlayerBase player = mPlayer;
652         if (player != null) {
653             return mPlayer.getCurrentPosition();
654         } else if (DEBUG) {
655             Log.d(TAG, "API calls after the close()", new IllegalStateException());
656         }
657         return MediaPlayerBase.UNKNOWN_TIME;
658     }
659 
660     @Override
getBufferedPosition_impl()661     public long getBufferedPosition_impl() {
662         final MediaPlayerBase player = mPlayer;
663         if (player != null) {
664             return mPlayer.getBufferedPosition();
665         } else if (DEBUG) {
666             Log.d(TAG, "API calls after the close()", new IllegalStateException());
667         }
668         return MediaPlayerBase.UNKNOWN_TIME;
669     }
670 
671     @Override
notifyError_impl(int errorCode, Bundle extras)672     public void notifyError_impl(int errorCode, Bundle extras) {
673         mSessionStub.notifyError(errorCode, extras);
674     }
675 
676     @Override
setOnDataSourceMissingHelper_impl(@onNull OnDataSourceMissingHelper helper)677     public void setOnDataSourceMissingHelper_impl(@NonNull OnDataSourceMissingHelper helper) {
678         if (helper == null) {
679             throw new IllegalArgumentException("helper shouldn't be null");
680         }
681         synchronized (mLock) {
682             mDsmHelper = helper;
683             if (mSessionPlaylistAgent != null) {
684                 mSessionPlaylistAgent.setOnDataSourceMissingHelper(helper);
685             }
686         }
687     }
688 
689     @Override
clearOnDataSourceMissingHelper_impl()690     public void clearOnDataSourceMissingHelper_impl() {
691         synchronized (mLock) {
692             mDsmHelper = null;
693             if (mSessionPlaylistAgent != null) {
694                 mSessionPlaylistAgent.clearOnDataSourceMissingHelper();
695             }
696         }
697     }
698 
699     ///////////////////////////////////////////////////
700     // Protected or private methods
701     ///////////////////////////////////////////////////
702 
703     // Enforces developers to call all the methods on the initially given thread
704     // because calls from the MediaController2 will be run on the thread.
705     // TODO(jaewan): Should we allow calls from the multiple thread?
706     //               I prefer this way because allowing multiple thread may case tricky issue like
707     //               b/63446360. If the {@link #setPlayer()} with {@code null} can be called from
708     //               another thread, transport controls can be called after that.
709     //               That's basically the developer's mistake, but they cannot understand what's
710     //               happening behind until we tell them so.
711     //               If enforcing callling thread doesn't look good, we can alternatively pick
712     //               1. Allow calls from random threads for all methods.
713     //               2. Allow calls from random threads for all methods, except for the
714     //                  {@link #setPlayer()}.
ensureCallingThread()715     void ensureCallingThread() {
716         // TODO(jaewan): Uncomment or remove
717         /*
718         if (mHandler.getLooper() != Looper.myLooper()) {
719             throw new IllegalStateException("Run this on the given thread");
720         }*/
721     }
722 
notifyPlaylistChangedOnExecutor(MediaPlaylistAgent playlistAgent, List<MediaItem2> list, MediaMetadata2 metadata)723     private void notifyPlaylistChangedOnExecutor(MediaPlaylistAgent playlistAgent,
724             List<MediaItem2> list, MediaMetadata2 metadata) {
725         if (playlistAgent != mPlaylistAgent) {
726             // Ignore calls from the old agent.
727             return;
728         }
729         mCallback.onPlaylistChanged(mInstance, playlistAgent, list, metadata);
730         mSessionStub.notifyPlaylistChangedNotLocked(list, metadata);
731     }
732 
notifyPlaylistMetadataChangedOnExecutor(MediaPlaylistAgent playlistAgent, MediaMetadata2 metadata)733     private void notifyPlaylistMetadataChangedOnExecutor(MediaPlaylistAgent playlistAgent,
734             MediaMetadata2 metadata) {
735         if (playlistAgent != mPlaylistAgent) {
736             // Ignore calls from the old agent.
737             return;
738         }
739         mCallback.onPlaylistMetadataChanged(mInstance, playlistAgent, metadata);
740         mSessionStub.notifyPlaylistMetadataChangedNotLocked(metadata);
741     }
742 
notifyRepeatModeChangedOnExecutor(MediaPlaylistAgent playlistAgent, int repeatMode)743     private void notifyRepeatModeChangedOnExecutor(MediaPlaylistAgent playlistAgent,
744             int repeatMode) {
745         if (playlistAgent != mPlaylistAgent) {
746             // Ignore calls from the old agent.
747             return;
748         }
749         mCallback.onRepeatModeChanged(mInstance, playlistAgent, repeatMode);
750         mSessionStub.notifyRepeatModeChangedNotLocked(repeatMode);
751     }
752 
notifyShuffleModeChangedOnExecutor(MediaPlaylistAgent playlistAgent, int shuffleMode)753     private void notifyShuffleModeChangedOnExecutor(MediaPlaylistAgent playlistAgent,
754             int shuffleMode) {
755         if (playlistAgent != mPlaylistAgent) {
756             // Ignore calls from the old agent.
757             return;
758         }
759         mCallback.onShuffleModeChanged(mInstance, playlistAgent, shuffleMode);
760         mSessionStub.notifyShuffleModeChangedNotLocked(shuffleMode);
761     }
762 
notifyPlayerUpdatedNotLocked(MediaPlayerBase oldPlayer)763     private void notifyPlayerUpdatedNotLocked(MediaPlayerBase oldPlayer) {
764         final MediaPlayerBase player = mPlayer;
765         // TODO(jaewan): (Can be post-P) Find better way for player.getPlayerState() //
766         //               In theory, Session.getXXX() may not be the same as Player.getXXX()
767         //               and we should notify information of the session.getXXX() instead of
768         //               player.getXXX()
769         // Notify to controllers as well.
770         final int state = player.getPlayerState();
771         if (state != oldPlayer.getPlayerState()) {
772             mSessionStub.notifyPlayerStateChangedNotLocked(state);
773         }
774 
775         final long currentTimeMs = System.currentTimeMillis();
776         final long position = player.getCurrentPosition();
777         if (position != oldPlayer.getCurrentPosition()) {
778             mSessionStub.notifyPositionChangedNotLocked(currentTimeMs, position);
779         }
780 
781         final float speed = player.getPlaybackSpeed();
782         if (speed != oldPlayer.getPlaybackSpeed()) {
783             mSessionStub.notifyPlaybackSpeedChangedNotLocked(speed);
784         }
785 
786         final long bufferedPosition = player.getBufferedPosition();
787         if (bufferedPosition != oldPlayer.getBufferedPosition()) {
788             mSessionStub.notifyBufferedPositionChangedNotLocked(bufferedPosition);
789         }
790     }
791 
getContext()792     Context getContext() {
793         return mContext;
794     }
795 
getInstance()796     MediaSession2 getInstance() {
797         return mInstance;
798     }
799 
getPlayer()800     MediaPlayerBase getPlayer() {
801         return mPlayer;
802     }
803 
getPlaylistAgent()804     MediaPlaylistAgent getPlaylistAgent() {
805         return mPlaylistAgent;
806     }
807 
getCallbackExecutor()808     Executor getCallbackExecutor() {
809         return mCallbackExecutor;
810     }
811 
getCallback()812     SessionCallback getCallback() {
813         return mCallback;
814     }
815 
getSessionStub()816     MediaSession2Stub getSessionStub() {
817         return mSessionStub;
818     }
819 
getVolumeProvider()820     VolumeProvider2 getVolumeProvider() {
821         return mVolumeProvider;
822     }
823 
getPlaybackInfo()824     PlaybackInfo getPlaybackInfo() {
825         synchronized (mLock) {
826             return mPlaybackInfo;
827         }
828     }
829 
getSessionActivity()830     PendingIntent getSessionActivity() {
831         return mSessionActivity;
832     }
833 
834     private static class MyPlayerEventCallback extends PlayerEventCallback {
835         private final WeakReference<MediaSession2Impl> mSession;
836 
MyPlayerEventCallback(MediaSession2Impl session)837         private MyPlayerEventCallback(MediaSession2Impl session) {
838             mSession = new WeakReference<>(session);
839         }
840 
841         @Override
onCurrentDataSourceChanged(MediaPlayerBase mpb, DataSourceDesc dsd)842         public void onCurrentDataSourceChanged(MediaPlayerBase mpb, DataSourceDesc dsd) {
843             MediaSession2Impl session = getSession();
844             if (session == null || dsd == null) {
845                 return;
846             }
847             session.getCallbackExecutor().execute(() -> {
848                 MediaItem2 item = getMediaItem(session, dsd);
849                 if (item == null) {
850                     return;
851                 }
852                 session.getCallback().onCurrentMediaItemChanged(session.getInstance(), mpb, item);
853                 // TODO (jaewan): Notify controllers through appropriate callback. (b/74505936)
854             });
855         }
856 
857         @Override
onMediaPrepared(MediaPlayerBase mpb, DataSourceDesc dsd)858         public void onMediaPrepared(MediaPlayerBase mpb, DataSourceDesc dsd) {
859             MediaSession2Impl session = getSession();
860             if (session == null || dsd == null) {
861                 return;
862             }
863             session.getCallbackExecutor().execute(() -> {
864                 MediaItem2 item = getMediaItem(session, dsd);
865                 if (item == null) {
866                     return;
867                 }
868                 session.getCallback().onMediaPrepared(session.getInstance(), mpb, item);
869                 // TODO (jaewan): Notify controllers through appropriate callback. (b/74505936)
870             });
871         }
872 
873         @Override
onPlayerStateChanged(MediaPlayerBase mpb, int state)874         public void onPlayerStateChanged(MediaPlayerBase mpb, int state) {
875             MediaSession2Impl session = getSession();
876             if (session == null) {
877                 return;
878             }
879             session.getCallbackExecutor().execute(() -> {
880                 session.getCallback().onPlayerStateChanged(session.getInstance(), mpb, state);
881                 session.getSessionStub().notifyPlayerStateChangedNotLocked(state);
882             });
883         }
884 
885         @Override
onBufferingStateChanged(MediaPlayerBase mpb, DataSourceDesc dsd, int state)886         public void onBufferingStateChanged(MediaPlayerBase mpb, DataSourceDesc dsd, int state) {
887             MediaSession2Impl session = getSession();
888             if (session == null || dsd == null) {
889                 return;
890             }
891             session.getCallbackExecutor().execute(() -> {
892                 MediaItem2 item = getMediaItem(session, dsd);
893                 if (item == null) {
894                     return;
895                 }
896                 session.getCallback().onBufferingStateChanged(
897                         session.getInstance(), mpb, item, state);
898                 // TODO (jaewan): Notify controllers through appropriate callback. (b/74505936)
899             });
900         }
901 
getSession()902         private MediaSession2Impl getSession() {
903             final MediaSession2Impl session = mSession.get();
904             if (session == null && DEBUG) {
905                 Log.d(TAG, "Session is closed", new IllegalStateException());
906             }
907             return session;
908         }
909 
getMediaItem(MediaSession2Impl session, DataSourceDesc dsd)910         private MediaItem2 getMediaItem(MediaSession2Impl session, DataSourceDesc dsd) {
911             MediaPlaylistAgent agent = session.getPlaylistAgent();
912             if (agent == null) {
913                 if (DEBUG) {
914                     Log.d(TAG, "Session is closed", new IllegalStateException());
915                 }
916                 return null;
917             }
918             MediaItem2 item = agent.getMediaItem(dsd);
919             if (item == null) {
920                 if (DEBUG) {
921                     Log.d(TAG, "Could not find matching item for dsd=" + dsd,
922                             new NoSuchElementException());
923                 }
924             }
925             return item;
926         }
927     }
928 
929     private static class MyPlaylistEventCallback extends PlaylistEventCallback {
930         private final WeakReference<MediaSession2Impl> mSession;
931 
MyPlaylistEventCallback(MediaSession2Impl session)932         private MyPlaylistEventCallback(MediaSession2Impl session) {
933             mSession = new WeakReference<>(session);
934         }
935 
936         @Override
onPlaylistChanged(MediaPlaylistAgent playlistAgent, List<MediaItem2> list, MediaMetadata2 metadata)937         public void onPlaylistChanged(MediaPlaylistAgent playlistAgent, List<MediaItem2> list,
938                 MediaMetadata2 metadata) {
939             final MediaSession2Impl session = mSession.get();
940             if (session == null) {
941                 return;
942             }
943             session.notifyPlaylistChangedOnExecutor(playlistAgent, list, metadata);
944         }
945 
946         @Override
onPlaylistMetadataChanged(MediaPlaylistAgent playlistAgent, MediaMetadata2 metadata)947         public void onPlaylistMetadataChanged(MediaPlaylistAgent playlistAgent,
948                 MediaMetadata2 metadata) {
949             final MediaSession2Impl session = mSession.get();
950             if (session == null) {
951                 return;
952             }
953             session.notifyPlaylistMetadataChangedOnExecutor(playlistAgent, metadata);
954         }
955 
956         @Override
onRepeatModeChanged(MediaPlaylistAgent playlistAgent, int repeatMode)957         public void onRepeatModeChanged(MediaPlaylistAgent playlistAgent, int repeatMode) {
958             final MediaSession2Impl session = mSession.get();
959             if (session == null) {
960                 return;
961             }
962             session.notifyRepeatModeChangedOnExecutor(playlistAgent, repeatMode);
963         }
964 
965         @Override
onShuffleModeChanged(MediaPlaylistAgent playlistAgent, int shuffleMode)966         public void onShuffleModeChanged(MediaPlaylistAgent playlistAgent, int shuffleMode) {
967             final MediaSession2Impl session = mSession.get();
968             if (session == null) {
969                 return;
970             }
971             session.notifyShuffleModeChangedOnExecutor(playlistAgent, shuffleMode);
972         }
973     }
974 
975     public static final class CommandImpl implements CommandProvider {
976         private static final String KEY_COMMAND_CODE
977                 = "android.media.media_session2.command.command_code";
978         private static final String KEY_COMMAND_CUSTOM_COMMAND
979                 = "android.media.media_session2.command.custom_command";
980         private static final String KEY_COMMAND_EXTRAS
981                 = "android.media.media_session2.command.extras";
982 
983         private final SessionCommand2 mInstance;
984         private final int mCommandCode;
985         // Nonnull if it's custom command
986         private final String mCustomCommand;
987         private final Bundle mExtras;
988 
CommandImpl(SessionCommand2 instance, int commandCode)989         public CommandImpl(SessionCommand2 instance, int commandCode) {
990             mInstance = instance;
991             mCommandCode = commandCode;
992             mCustomCommand = null;
993             mExtras = null;
994         }
995 
CommandImpl(SessionCommand2 instance, @NonNull String action, @Nullable Bundle extras)996         public CommandImpl(SessionCommand2 instance, @NonNull String action,
997                 @Nullable Bundle extras) {
998             if (action == null) {
999                 throw new IllegalArgumentException("action shouldn't be null");
1000             }
1001             mInstance = instance;
1002             mCommandCode = COMMAND_CODE_CUSTOM;
1003             mCustomCommand = action;
1004             mExtras = extras;
1005         }
1006 
1007         @Override
getCommandCode_impl()1008         public int getCommandCode_impl() {
1009             return mCommandCode;
1010         }
1011 
1012         @Override
getCustomCommand_impl()1013         public @Nullable String getCustomCommand_impl() {
1014             return mCustomCommand;
1015         }
1016 
1017         @Override
getExtras_impl()1018         public @Nullable Bundle getExtras_impl() {
1019             return mExtras;
1020         }
1021 
1022         /**
1023          * @return a new Bundle instance from the Command
1024          */
1025         @Override
toBundle_impl()1026         public Bundle toBundle_impl() {
1027             Bundle bundle = new Bundle();
1028             bundle.putInt(KEY_COMMAND_CODE, mCommandCode);
1029             bundle.putString(KEY_COMMAND_CUSTOM_COMMAND, mCustomCommand);
1030             bundle.putBundle(KEY_COMMAND_EXTRAS, mExtras);
1031             return bundle;
1032         }
1033 
1034         /**
1035          * @return a new Command instance from the Bundle
1036          */
fromBundle_impl(@onNull Bundle command)1037         public static SessionCommand2 fromBundle_impl(@NonNull Bundle command) {
1038             if (command == null) {
1039                 throw new IllegalArgumentException("command shouldn't be null");
1040             }
1041             int code = command.getInt(KEY_COMMAND_CODE);
1042             if (code != COMMAND_CODE_CUSTOM) {
1043                 return new SessionCommand2(code);
1044             } else {
1045                 String customCommand = command.getString(KEY_COMMAND_CUSTOM_COMMAND);
1046                 if (customCommand == null) {
1047                     return null;
1048                 }
1049                 return new SessionCommand2(customCommand, command.getBundle(KEY_COMMAND_EXTRAS));
1050             }
1051         }
1052 
1053         @Override
equals_impl(Object obj)1054         public boolean equals_impl(Object obj) {
1055             if (!(obj instanceof CommandImpl)) {
1056                 return false;
1057             }
1058             CommandImpl other = (CommandImpl) obj;
1059             // TODO(jaewan): Compare Commands with the generated UUID, as we're doing for the MI2.
1060             return mCommandCode == other.mCommandCode
1061                     && TextUtils.equals(mCustomCommand, other.mCustomCommand);
1062         }
1063 
1064         @Override
hashCode_impl()1065         public int hashCode_impl() {
1066             final int prime = 31;
1067             return ((mCustomCommand != null)
1068                     ? mCustomCommand.hashCode() : 0) * prime + mCommandCode;
1069         }
1070     }
1071 
1072     /**
1073      * Represent set of {@link SessionCommand2}.
1074      */
1075     public static class CommandGroupImpl implements CommandGroupProvider {
1076         private static final String KEY_COMMANDS =
1077                 "android.media.mediasession2.commandgroup.commands";
1078 
1079         // Prefix for all command codes
1080         private static final String PREFIX_COMMAND_CODE = "COMMAND_CODE_";
1081 
1082         // Prefix for command codes that will be sent directly to the MediaPlayerBase
1083         private static final String PREFIX_COMMAND_CODE_PLAYBACK = "COMMAND_CODE_PLAYBACK_";
1084 
1085         // Prefix for command codes that will be sent directly to the MediaPlaylistAgent
1086         private static final String PREFIX_COMMAND_CODE_PLAYLIST = "COMMAND_CODE_PLAYLIST_";
1087 
1088         private Set<SessionCommand2> mCommands = new HashSet<>();
1089         private final SessionCommandGroup2 mInstance;
1090 
CommandGroupImpl(SessionCommandGroup2 instance, Object other)1091         public CommandGroupImpl(SessionCommandGroup2 instance, Object other) {
1092             mInstance = instance;
1093             if (other != null && other instanceof CommandGroupImpl) {
1094                 mCommands.addAll(((CommandGroupImpl) other).mCommands);
1095             }
1096         }
1097 
CommandGroupImpl()1098         public CommandGroupImpl() {
1099             mInstance = new SessionCommandGroup2(this);
1100         }
1101 
1102         @Override
addCommand_impl(@onNull SessionCommand2 command)1103         public void addCommand_impl(@NonNull SessionCommand2 command) {
1104             if (command == null) {
1105                 throw new IllegalArgumentException("command shouldn't be null");
1106             }
1107             mCommands.add(command);
1108         }
1109 
1110         @Override
addAllPredefinedCommands_impl()1111         public void addAllPredefinedCommands_impl() {
1112             addCommandsWithPrefix(PREFIX_COMMAND_CODE);
1113         }
1114 
addAllPlaybackCommands()1115         void addAllPlaybackCommands() {
1116             addCommandsWithPrefix(PREFIX_COMMAND_CODE_PLAYBACK);
1117         }
1118 
addAllPlaylistCommands()1119         void addAllPlaylistCommands() {
1120             addCommandsWithPrefix(PREFIX_COMMAND_CODE_PLAYLIST);
1121         }
1122 
addCommandsWithPrefix(String prefix)1123         private void addCommandsWithPrefix(String prefix) {
1124             // TODO(jaewan): (Can be post-P): Don't use reflection for this purpose.
1125             final Field[] fields = MediaSession2.class.getFields();
1126             if (fields != null) {
1127                 for (int i = 0; i < fields.length; i++) {
1128                     if (fields[i].getName().startsWith(prefix)) {
1129                         try {
1130                             mCommands.add(new SessionCommand2(fields[i].getInt(null)));
1131                         } catch (IllegalAccessException e) {
1132                             Log.w(TAG, "Unexpected " + fields[i] + " in MediaSession2");
1133                         }
1134                     }
1135                 }
1136             }
1137         }
1138 
1139         @Override
removeCommand_impl(@onNull SessionCommand2 command)1140         public void removeCommand_impl(@NonNull SessionCommand2 command) {
1141             if (command == null) {
1142                 throw new IllegalArgumentException("command shouldn't be null");
1143             }
1144             mCommands.remove(command);
1145         }
1146 
1147         @Override
hasCommand_impl(@onNull SessionCommand2 command)1148         public boolean hasCommand_impl(@NonNull SessionCommand2 command) {
1149             if (command == null) {
1150                 throw new IllegalArgumentException("command shouldn't be null");
1151             }
1152             return mCommands.contains(command);
1153         }
1154 
1155         @Override
hasCommand_impl(int code)1156         public boolean hasCommand_impl(int code) {
1157             if (code == COMMAND_CODE_CUSTOM) {
1158                 throw new IllegalArgumentException("Use hasCommand(Command) for custom command");
1159             }
1160             for (SessionCommand2 command : mCommands) {
1161                 if (command.getCommandCode() == code) {
1162                     return true;
1163                 }
1164             }
1165             return false;
1166         }
1167 
1168         @Override
getCommands_impl()1169         public Set<SessionCommand2> getCommands_impl() {
1170             return getCommands();
1171         }
1172 
getCommands()1173         public Set<SessionCommand2> getCommands() {
1174             return Collections.unmodifiableSet(mCommands);
1175         }
1176 
1177         /**
1178          * @return new bundle from the CommandGroup
1179          * @hide
1180          */
1181         @Override
toBundle_impl()1182         public Bundle toBundle_impl() {
1183             ArrayList<Bundle> list = new ArrayList<>();
1184             for (SessionCommand2 command : mCommands) {
1185                 list.add(command.toBundle());
1186             }
1187             Bundle bundle = new Bundle();
1188             bundle.putParcelableArrayList(KEY_COMMANDS, list);
1189             return bundle;
1190         }
1191 
1192         /**
1193          * @return new instance of CommandGroup from the bundle
1194          * @hide
1195          */
fromBundle_impl(Bundle commands)1196         public static @Nullable SessionCommandGroup2 fromBundle_impl(Bundle commands) {
1197             if (commands == null) {
1198                 return null;
1199             }
1200             List<Parcelable> list = commands.getParcelableArrayList(KEY_COMMANDS);
1201             if (list == null) {
1202                 return null;
1203             }
1204             SessionCommandGroup2 commandGroup = new SessionCommandGroup2();
1205             for (int i = 0; i < list.size(); i++) {
1206                 Parcelable parcelable = list.get(i);
1207                 if (!(parcelable instanceof Bundle)) {
1208                     continue;
1209                 }
1210                 Bundle commandBundle = (Bundle) parcelable;
1211                 SessionCommand2 command = SessionCommand2.fromBundle(commandBundle);
1212                 if (command != null) {
1213                     commandGroup.addCommand(command);
1214                 }
1215             }
1216             return commandGroup;
1217         }
1218     }
1219 
1220     public static class ControllerInfoImpl implements ControllerInfoProvider {
1221         private final ControllerInfo mInstance;
1222         private final int mUid;
1223         private final String mPackageName;
1224         private final boolean mIsTrusted;
1225         private final IMediaController2 mControllerBinder;
1226 
ControllerInfoImpl(Context context, ControllerInfo instance, int uid, int pid, @NonNull String packageName, @NonNull IMediaController2 callback)1227         public ControllerInfoImpl(Context context, ControllerInfo instance, int uid,
1228                 int pid, @NonNull String packageName, @NonNull IMediaController2 callback) {
1229             if (TextUtils.isEmpty(packageName)) {
1230                 throw new IllegalArgumentException("packageName shouldn't be empty");
1231             }
1232             if (callback == null) {
1233                 throw new IllegalArgumentException("callback shouldn't be null");
1234             }
1235 
1236             mInstance = instance;
1237             mUid = uid;
1238             mPackageName = packageName;
1239             mControllerBinder = callback;
1240             MediaSessionManager manager =
1241                   (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
1242             // Ask server whether the controller is trusted.
1243             // App cannot know this because apps cannot query enabled notification listener for
1244             // another package, but system server can do.
1245             mIsTrusted = manager.isTrustedForMediaControl(
1246                     new MediaSessionManager.RemoteUserInfo(packageName, pid, uid));
1247         }
1248 
1249         @Override
getPackageName_impl()1250         public String getPackageName_impl() {
1251             return mPackageName;
1252         }
1253 
1254         @Override
getUid_impl()1255         public int getUid_impl() {
1256             return mUid;
1257         }
1258 
1259         @Override
isTrusted_impl()1260         public boolean isTrusted_impl() {
1261             return mIsTrusted;
1262         }
1263 
1264         @Override
hashCode_impl()1265         public int hashCode_impl() {
1266             return mControllerBinder.hashCode();
1267         }
1268 
1269         @Override
equals_impl(Object obj)1270         public boolean equals_impl(Object obj) {
1271             if (!(obj instanceof ControllerInfo)) {
1272                 return false;
1273             }
1274             return equals(((ControllerInfo) obj).getProvider());
1275         }
1276 
1277         @Override
toString_impl()1278         public String toString_impl() {
1279             return "ControllerInfo {pkg=" + mPackageName + ", uid=" + mUid + ", trusted="
1280                     + mIsTrusted + "}";
1281         }
1282 
1283         @Override
hashCode()1284         public int hashCode() {
1285             return mControllerBinder.hashCode();
1286         }
1287 
1288         @Override
equals(Object obj)1289         public boolean equals(Object obj) {
1290             if (!(obj instanceof ControllerInfoImpl)) {
1291                 return false;
1292             }
1293             ControllerInfoImpl other = (ControllerInfoImpl) obj;
1294             return mControllerBinder.asBinder().equals(other.mControllerBinder.asBinder());
1295         }
1296 
getInstance()1297         ControllerInfo getInstance() {
1298             return mInstance;
1299         }
1300 
getId()1301         IBinder getId() {
1302             return mControllerBinder.asBinder();
1303         }
1304 
getControllerBinder()1305         IMediaController2 getControllerBinder() {
1306             return mControllerBinder;
1307         }
1308 
from(ControllerInfo controller)1309         static ControllerInfoImpl from(ControllerInfo controller) {
1310             return (ControllerInfoImpl) controller.getProvider();
1311         }
1312     }
1313 
1314     public static class CommandButtonImpl implements CommandButtonProvider {
1315         private static final String KEY_COMMAND
1316                 = "android.media.media_session2.command_button.command";
1317         private static final String KEY_ICON_RES_ID
1318                 = "android.media.media_session2.command_button.icon_res_id";
1319         private static final String KEY_DISPLAY_NAME
1320                 = "android.media.media_session2.command_button.display_name";
1321         private static final String KEY_EXTRAS
1322                 = "android.media.media_session2.command_button.extras";
1323         private static final String KEY_ENABLED
1324                 = "android.media.media_session2.command_button.enabled";
1325 
1326         private final CommandButton mInstance;
1327         private SessionCommand2 mCommand;
1328         private int mIconResId;
1329         private String mDisplayName;
1330         private Bundle mExtras;
1331         private boolean mEnabled;
1332 
CommandButtonImpl(@ullable SessionCommand2 command, int iconResId, @Nullable String displayName, Bundle extras, boolean enabled)1333         public CommandButtonImpl(@Nullable SessionCommand2 command, int iconResId,
1334                 @Nullable String displayName, Bundle extras, boolean enabled) {
1335             mCommand = command;
1336             mIconResId = iconResId;
1337             mDisplayName = displayName;
1338             mExtras = extras;
1339             mEnabled = enabled;
1340             mInstance = new CommandButton(this);
1341         }
1342 
1343         @Override
1344         public @Nullable
getCommand_impl()1345         SessionCommand2 getCommand_impl() {
1346             return mCommand;
1347         }
1348 
1349         @Override
getIconResId_impl()1350         public int getIconResId_impl() {
1351             return mIconResId;
1352         }
1353 
1354         @Override
getDisplayName_impl()1355         public @Nullable String getDisplayName_impl() {
1356             return mDisplayName;
1357         }
1358 
1359         @Override
getExtras_impl()1360         public @Nullable Bundle getExtras_impl() {
1361             return mExtras;
1362         }
1363 
1364         @Override
isEnabled_impl()1365         public boolean isEnabled_impl() {
1366             return mEnabled;
1367         }
1368 
toBundle()1369         @NonNull Bundle toBundle() {
1370             Bundle bundle = new Bundle();
1371             bundle.putBundle(KEY_COMMAND, mCommand.toBundle());
1372             bundle.putInt(KEY_ICON_RES_ID, mIconResId);
1373             bundle.putString(KEY_DISPLAY_NAME, mDisplayName);
1374             bundle.putBundle(KEY_EXTRAS, mExtras);
1375             bundle.putBoolean(KEY_ENABLED, mEnabled);
1376             return bundle;
1377         }
1378 
fromBundle(Bundle bundle)1379         static @Nullable CommandButton fromBundle(Bundle bundle) {
1380             if (bundle == null) {
1381                 return null;
1382             }
1383             CommandButton.Builder builder = new CommandButton.Builder();
1384             builder.setCommand(SessionCommand2.fromBundle(bundle.getBundle(KEY_COMMAND)));
1385             builder.setIconResId(bundle.getInt(KEY_ICON_RES_ID, 0));
1386             builder.setDisplayName(bundle.getString(KEY_DISPLAY_NAME));
1387             builder.setExtras(bundle.getBundle(KEY_EXTRAS));
1388             builder.setEnabled(bundle.getBoolean(KEY_ENABLED));
1389             try {
1390                 return builder.build();
1391             } catch (IllegalStateException e) {
1392                 // Malformed or version mismatch. Return null for now.
1393                 return null;
1394             }
1395         }
1396 
1397         /**
1398          * Builder for {@link CommandButton}.
1399          */
1400         public static class BuilderImpl implements CommandButtonProvider.BuilderProvider {
1401             private final CommandButton.Builder mInstance;
1402             private SessionCommand2 mCommand;
1403             private int mIconResId;
1404             private String mDisplayName;
1405             private Bundle mExtras;
1406             private boolean mEnabled;
1407 
BuilderImpl(CommandButton.Builder instance)1408             public BuilderImpl(CommandButton.Builder instance) {
1409                 mInstance = instance;
1410                 mEnabled = true;
1411             }
1412 
1413             @Override
setCommand_impl(SessionCommand2 command)1414             public CommandButton.Builder setCommand_impl(SessionCommand2 command) {
1415                 mCommand = command;
1416                 return mInstance;
1417             }
1418 
1419             @Override
setIconResId_impl(int resId)1420             public CommandButton.Builder setIconResId_impl(int resId) {
1421                 mIconResId = resId;
1422                 return mInstance;
1423             }
1424 
1425             @Override
setDisplayName_impl(String displayName)1426             public CommandButton.Builder setDisplayName_impl(String displayName) {
1427                 mDisplayName = displayName;
1428                 return mInstance;
1429             }
1430 
1431             @Override
setEnabled_impl(boolean enabled)1432             public CommandButton.Builder setEnabled_impl(boolean enabled) {
1433                 mEnabled = enabled;
1434                 return mInstance;
1435             }
1436 
1437             @Override
setExtras_impl(Bundle extras)1438             public CommandButton.Builder setExtras_impl(Bundle extras) {
1439                 mExtras = extras;
1440                 return mInstance;
1441             }
1442 
1443             @Override
build_impl()1444             public CommandButton build_impl() {
1445                 if (mEnabled && mCommand == null) {
1446                     throw new IllegalStateException("Enabled button needs Command"
1447                             + " for controller to invoke the command");
1448                 }
1449                 if (mCommand != null && mCommand.getCommandCode() == COMMAND_CODE_CUSTOM
1450                         && (mIconResId == 0 || TextUtils.isEmpty(mDisplayName))) {
1451                     throw new IllegalStateException("Custom commands needs icon and"
1452                             + " and name to display");
1453                 }
1454                 return new CommandButtonImpl(mCommand, mIconResId, mDisplayName, mExtras, mEnabled)
1455                         .mInstance;
1456             }
1457         }
1458     }
1459 
1460     public static abstract class BuilderBaseImpl<T extends MediaSession2, C extends SessionCallback>
1461             implements BuilderBaseProvider<T, C> {
1462         final Context mContext;
1463         MediaPlayerBase mPlayer;
1464         String mId;
1465         Executor mCallbackExecutor;
1466         C mCallback;
1467         MediaPlaylistAgent mPlaylistAgent;
1468         VolumeProvider2 mVolumeProvider;
1469         PendingIntent mSessionActivity;
1470 
1471         /**
1472          * Constructor.
1473          *
1474          * @param context a context
1475          * @throws IllegalArgumentException if any parameter is null, or the player is a
1476          *      {@link MediaSession2} or {@link MediaController2}.
1477          */
1478         // TODO(jaewan): Also need executor
BuilderBaseImpl(@onNull Context context)1479         public BuilderBaseImpl(@NonNull Context context) {
1480             if (context == null) {
1481                 throw new IllegalArgumentException("context shouldn't be null");
1482             }
1483             mContext = context;
1484             // Ensure non-null
1485             mId = "";
1486         }
1487 
1488         @Override
setPlayer_impl(@onNull MediaPlayerBase player)1489         public void setPlayer_impl(@NonNull MediaPlayerBase player) {
1490             if (player == null) {
1491                 throw new IllegalArgumentException("player shouldn't be null");
1492             }
1493             mPlayer = player;
1494         }
1495 
1496         @Override
setPlaylistAgent_impl(@onNull MediaPlaylistAgent playlistAgent)1497         public void setPlaylistAgent_impl(@NonNull MediaPlaylistAgent playlistAgent) {
1498             if (playlistAgent == null) {
1499                 throw new IllegalArgumentException("playlistAgent shouldn't be null");
1500             }
1501             mPlaylistAgent = playlistAgent;
1502         }
1503 
1504         @Override
setVolumeProvider_impl(VolumeProvider2 volumeProvider)1505         public void setVolumeProvider_impl(VolumeProvider2 volumeProvider) {
1506             mVolumeProvider = volumeProvider;
1507         }
1508 
1509         @Override
setSessionActivity_impl(PendingIntent pi)1510         public void setSessionActivity_impl(PendingIntent pi) {
1511             mSessionActivity = pi;
1512         }
1513 
1514         @Override
setId_impl(@onNull String id)1515         public void setId_impl(@NonNull String id) {
1516             if (id == null) {
1517                 throw new IllegalArgumentException("id shouldn't be null");
1518             }
1519             mId = id;
1520         }
1521 
1522         @Override
setSessionCallback_impl(@onNull Executor executor, @NonNull C callback)1523         public void setSessionCallback_impl(@NonNull Executor executor, @NonNull C callback) {
1524             if (executor == null) {
1525                 throw new IllegalArgumentException("executor shouldn't be null");
1526             }
1527             if (callback == null) {
1528                 throw new IllegalArgumentException("callback shouldn't be null");
1529             }
1530             mCallbackExecutor = executor;
1531             mCallback = callback;
1532         }
1533 
1534         @Override
build_impl()1535         public abstract T build_impl();
1536     }
1537 
1538     public static class BuilderImpl extends BuilderBaseImpl<MediaSession2, SessionCallback> {
BuilderImpl(Context context, Builder instance)1539         public BuilderImpl(Context context, Builder instance) {
1540             super(context);
1541         }
1542 
1543         @Override
build_impl()1544         public MediaSession2 build_impl() {
1545             if (mCallbackExecutor == null) {
1546                 mCallbackExecutor = mContext.getMainExecutor();
1547             }
1548             if (mCallback == null) {
1549                 mCallback = new SessionCallback() {};
1550             }
1551             return new MediaSession2Impl(mContext, mPlayer, mId, mPlaylistAgent,
1552                     mVolumeProvider, mSessionActivity, mCallbackExecutor, mCallback).getInstance();
1553         }
1554     }
1555 }
1556