1/*
2 * Copyright 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.media;
18
19import static android.media.SessionCommand2.COMMAND_CODE_CUSTOM;
20import static android.media.SessionToken2.TYPE_LIBRARY_SERVICE;
21import static android.media.SessionToken2.TYPE_SESSION;
22import static android.media.SessionToken2.TYPE_SESSION_SERVICE;
23
24import android.annotation.NonNull;
25import android.annotation.Nullable;
26import android.app.PendingIntent;
27import android.content.Context;
28import android.content.Intent;
29import android.content.pm.PackageManager;
30import android.content.pm.ResolveInfo;
31import android.media.AudioAttributes;
32import android.media.AudioFocusRequest;
33import android.media.AudioManager;
34import android.media.DataSourceDesc;
35import android.media.MediaController2;
36import android.media.MediaController2.PlaybackInfo;
37import android.media.MediaItem2;
38import android.media.MediaLibraryService2;
39import android.media.MediaMetadata2;
40import android.media.MediaPlayerBase;
41import android.media.MediaPlayerBase.PlayerEventCallback;
42import android.media.MediaPlayerBase.PlayerState;
43import android.media.MediaPlaylistAgent;
44import android.media.MediaPlaylistAgent.PlaylistEventCallback;
45import android.media.MediaSession2;
46import android.media.MediaSession2.Builder;
47import android.media.SessionCommand2;
48import android.media.MediaSession2.CommandButton;
49import android.media.SessionCommandGroup2;
50import android.media.MediaSession2.ControllerInfo;
51import android.media.MediaSession2.OnDataSourceMissingHelper;
52import android.media.MediaSession2.SessionCallback;
53import android.media.MediaSessionService2;
54import android.media.SessionToken2;
55import android.media.VolumeProvider2;
56import android.media.session.MediaSessionManager;
57import android.media.update.MediaSession2Provider;
58import android.os.Bundle;
59import android.os.IBinder;
60import android.os.Parcelable;
61import android.os.Process;
62import android.os.ResultReceiver;
63import android.support.annotation.GuardedBy;
64import android.text.TextUtils;
65import android.util.Log;
66
67import java.lang.ref.WeakReference;
68import java.lang.reflect.Field;
69import java.util.ArrayList;
70import java.util.Collections;
71import java.util.HashSet;
72import java.util.List;
73import java.util.NoSuchElementException;
74import java.util.Set;
75import java.util.concurrent.Executor;
76
77public 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     */
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
186    MediaSession2 createInstance() {
187        return new MediaSession2(this);
188    }
189
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
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
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
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
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
333    public MediaPlayerBase getPlayer_impl() {
334        return getPlayer();
335    }
336
337    @Override
338    public MediaPlaylistAgent getPlaylistAgent_impl() {
339        return mPlaylistAgent;
340    }
341
342    @Override
343    public VolumeProvider2 getVolumeProvider_impl() {
344        return mVolumeProvider;
345    }
346
347    @Override
348    public SessionToken2 getToken_impl() {
349        return mSessionToken;
350    }
351
352    @Override
353    public List<ControllerInfo> getConnectedControllers_impl() {
354        return mSessionStub.getControllers();
355    }
356
357    @Override
358    public void setAudioFocusRequest_impl(AudioFocusRequest afr) {
359        // implement
360    }
361
362    @Override
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
569    public MediaItem2 getCurrentPlaylistItem_impl() {
570        // TODO(jaewan): Implement
571        return null;
572    }
573
574    @Override
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
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
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
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
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
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
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
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
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
672    public void notifyError_impl(int errorCode, Bundle extras) {
673        mSessionStub.notifyError(errorCode, extras);
674    }
675
676    @Override
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
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()}.
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
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
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
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
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
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
792    Context getContext() {
793        return mContext;
794    }
795
796    MediaSession2 getInstance() {
797        return mInstance;
798    }
799
800    MediaPlayerBase getPlayer() {
801        return mPlayer;
802    }
803
804    MediaPlaylistAgent getPlaylistAgent() {
805        return mPlaylistAgent;
806    }
807
808    Executor getCallbackExecutor() {
809        return mCallbackExecutor;
810    }
811
812    SessionCallback getCallback() {
813        return mCallback;
814    }
815
816    MediaSession2Stub getSessionStub() {
817        return mSessionStub;
818    }
819
820    VolumeProvider2 getVolumeProvider() {
821        return mVolumeProvider;
822    }
823
824    PlaybackInfo getPlaybackInfo() {
825        synchronized (mLock) {
826            return mPlaybackInfo;
827        }
828    }
829
830    PendingIntent getSessionActivity() {
831        return mSessionActivity;
832    }
833
834    private static class MyPlayerEventCallback extends PlayerEventCallback {
835        private final WeakReference<MediaSession2Impl> mSession;
836
837        private MyPlayerEventCallback(MediaSession2Impl session) {
838            mSession = new WeakReference<>(session);
839        }
840
841        @Override
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
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
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
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
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
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
932        private MyPlaylistEventCallback(MediaSession2Impl session) {
933            mSession = new WeakReference<>(session);
934        }
935
936        @Override
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
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
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
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
989        public CommandImpl(SessionCommand2 instance, int commandCode) {
990            mInstance = instance;
991            mCommandCode = commandCode;
992            mCustomCommand = null;
993            mExtras = null;
994        }
995
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
1008        public int getCommandCode_impl() {
1009            return mCommandCode;
1010        }
1011
1012        @Override
1013        public @Nullable String getCustomCommand_impl() {
1014            return mCustomCommand;
1015        }
1016
1017        @Override
1018        public @Nullable Bundle getExtras_impl() {
1019            return mExtras;
1020        }
1021
1022        /**
1023         * @return a new Bundle instance from the Command
1024         */
1025        @Override
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         */
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
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
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
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
1098        public CommandGroupImpl() {
1099            mInstance = new SessionCommandGroup2(this);
1100        }
1101
1102        @Override
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
1111        public void addAllPredefinedCommands_impl() {
1112            addCommandsWithPrefix(PREFIX_COMMAND_CODE);
1113        }
1114
1115        void addAllPlaybackCommands() {
1116            addCommandsWithPrefix(PREFIX_COMMAND_CODE_PLAYBACK);
1117        }
1118
1119        void addAllPlaylistCommands() {
1120            addCommandsWithPrefix(PREFIX_COMMAND_CODE_PLAYLIST);
1121        }
1122
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
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
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
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
1169        public Set<SessionCommand2> getCommands_impl() {
1170            return getCommands();
1171        }
1172
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
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         */
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
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
1250        public String getPackageName_impl() {
1251            return mPackageName;
1252        }
1253
1254        @Override
1255        public int getUid_impl() {
1256            return mUid;
1257        }
1258
1259        @Override
1260        public boolean isTrusted_impl() {
1261            return mIsTrusted;
1262        }
1263
1264        @Override
1265        public int hashCode_impl() {
1266            return mControllerBinder.hashCode();
1267        }
1268
1269        @Override
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
1278        public String toString_impl() {
1279            return "ControllerInfo {pkg=" + mPackageName + ", uid=" + mUid + ", trusted="
1280                    + mIsTrusted + "}";
1281        }
1282
1283        @Override
1284        public int hashCode() {
1285            return mControllerBinder.hashCode();
1286        }
1287
1288        @Override
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
1297        ControllerInfo getInstance() {
1298            return mInstance;
1299        }
1300
1301        IBinder getId() {
1302            return mControllerBinder.asBinder();
1303        }
1304
1305        IMediaController2 getControllerBinder() {
1306            return mControllerBinder;
1307        }
1308
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
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
1345        SessionCommand2 getCommand_impl() {
1346            return mCommand;
1347        }
1348
1349        @Override
1350        public int getIconResId_impl() {
1351            return mIconResId;
1352        }
1353
1354        @Override
1355        public @Nullable String getDisplayName_impl() {
1356            return mDisplayName;
1357        }
1358
1359        @Override
1360        public @Nullable Bundle getExtras_impl() {
1361            return mExtras;
1362        }
1363
1364        @Override
1365        public boolean isEnabled_impl() {
1366            return mEnabled;
1367        }
1368
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
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
1408            public BuilderImpl(CommandButton.Builder instance) {
1409                mInstance = instance;
1410                mEnabled = true;
1411            }
1412
1413            @Override
1414            public CommandButton.Builder setCommand_impl(SessionCommand2 command) {
1415                mCommand = command;
1416                return mInstance;
1417            }
1418
1419            @Override
1420            public CommandButton.Builder setIconResId_impl(int resId) {
1421                mIconResId = resId;
1422                return mInstance;
1423            }
1424
1425            @Override
1426            public CommandButton.Builder setDisplayName_impl(String displayName) {
1427                mDisplayName = displayName;
1428                return mInstance;
1429            }
1430
1431            @Override
1432            public CommandButton.Builder setEnabled_impl(boolean enabled) {
1433                mEnabled = enabled;
1434                return mInstance;
1435            }
1436
1437            @Override
1438            public CommandButton.Builder setExtras_impl(Bundle extras) {
1439                mExtras = extras;
1440                return mInstance;
1441            }
1442
1443            @Override
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
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
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
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
1505        public void setVolumeProvider_impl(VolumeProvider2 volumeProvider) {
1506            mVolumeProvider = volumeProvider;
1507        }
1508
1509        @Override
1510        public void setSessionActivity_impl(PendingIntent pi) {
1511            mSessionActivity = pi;
1512        }
1513
1514        @Override
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
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
1535        public abstract T build_impl();
1536    }
1537
1538    public static class BuilderImpl extends BuilderBaseImpl<MediaSession2, SessionCallback> {
1539        public BuilderImpl(Context context, Builder instance) {
1540            super(context);
1541        }
1542
1543        @Override
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