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_SET_VOLUME;
20import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM;
21import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM;
22import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM;
23import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST;
24import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST_METADATA;
25import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE;
26import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE;
27import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID;
28import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH;
29import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI;
30import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID;
31import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH;
32import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI;
33
34import android.app.PendingIntent;
35import android.content.ComponentName;
36import android.content.Context;
37import android.content.Intent;
38import android.content.ServiceConnection;
39import android.media.AudioAttributes;
40import android.media.MediaController2;
41import android.media.MediaController2.ControllerCallback;
42import android.media.MediaController2.PlaybackInfo;
43import android.media.MediaItem2;
44import android.media.MediaMetadata2;
45import android.media.MediaPlaylistAgent.RepeatMode;
46import android.media.MediaPlaylistAgent.ShuffleMode;
47import android.media.SessionCommand2;
48import android.media.MediaSession2.CommandButton;
49import android.media.SessionCommandGroup2;
50import android.media.MediaSessionService2;
51import android.media.Rating2;
52import android.media.SessionToken2;
53import android.media.update.MediaController2Provider;
54import android.net.Uri;
55import android.os.Bundle;
56import android.os.IBinder;
57import android.os.Process;
58import android.os.RemoteException;
59import android.os.ResultReceiver;
60import android.os.UserHandle;
61import android.support.annotation.GuardedBy;
62import android.text.TextUtils;
63import android.util.Log;
64
65import java.util.ArrayList;
66import java.util.List;
67import java.util.concurrent.Executor;
68
69public class MediaController2Impl implements MediaController2Provider {
70    private static final String TAG = "MediaController2";
71    private static final boolean DEBUG = true; // TODO(jaewan): Change
72
73    private final MediaController2 mInstance;
74    private final Context mContext;
75    private final Object mLock = new Object();
76
77    private final MediaController2Stub mControllerStub;
78    private final SessionToken2 mToken;
79    private final ControllerCallback mCallback;
80    private final Executor mCallbackExecutor;
81    private final IBinder.DeathRecipient mDeathRecipient;
82
83    @GuardedBy("mLock")
84    private SessionServiceConnection mServiceConnection;
85    @GuardedBy("mLock")
86    private boolean mIsReleased;
87    @GuardedBy("mLock")
88    private List<MediaItem2> mPlaylist;
89    @GuardedBy("mLock")
90    private MediaMetadata2 mPlaylistMetadata;
91    @GuardedBy("mLock")
92    private @RepeatMode int mRepeatMode;
93    @GuardedBy("mLock")
94    private @ShuffleMode int mShuffleMode;
95    @GuardedBy("mLock")
96    private int mPlayerState;
97    @GuardedBy("mLock")
98    private long mPositionEventTimeMs;
99    @GuardedBy("mLock")
100    private long mPositionMs;
101    @GuardedBy("mLock")
102    private float mPlaybackSpeed;
103    @GuardedBy("mLock")
104    private long mBufferedPositionMs;
105    @GuardedBy("mLock")
106    private PlaybackInfo mPlaybackInfo;
107    @GuardedBy("mLock")
108    private PendingIntent mSessionActivity;
109    @GuardedBy("mLock")
110    private SessionCommandGroup2 mAllowedCommands;
111
112    // Assignment should be used with the lock hold, but should be used without a lock to prevent
113    // potential deadlock.
114    // Postfix -Binder is added to explicitly show that it's potentially remote process call.
115    // Technically -Interface is more correct, but it may misread that it's interface (vs class)
116    // so let's keep this postfix until we find better postfix.
117    @GuardedBy("mLock")
118    private volatile IMediaSession2 mSessionBinder;
119
120    // TODO(jaewan): Require session activeness changed listener, because controller can be
121    //               available when the session's player is null.
122    public MediaController2Impl(Context context, MediaController2 instance, SessionToken2 token,
123            Executor executor, ControllerCallback callback) {
124        mInstance = instance;
125        if (context == null) {
126            throw new IllegalArgumentException("context shouldn't be null");
127        }
128        if (token == null) {
129            throw new IllegalArgumentException("token shouldn't be null");
130        }
131        if (callback == null) {
132            throw new IllegalArgumentException("callback shouldn't be null");
133        }
134        if (executor == null) {
135            throw new IllegalArgumentException("executor shouldn't be null");
136        }
137        mContext = context;
138        mControllerStub = new MediaController2Stub(this);
139        mToken = token;
140        mCallback = callback;
141        mCallbackExecutor = executor;
142        mDeathRecipient = () -> {
143            mInstance.close();
144        };
145
146        mSessionBinder = null;
147    }
148
149    @Override
150    public void initialize() {
151        // TODO(jaewan): More sanity checks.
152        if (mToken.getType() == SessionToken2.TYPE_SESSION) {
153            // Session
154            mServiceConnection = null;
155            connectToSession(SessionToken2Impl.from(mToken).getSessionBinder());
156        } else {
157            // Session service
158            if (Process.myUid() == Process.SYSTEM_UID) {
159                // It's system server (MediaSessionService) that wants to monitor session.
160                // Don't bind if able..
161                IMediaSession2 binder = SessionToken2Impl.from(mToken).getSessionBinder();
162                if (binder != null) {
163                    // Use binder in the session token instead of bind by its own.
164                    // Otherwise server will holds the binding to the service *forever* and service
165                    // will never stop.
166                    mServiceConnection = null;
167                    connectToSession(SessionToken2Impl.from(mToken).getSessionBinder());
168                    return;
169                } else if (DEBUG) {
170                    // Should happen only when system server wants to dispatch media key events to
171                    // a dead service.
172                    Log.d(TAG, "System server binds to a session service. Should unbind"
173                            + " immediately after the use.");
174                }
175            }
176            mServiceConnection = new SessionServiceConnection();
177            connectToService();
178        }
179    }
180
181    private void connectToService() {
182        // Service. Needs to get fresh binder whenever connection is needed.
183        SessionToken2Impl impl = SessionToken2Impl.from(mToken);
184        final Intent intent = new Intent(MediaSessionService2.SERVICE_INTERFACE);
185        intent.setClassName(mToken.getPackageName(), impl.getServiceName());
186
187        // Use bindService() instead of startForegroundService() to start session service for three
188        // reasons.
189        // 1. Prevent session service owner's stopSelf() from destroying service.
190        //    With the startForegroundService(), service's call of stopSelf() will trigger immediate
191        //    onDestroy() calls on the main thread even when onConnect() is running in another
192        //    thread.
193        // 2. Minimize APIs for developers to take care about.
194        //    With bindService(), developers only need to take care about Service.onBind()
195        //    but Service.onStartCommand() should be also taken care about with the
196        //    startForegroundService().
197        // 3. Future support for UI-less playback
198        //    If a service wants to keep running, it should be either foreground service or
199        //    bounded service. But there had been request for the feature for system apps
200        //    and using bindService() will be better fit with it.
201        boolean result;
202        if (Process.myUid() == Process.SYSTEM_UID) {
203            // Use bindServiceAsUser() for binding from system service to avoid following warning.
204            // ContextImpl: Calling a method in the system process without a qualified user
205            result = mContext.bindServiceAsUser(intent, mServiceConnection, Context.BIND_AUTO_CREATE,
206                    UserHandle.getUserHandleForUid(mToken.getUid()));
207        } else {
208            result = mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
209        }
210        if (!result) {
211            Log.w(TAG, "bind to " + mToken + " failed");
212        } else if (DEBUG) {
213            Log.d(TAG, "bind to " + mToken + " success");
214        }
215    }
216
217    private void connectToSession(IMediaSession2 sessionBinder) {
218        try {
219            sessionBinder.connect(mControllerStub, mContext.getPackageName());
220        } catch (RemoteException e) {
221            Log.w(TAG, "Failed to call connection request. Framework will retry"
222                    + " automatically");
223        }
224    }
225
226    @Override
227    public void close_impl() {
228        if (DEBUG) {
229            Log.d(TAG, "release from " + mToken);
230        }
231        final IMediaSession2 binder;
232        synchronized (mLock) {
233            if (mIsReleased) {
234                // Prevent re-enterance from the ControllerCallback.onDisconnected()
235                return;
236            }
237            mIsReleased = true;
238            if (mServiceConnection != null) {
239                mContext.unbindService(mServiceConnection);
240                mServiceConnection = null;
241            }
242            binder = mSessionBinder;
243            mSessionBinder = null;
244            mControllerStub.destroy();
245        }
246        if (binder != null) {
247            try {
248                binder.asBinder().unlinkToDeath(mDeathRecipient, 0);
249                binder.release(mControllerStub);
250            } catch (RemoteException e) {
251                // No-op.
252            }
253        }
254        mCallbackExecutor.execute(() -> {
255            mCallback.onDisconnected(mInstance);
256        });
257    }
258
259    IMediaSession2 getSessionBinder() {
260        return mSessionBinder;
261    }
262
263    MediaController2Stub getControllerStub() {
264        return mControllerStub;
265    }
266
267    Executor getCallbackExecutor() {
268        return mCallbackExecutor;
269    }
270
271    Context getContext() {
272        return mContext;
273    }
274
275    MediaController2 getInstance() {
276        return mInstance;
277    }
278
279    // Returns session binder if the controller can send the command.
280    IMediaSession2 getSessionBinderIfAble(int commandCode) {
281        synchronized (mLock) {
282            if (!mAllowedCommands.hasCommand(commandCode)) {
283                // Cannot send because isn't allowed to.
284                Log.w(TAG, "Controller isn't allowed to call command, commandCode="
285                        + commandCode);
286                return null;
287            }
288        }
289        // TODO(jaewan): Should we do this with the lock hold?
290        final IMediaSession2 binder = mSessionBinder;
291        if (binder == null) {
292            // Cannot send because disconnected.
293            Log.w(TAG, "Session is disconnected");
294        }
295        return binder;
296    }
297
298    // Returns session binder if the controller can send the command.
299    IMediaSession2 getSessionBinderIfAble(SessionCommand2 command) {
300        synchronized (mLock) {
301            if (!mAllowedCommands.hasCommand(command)) {
302                Log.w(TAG, "Controller isn't allowed to call command, command=" + command);
303                return null;
304            }
305        }
306        // TODO(jaewan): Should we do this with the lock hold?
307        final IMediaSession2 binder = mSessionBinder;
308        if (binder == null) {
309            // Cannot send because disconnected.
310            Log.w(TAG, "Session is disconnected");
311        }
312        return binder;
313    }
314
315    @Override
316    public SessionToken2 getSessionToken_impl() {
317        return mToken;
318    }
319
320    @Override
321    public boolean isConnected_impl() {
322        final IMediaSession2 binder = mSessionBinder;
323        return binder != null;
324    }
325
326    @Override
327    public void play_impl() {
328        sendTransportControlCommand(SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY);
329    }
330
331    @Override
332    public void pause_impl() {
333        sendTransportControlCommand(SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE);
334    }
335
336    @Override
337    public void stop_impl() {
338        sendTransportControlCommand(SessionCommand2.COMMAND_CODE_PLAYBACK_STOP);
339    }
340
341    @Override
342    public void skipToPlaylistItem_impl(MediaItem2 item) {
343        if (item == null) {
344            throw new IllegalArgumentException("item shouldn't be null");
345        }
346        final IMediaSession2 binder = mSessionBinder;
347        if (binder != null) {
348            try {
349                binder.skipToPlaylistItem(mControllerStub, item.toBundle());
350            } catch (RemoteException e) {
351                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
352            }
353        } else {
354            Log.w(TAG, "Session isn't active", new IllegalStateException());
355        }
356    }
357
358    @Override
359    public void skipToPreviousItem_impl() {
360        final IMediaSession2 binder = mSessionBinder;
361        if (binder != null) {
362            try {
363                binder.skipToPreviousItem(mControllerStub);
364            } catch (RemoteException e) {
365                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
366            }
367        } else {
368            Log.w(TAG, "Session isn't active", new IllegalStateException());
369        }
370    }
371
372    @Override
373    public void skipToNextItem_impl() {
374        final IMediaSession2 binder = mSessionBinder;
375        if (binder != null) {
376            try {
377                binder.skipToNextItem(mControllerStub);
378            } catch (RemoteException e) {
379                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
380            }
381        } else {
382            Log.w(TAG, "Session isn't active", new IllegalStateException());
383        }
384    }
385
386    private void sendTransportControlCommand(int commandCode) {
387        sendTransportControlCommand(commandCode, null);
388    }
389
390    private void sendTransportControlCommand(int commandCode, Bundle args) {
391        final IMediaSession2 binder = mSessionBinder;
392        if (binder != null) {
393            try {
394                binder.sendTransportControlCommand(mControllerStub, commandCode, args);
395            } catch (RemoteException e) {
396                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
397            }
398        } else {
399            Log.w(TAG, "Session isn't active", new IllegalStateException());
400        }
401    }
402
403    @Override
404    public PendingIntent getSessionActivity_impl() {
405        return mSessionActivity;
406    }
407
408    @Override
409    public void setVolumeTo_impl(int value, int flags) {
410        // TODO(hdmoon): sanity check
411        final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_SET_VOLUME);
412        if (binder != null) {
413            try {
414                binder.setVolumeTo(mControllerStub, value, flags);
415            } catch (RemoteException e) {
416                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
417            }
418        } else {
419            Log.w(TAG, "Session isn't active", new IllegalStateException());
420        }
421    }
422
423    @Override
424    public void adjustVolume_impl(int direction, int flags) {
425        // TODO(hdmoon): sanity check
426        final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_SET_VOLUME);
427        if (binder != null) {
428            try {
429                binder.adjustVolume(mControllerStub, direction, flags);
430            } catch (RemoteException e) {
431                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
432            }
433        } else {
434            Log.w(TAG, "Session isn't active", new IllegalStateException());
435        }
436    }
437
438    @Override
439    public void prepareFromUri_impl(Uri uri, Bundle extras) {
440        final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_SESSION_PREPARE_FROM_URI);
441        if (uri == null) {
442            throw new IllegalArgumentException("uri shouldn't be null");
443        }
444        if (binder != null) {
445            try {
446                binder.prepareFromUri(mControllerStub, uri, extras);
447            } catch (RemoteException e) {
448                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
449            }
450        } else {
451            // TODO(jaewan): Handle.
452        }
453    }
454
455    @Override
456    public void prepareFromSearch_impl(String query, Bundle extras) {
457        final IMediaSession2 binder = getSessionBinderIfAble(
458                COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH);
459        if (TextUtils.isEmpty(query)) {
460            throw new IllegalArgumentException("query shouldn't be empty");
461        }
462        if (binder != null) {
463            try {
464                binder.prepareFromSearch(mControllerStub, query, extras);
465            } catch (RemoteException e) {
466                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
467            }
468        } else {
469            // TODO(jaewan): Handle.
470        }
471    }
472
473    @Override
474    public void prepareFromMediaId_impl(String mediaId, Bundle extras) {
475        final IMediaSession2 binder = getSessionBinderIfAble(
476                COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID);
477        if (mediaId == null) {
478            throw new IllegalArgumentException("mediaId shouldn't be null");
479        }
480        if (binder != null) {
481            try {
482                binder.prepareFromMediaId(mControllerStub, mediaId, extras);
483            } catch (RemoteException e) {
484                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
485            }
486        } else {
487            // TODO(jaewan): Handle.
488        }
489    }
490
491    @Override
492    public void playFromUri_impl(Uri uri, Bundle extras) {
493        final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_SESSION_PLAY_FROM_URI);
494        if (uri == null) {
495            throw new IllegalArgumentException("uri shouldn't be null");
496        }
497        if (binder != null) {
498            try {
499                binder.playFromUri(mControllerStub, uri, extras);
500            } catch (RemoteException e) {
501                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
502            }
503        } else {
504            // TODO(jaewan): Handle.
505        }
506    }
507
508    @Override
509    public void playFromSearch_impl(String query, Bundle extras) {
510        final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_SESSION_PLAY_FROM_SEARCH);
511        if (TextUtils.isEmpty(query)) {
512            throw new IllegalArgumentException("query shouldn't be empty");
513        }
514        if (binder != null) {
515            try {
516                binder.playFromSearch(mControllerStub, query, extras);
517            } catch (RemoteException e) {
518                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
519            }
520        } else {
521            // TODO(jaewan): Handle.
522        }
523    }
524
525    @Override
526    public void playFromMediaId_impl(String mediaId, Bundle extras) {
527        final IMediaSession2 binder = getSessionBinderIfAble(
528                COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID);
529        if (mediaId == null) {
530            throw new IllegalArgumentException("mediaId shouldn't be null");
531        }
532        if (binder != null) {
533            try {
534                binder.playFromMediaId(mControllerStub, mediaId, extras);
535            } catch (RemoteException e) {
536                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
537            }
538        } else {
539            // TODO(jaewan): Handle.
540        }
541    }
542
543    @Override
544    public void setRating_impl(String mediaId, Rating2 rating) {
545        if (mediaId == null) {
546            throw new IllegalArgumentException("mediaId shouldn't be null");
547        }
548        if (rating == null) {
549            throw new IllegalArgumentException("rating shouldn't be null");
550        }
551
552        final IMediaSession2 binder = mSessionBinder;
553        if (binder != null) {
554            try {
555                binder.setRating(mControllerStub, mediaId, rating.toBundle());
556            } catch (RemoteException e) {
557                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
558            }
559        } else {
560            // TODO(jaewan): Handle.
561        }
562    }
563
564    @Override
565    public void sendCustomCommand_impl(SessionCommand2 command, Bundle args, ResultReceiver cb) {
566        if (command == null) {
567            throw new IllegalArgumentException("command shouldn't be null");
568        }
569        final IMediaSession2 binder = getSessionBinderIfAble(command);
570        if (binder != null) {
571            try {
572                binder.sendCustomCommand(mControllerStub, command.toBundle(), args, cb);
573            } catch (RemoteException e) {
574                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
575            }
576        } else {
577            Log.w(TAG, "Session isn't active", new IllegalStateException());
578        }
579    }
580
581    @Override
582    public List<MediaItem2> getPlaylist_impl() {
583        synchronized (mLock) {
584            return mPlaylist;
585        }
586    }
587
588    @Override
589    public void setPlaylist_impl(List<MediaItem2> list, MediaMetadata2 metadata) {
590        if (list == null) {
591            throw new IllegalArgumentException("list shouldn't be null");
592        }
593        final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_PLAYLIST_SET_LIST);
594        if (binder != null) {
595            List<Bundle> bundleList = new ArrayList<>();
596            for (int i = 0; i < list.size(); i++) {
597                bundleList.add(list.get(i).toBundle());
598            }
599            Bundle metadataBundle = (metadata == null) ? null : metadata.toBundle();
600            try {
601                binder.setPlaylist(mControllerStub, bundleList, metadataBundle);
602            } catch (RemoteException e) {
603                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
604            }
605        } else {
606            Log.w(TAG, "Session isn't active", new IllegalStateException());
607        }
608    }
609
610    @Override
611    public MediaMetadata2 getPlaylistMetadata_impl() {
612        synchronized (mLock) {
613            return mPlaylistMetadata;
614        }
615    }
616
617    @Override
618    public void updatePlaylistMetadata_impl(MediaMetadata2 metadata) {
619        final IMediaSession2 binder = getSessionBinderIfAble(
620                COMMAND_CODE_PLAYLIST_SET_LIST_METADATA);
621        if (binder != null) {
622            Bundle metadataBundle = (metadata == null) ? null : metadata.toBundle();
623            try {
624                binder.updatePlaylistMetadata(mControllerStub, metadataBundle);
625            } catch (RemoteException e) {
626                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
627            }
628        } else {
629            Log.w(TAG, "Session isn't active", new IllegalStateException());
630        }
631    }
632
633    @Override
634    public void prepare_impl() {
635        sendTransportControlCommand(SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE);
636    }
637
638    @Override
639    public void fastForward_impl() {
640        // TODO(jaewan): Implement this. Note that fast forward isn't a transport command anymore
641        //sendTransportControlCommand(MediaSession2.COMMAND_CODE_SESSION_FAST_FORWARD);
642    }
643
644    @Override
645    public void rewind_impl() {
646        // TODO(jaewan): Implement this. Note that rewind isn't a transport command anymore
647        //sendTransportControlCommand(MediaSession2.COMMAND_CODE_SESSION_REWIND);
648    }
649
650    @Override
651    public void seekTo_impl(long pos) {
652        if (pos < 0) {
653            throw new IllegalArgumentException("position shouldn't be negative");
654        }
655        Bundle args = new Bundle();
656        args.putLong(MediaSession2Stub.ARGUMENT_KEY_POSITION, pos);
657        sendTransportControlCommand(SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO, args);
658    }
659
660    @Override
661    public void addPlaylistItem_impl(int index, MediaItem2 item) {
662        if (index < 0) {
663            throw new IllegalArgumentException("index shouldn't be negative");
664        }
665        if (item == null) {
666            throw new IllegalArgumentException("item shouldn't be null");
667        }
668        final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_PLAYLIST_ADD_ITEM);
669        if (binder != null) {
670            try {
671                binder.addPlaylistItem(mControllerStub, index, item.toBundle());
672            } catch (RemoteException e) {
673                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
674            }
675        } else {
676            Log.w(TAG, "Session isn't active", new IllegalStateException());
677        }
678    }
679
680    @Override
681    public void removePlaylistItem_impl(MediaItem2 item) {
682        if (item == null) {
683            throw new IllegalArgumentException("item shouldn't be null");
684        }
685        final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_PLAYLIST_REMOVE_ITEM);
686        if (binder != null) {
687            try {
688                binder.removePlaylistItem(mControllerStub, item.toBundle());
689            } catch (RemoteException e) {
690                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
691            }
692        } else {
693            Log.w(TAG, "Session isn't active", new IllegalStateException());
694        }
695    }
696
697    @Override
698    public void replacePlaylistItem_impl(int index, MediaItem2 item) {
699        if (index < 0) {
700            throw new IllegalArgumentException("index shouldn't be negative");
701        }
702        if (item == null) {
703            throw new IllegalArgumentException("item shouldn't be null");
704        }
705        final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_PLAYLIST_REPLACE_ITEM);
706        if (binder != null) {
707            try {
708                binder.replacePlaylistItem(mControllerStub, index, item.toBundle());
709            } catch (RemoteException e) {
710                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
711            }
712        } else {
713            Log.w(TAG, "Session isn't active", new IllegalStateException());
714        }
715    }
716
717    @Override
718    public int getShuffleMode_impl() {
719        return mShuffleMode;
720    }
721
722    @Override
723    public void setShuffleMode_impl(int shuffleMode) {
724        final IMediaSession2 binder = getSessionBinderIfAble(
725                COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE);
726        if (binder != null) {
727            try {
728                binder.setShuffleMode(mControllerStub, shuffleMode);
729            } catch (RemoteException e) {
730                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
731            }
732        } else {
733            Log.w(TAG, "Session isn't active", new IllegalStateException());
734        }
735    }
736
737    @Override
738    public int getRepeatMode_impl() {
739        return mRepeatMode;
740    }
741
742    @Override
743    public void setRepeatMode_impl(int repeatMode) {
744        final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE);
745        if (binder != null) {
746            try {
747                binder.setRepeatMode(mControllerStub, repeatMode);
748            } catch (RemoteException e) {
749                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
750            }
751        } else {
752            Log.w(TAG, "Session isn't active", new IllegalStateException());
753        }
754    }
755
756    @Override
757    public PlaybackInfo getPlaybackInfo_impl() {
758        synchronized (mLock) {
759            return mPlaybackInfo;
760        }
761    }
762
763    @Override
764    public int getPlayerState_impl() {
765        synchronized (mLock) {
766            return mPlayerState;
767        }
768    }
769
770    @Override
771    public long getCurrentPosition_impl() {
772        synchronized (mLock) {
773            long timeDiff = System.currentTimeMillis() - mPositionEventTimeMs;
774            long expectedPosition = mPositionMs + (long) (mPlaybackSpeed * timeDiff);
775            return Math.max(0, expectedPosition);
776        }
777    }
778
779    @Override
780    public float getPlaybackSpeed_impl() {
781        synchronized (mLock) {
782            return mPlaybackSpeed;
783        }
784    }
785
786    @Override
787    public long getBufferedPosition_impl() {
788        synchronized (mLock) {
789            return mBufferedPositionMs;
790        }
791    }
792
793    @Override
794    public MediaItem2 getCurrentMediaItem_impl() {
795        // TODO(jaewan): Implement
796        return null;
797    }
798
799    void pushPlayerStateChanges(final int state) {
800        synchronized (mLock) {
801            mPlayerState = state;
802        }
803        mCallbackExecutor.execute(() -> {
804            if (!mInstance.isConnected()) {
805                return;
806            }
807            mCallback.onPlayerStateChanged(mInstance, state);
808        });
809    }
810
811    // TODO(jaewan): Rename to seek completed
812    void pushPositionChanges(final long eventTimeMs, final long positionMs) {
813        synchronized (mLock) {
814            mPositionEventTimeMs = eventTimeMs;
815            mPositionMs = positionMs;
816        }
817        mCallbackExecutor.execute(() -> {
818            if (!mInstance.isConnected()) {
819                return;
820            }
821            mCallback.onSeekCompleted(mInstance, positionMs);
822        });
823    }
824
825    void pushPlaybackSpeedChanges(final float speed) {
826        synchronized (mLock) {
827            mPlaybackSpeed = speed;
828        }
829        mCallbackExecutor.execute(() -> {
830            if (!mInstance.isConnected()) {
831                return;
832            }
833            mCallback.onPlaybackSpeedChanged(mInstance, speed);
834        });
835    }
836
837    void pushBufferedPositionChanges(final long bufferedPositionMs) {
838        synchronized (mLock) {
839            mBufferedPositionMs = bufferedPositionMs;
840        }
841        mCallbackExecutor.execute(() -> {
842            if (!mInstance.isConnected()) {
843                return;
844            }
845            // TODO(jaewan): Fix this -- it's now buffered state
846            //mCallback.onBufferedPositionChanged(mInstance, bufferedPositionMs);
847        });
848    }
849
850    void pushPlaybackInfoChanges(final PlaybackInfo info) {
851        synchronized (mLock) {
852            mPlaybackInfo = info;
853        }
854        mCallbackExecutor.execute(() -> {
855            if (!mInstance.isConnected()) {
856                return;
857            }
858            mCallback.onPlaybackInfoChanged(mInstance, info);
859        });
860    }
861
862    void pushPlaylistChanges(final List<MediaItem2> playlist, final MediaMetadata2 metadata) {
863        synchronized (mLock) {
864            mPlaylist = playlist;
865            mPlaylistMetadata = metadata;
866        }
867        mCallbackExecutor.execute(() -> {
868            if (!mInstance.isConnected()) {
869                return;
870            }
871            mCallback.onPlaylistChanged(mInstance, playlist, metadata);
872        });
873    }
874
875    void pushPlaylistMetadataChanges(MediaMetadata2 metadata) {
876        synchronized (mLock) {
877            mPlaylistMetadata = metadata;
878        }
879        mCallbackExecutor.execute(() -> {
880            if (!mInstance.isConnected()) {
881                return;
882            }
883            mCallback.onPlaylistMetadataChanged(mInstance, metadata);
884        });
885    }
886
887    void pushShuffleModeChanges(int shuffleMode) {
888        synchronized (mLock) {
889            mShuffleMode = shuffleMode;
890        }
891        mCallbackExecutor.execute(() -> {
892            if (!mInstance.isConnected()) {
893                return;
894            }
895            mCallback.onShuffleModeChanged(mInstance, shuffleMode);
896        });
897    }
898
899    void pushRepeatModeChanges(int repeatMode) {
900        synchronized (mLock) {
901            mRepeatMode = repeatMode;
902        }
903        mCallbackExecutor.execute(() -> {
904            if (!mInstance.isConnected()) {
905                return;
906            }
907            mCallback.onRepeatModeChanged(mInstance, repeatMode);
908        });
909    }
910
911    void pushError(int errorCode, Bundle extras) {
912        mCallbackExecutor.execute(() -> {
913            if (!mInstance.isConnected()) {
914                return;
915            }
916            mCallback.onError(mInstance, errorCode, extras);
917        });
918    }
919
920    // Should be used without a lock to prevent potential deadlock.
921    void onConnectedNotLocked(IMediaSession2 sessionBinder,
922            final SessionCommandGroup2 allowedCommands,
923            final int playerState,
924            final long positionEventTimeMs,
925            final long positionMs,
926            final float playbackSpeed,
927            final long bufferedPositionMs,
928            final PlaybackInfo info,
929            final int repeatMode,
930            final int shuffleMode,
931            final List<MediaItem2> playlist,
932            final PendingIntent sessionActivity) {
933        if (DEBUG) {
934            Log.d(TAG, "onConnectedNotLocked sessionBinder=" + sessionBinder
935                    + ", allowedCommands=" + allowedCommands);
936        }
937        boolean close = false;
938        try {
939            if (sessionBinder == null || allowedCommands == null) {
940                // Connection rejected.
941                close = true;
942                return;
943            }
944            synchronized (mLock) {
945                if (mIsReleased) {
946                    return;
947                }
948                if (mSessionBinder != null) {
949                    Log.e(TAG, "Cannot be notified about the connection result many times."
950                            + " Probably a bug or malicious app.");
951                    close = true;
952                    return;
953                }
954                mAllowedCommands = allowedCommands;
955                mPlayerState = playerState;
956                mPositionEventTimeMs = positionEventTimeMs;
957                mPositionMs = positionMs;
958                mPlaybackSpeed = playbackSpeed;
959                mBufferedPositionMs = bufferedPositionMs;
960                mPlaybackInfo = info;
961                mRepeatMode = repeatMode;
962                mShuffleMode = shuffleMode;
963                mPlaylist = playlist;
964                mSessionActivity = sessionActivity;
965                mSessionBinder = sessionBinder;
966                try {
967                    // Implementation for the local binder is no-op,
968                    // so can be used without worrying about deadlock.
969                    mSessionBinder.asBinder().linkToDeath(mDeathRecipient, 0);
970                } catch (RemoteException e) {
971                    if (DEBUG) {
972                        Log.d(TAG, "Session died too early.", e);
973                    }
974                    close = true;
975                    return;
976                }
977            }
978            // TODO(jaewan): Keep commands to prevents illegal API calls.
979            mCallbackExecutor.execute(() -> {
980                // Note: We may trigger ControllerCallbacks with the initial values
981                // But it's hard to define the order of the controller callbacks
982                // Only notify about the
983                mCallback.onConnected(mInstance, allowedCommands);
984            });
985        } finally {
986            if (close) {
987                // Trick to call release() without holding the lock, to prevent potential deadlock
988                // with the developer's custom lock within the ControllerCallback.onDisconnected().
989                mInstance.close();
990            }
991        }
992    }
993
994    void onCustomCommand(final SessionCommand2 command, final Bundle args,
995            final ResultReceiver receiver) {
996        if (DEBUG) {
997            Log.d(TAG, "onCustomCommand cmd=" + command);
998        }
999        mCallbackExecutor.execute(() -> {
1000            // TODO(jaewan): Double check if the controller exists.
1001            mCallback.onCustomCommand(mInstance, command, args, receiver);
1002        });
1003    }
1004
1005    void onAllowedCommandsChanged(final SessionCommandGroup2 commands) {
1006        mCallbackExecutor.execute(() -> {
1007            mCallback.onAllowedCommandsChanged(mInstance, commands);
1008        });
1009    }
1010
1011    void onCustomLayoutChanged(final List<CommandButton> layout) {
1012        mCallbackExecutor.execute(() -> {
1013            mCallback.onCustomLayoutChanged(mInstance, layout);
1014        });
1015    }
1016
1017    // This will be called on the main thread.
1018    private class SessionServiceConnection implements ServiceConnection {
1019        @Override
1020        public void onServiceConnected(ComponentName name, IBinder service) {
1021            // Note that it's always main-thread.
1022            if (DEBUG) {
1023                Log.d(TAG, "onServiceConnected " + name + " " + this);
1024            }
1025            // Sanity check
1026            if (!mToken.getPackageName().equals(name.getPackageName())) {
1027                Log.wtf(TAG, name + " was connected, but expected pkg="
1028                        + mToken.getPackageName() + " with id=" + mToken.getId());
1029                return;
1030            }
1031            final IMediaSession2 sessionBinder = IMediaSession2.Stub.asInterface(service);
1032            connectToSession(sessionBinder);
1033        }
1034
1035        @Override
1036        public void onServiceDisconnected(ComponentName name) {
1037            // Temporal lose of the binding because of the service crash. System will automatically
1038            // rebind, so just no-op.
1039            // TODO(jaewan): Really? Either disconnect cleanly or
1040            if (DEBUG) {
1041                Log.w(TAG, "Session service " + name + " is disconnected.");
1042            }
1043        }
1044
1045        @Override
1046        public void onBindingDied(ComponentName name) {
1047            // Permanent lose of the binding because of the service package update or removed.
1048            // This SessionServiceRecord will be removed accordingly, but forget session binder here
1049            // for sure.
1050            mInstance.close();
1051        }
1052    }
1053
1054    public static final class PlaybackInfoImpl implements PlaybackInfoProvider {
1055
1056        private static final String KEY_PLAYBACK_TYPE =
1057                "android.media.playbackinfo_impl.playback_type";
1058        private static final String KEY_CONTROL_TYPE =
1059                "android.media.playbackinfo_impl.control_type";
1060        private static final String KEY_MAX_VOLUME =
1061                "android.media.playbackinfo_impl.max_volume";
1062        private static final String KEY_CURRENT_VOLUME =
1063                "android.media.playbackinfo_impl.current_volume";
1064        private static final String KEY_AUDIO_ATTRIBUTES =
1065                "android.media.playbackinfo_impl.audio_attrs";
1066
1067        private final PlaybackInfo mInstance;
1068
1069        private final int mPlaybackType;
1070        private final int mControlType;
1071        private final int mMaxVolume;
1072        private final int mCurrentVolume;
1073        private final AudioAttributes mAudioAttrs;
1074
1075        private PlaybackInfoImpl(int playbackType, AudioAttributes attrs, int controlType,
1076                int max, int current) {
1077            mPlaybackType = playbackType;
1078            mAudioAttrs = attrs;
1079            mControlType = controlType;
1080            mMaxVolume = max;
1081            mCurrentVolume = current;
1082            mInstance = new PlaybackInfo(this);
1083        }
1084
1085        @Override
1086        public int getPlaybackType_impl() {
1087            return mPlaybackType;
1088        }
1089
1090        @Override
1091        public AudioAttributes getAudioAttributes_impl() {
1092            return mAudioAttrs;
1093        }
1094
1095        @Override
1096        public int getControlType_impl() {
1097            return mControlType;
1098        }
1099
1100        @Override
1101        public int getMaxVolume_impl() {
1102            return mMaxVolume;
1103        }
1104
1105        @Override
1106        public int getCurrentVolume_impl() {
1107            return mCurrentVolume;
1108        }
1109
1110        PlaybackInfo getInstance() {
1111            return mInstance;
1112        }
1113
1114        Bundle toBundle() {
1115            Bundle bundle = new Bundle();
1116            bundle.putInt(KEY_PLAYBACK_TYPE, mPlaybackType);
1117            bundle.putInt(KEY_CONTROL_TYPE, mControlType);
1118            bundle.putInt(KEY_MAX_VOLUME, mMaxVolume);
1119            bundle.putInt(KEY_CURRENT_VOLUME, mCurrentVolume);
1120            bundle.putParcelable(KEY_AUDIO_ATTRIBUTES, mAudioAttrs);
1121            return bundle;
1122        }
1123
1124        static PlaybackInfo createPlaybackInfo(int playbackType, AudioAttributes attrs,
1125                int controlType, int max, int current) {
1126            return new PlaybackInfoImpl(playbackType, attrs, controlType, max, current)
1127                    .getInstance();
1128        }
1129
1130        static PlaybackInfo fromBundle(Bundle bundle) {
1131            if (bundle == null) {
1132                return null;
1133            }
1134            final int volumeType = bundle.getInt(KEY_PLAYBACK_TYPE);
1135            final int volumeControl = bundle.getInt(KEY_CONTROL_TYPE);
1136            final int maxVolume = bundle.getInt(KEY_MAX_VOLUME);
1137            final int currentVolume = bundle.getInt(KEY_CURRENT_VOLUME);
1138            final AudioAttributes attrs = bundle.getParcelable(KEY_AUDIO_ATTRIBUTES);
1139
1140            return createPlaybackInfo(volumeType, attrs, volumeControl, maxVolume, currentVolume);
1141        }
1142    }
1143}
1144