• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.media;
18 
19 import static android.media.SessionCommand2.COMMAND_CODE_SET_VOLUME;
20 import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM;
21 import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM;
22 import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM;
23 import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST;
24 import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST_METADATA;
25 import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE;
26 import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE;
27 import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID;
28 import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH;
29 import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI;
30 import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID;
31 import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH;
32 import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI;
33 
34 import android.app.PendingIntent;
35 import android.content.ComponentName;
36 import android.content.Context;
37 import android.content.Intent;
38 import android.content.ServiceConnection;
39 import android.media.AudioAttributes;
40 import android.media.MediaController2;
41 import android.media.MediaController2.ControllerCallback;
42 import android.media.MediaController2.PlaybackInfo;
43 import android.media.MediaItem2;
44 import android.media.MediaMetadata2;
45 import android.media.MediaPlaylistAgent.RepeatMode;
46 import android.media.MediaPlaylistAgent.ShuffleMode;
47 import android.media.SessionCommand2;
48 import android.media.MediaSession2.CommandButton;
49 import android.media.SessionCommandGroup2;
50 import android.media.MediaSessionService2;
51 import android.media.Rating2;
52 import android.media.SessionToken2;
53 import android.media.update.MediaController2Provider;
54 import android.net.Uri;
55 import android.os.Bundle;
56 import android.os.IBinder;
57 import android.os.Process;
58 import android.os.RemoteException;
59 import android.os.ResultReceiver;
60 import android.os.UserHandle;
61 import android.support.annotation.GuardedBy;
62 import android.text.TextUtils;
63 import android.util.Log;
64 
65 import java.util.ArrayList;
66 import java.util.List;
67 import java.util.concurrent.Executor;
68 
69 public 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.
MediaController2Impl(Context context, MediaController2 instance, SessionToken2 token, Executor executor, ControllerCallback callback)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
initialize()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 
connectToService()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 
connectToSession(IMediaSession2 sessionBinder)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
close_impl()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 
getSessionBinder()259     IMediaSession2 getSessionBinder() {
260         return mSessionBinder;
261     }
262 
getControllerStub()263     MediaController2Stub getControllerStub() {
264         return mControllerStub;
265     }
266 
getCallbackExecutor()267     Executor getCallbackExecutor() {
268         return mCallbackExecutor;
269     }
270 
getContext()271     Context getContext() {
272         return mContext;
273     }
274 
getInstance()275     MediaController2 getInstance() {
276         return mInstance;
277     }
278 
279     // Returns session binder if the controller can send the command.
getSessionBinderIfAble(int commandCode)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.
getSessionBinderIfAble(SessionCommand2 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
getSessionToken_impl()316     public SessionToken2 getSessionToken_impl() {
317         return mToken;
318     }
319 
320     @Override
isConnected_impl()321     public boolean isConnected_impl() {
322         final IMediaSession2 binder = mSessionBinder;
323         return binder != null;
324     }
325 
326     @Override
play_impl()327     public void play_impl() {
328         sendTransportControlCommand(SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY);
329     }
330 
331     @Override
pause_impl()332     public void pause_impl() {
333         sendTransportControlCommand(SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE);
334     }
335 
336     @Override
stop_impl()337     public void stop_impl() {
338         sendTransportControlCommand(SessionCommand2.COMMAND_CODE_PLAYBACK_STOP);
339     }
340 
341     @Override
skipToPlaylistItem_impl(MediaItem2 item)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
skipToPreviousItem_impl()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
skipToNextItem_impl()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 
sendTransportControlCommand(int commandCode)386     private void sendTransportControlCommand(int commandCode) {
387         sendTransportControlCommand(commandCode, null);
388     }
389 
sendTransportControlCommand(int commandCode, Bundle args)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
getSessionActivity_impl()404     public PendingIntent getSessionActivity_impl() {
405         return mSessionActivity;
406     }
407 
408     @Override
setVolumeTo_impl(int value, int flags)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
adjustVolume_impl(int direction, int flags)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
prepareFromUri_impl(Uri uri, Bundle extras)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
prepareFromSearch_impl(String query, Bundle extras)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
prepareFromMediaId_impl(String mediaId, Bundle extras)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
playFromUri_impl(Uri uri, Bundle extras)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
playFromSearch_impl(String query, Bundle extras)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
playFromMediaId_impl(String mediaId, Bundle extras)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
setRating_impl(String mediaId, Rating2 rating)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
sendCustomCommand_impl(SessionCommand2 command, Bundle args, ResultReceiver cb)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
getPlaylist_impl()582     public List<MediaItem2> getPlaylist_impl() {
583         synchronized (mLock) {
584             return mPlaylist;
585         }
586     }
587 
588     @Override
setPlaylist_impl(List<MediaItem2> list, MediaMetadata2 metadata)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
getPlaylistMetadata_impl()611     public MediaMetadata2 getPlaylistMetadata_impl() {
612         synchronized (mLock) {
613             return mPlaylistMetadata;
614         }
615     }
616 
617     @Override
updatePlaylistMetadata_impl(MediaMetadata2 metadata)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
prepare_impl()634     public void prepare_impl() {
635         sendTransportControlCommand(SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE);
636     }
637 
638     @Override
fastForward_impl()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
rewind_impl()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
seekTo_impl(long pos)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
addPlaylistItem_impl(int index, MediaItem2 item)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
removePlaylistItem_impl(MediaItem2 item)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
replacePlaylistItem_impl(int index, MediaItem2 item)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
getShuffleMode_impl()718     public int getShuffleMode_impl() {
719         return mShuffleMode;
720     }
721 
722     @Override
setShuffleMode_impl(int shuffleMode)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
getRepeatMode_impl()738     public int getRepeatMode_impl() {
739         return mRepeatMode;
740     }
741 
742     @Override
setRepeatMode_impl(int repeatMode)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
getPlaybackInfo_impl()757     public PlaybackInfo getPlaybackInfo_impl() {
758         synchronized (mLock) {
759             return mPlaybackInfo;
760         }
761     }
762 
763     @Override
getPlayerState_impl()764     public int getPlayerState_impl() {
765         synchronized (mLock) {
766             return mPlayerState;
767         }
768     }
769 
770     @Override
getCurrentPosition_impl()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
getPlaybackSpeed_impl()780     public float getPlaybackSpeed_impl() {
781         synchronized (mLock) {
782             return mPlaybackSpeed;
783         }
784     }
785 
786     @Override
getBufferedPosition_impl()787     public long getBufferedPosition_impl() {
788         synchronized (mLock) {
789             return mBufferedPositionMs;
790         }
791     }
792 
793     @Override
getCurrentMediaItem_impl()794     public MediaItem2 getCurrentMediaItem_impl() {
795         // TODO(jaewan): Implement
796         return null;
797     }
798 
pushPlayerStateChanges(final int state)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
pushPositionChanges(final long eventTimeMs, final long positionMs)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 
pushPlaybackSpeedChanges(final float speed)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 
pushBufferedPositionChanges(final long bufferedPositionMs)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 
pushPlaybackInfoChanges(final PlaybackInfo info)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 
pushPlaylistChanges(final List<MediaItem2> playlist, final MediaMetadata2 metadata)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 
pushPlaylistMetadataChanges(MediaMetadata2 metadata)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 
pushShuffleModeChanges(int shuffleMode)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 
pushRepeatModeChanges(int repeatMode)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 
pushError(int errorCode, Bundle extras)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.
onConnectedNotLocked(IMediaSession2 sessionBinder, final SessionCommandGroup2 allowedCommands, final int playerState, final long positionEventTimeMs, final long positionMs, final float playbackSpeed, final long bufferedPositionMs, final PlaybackInfo info, final int repeatMode, final int shuffleMode, final List<MediaItem2> playlist, final PendingIntent sessionActivity)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 
onCustomCommand(final SessionCommand2 command, final Bundle args, final ResultReceiver receiver)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 
onAllowedCommandsChanged(final SessionCommandGroup2 commands)1005     void onAllowedCommandsChanged(final SessionCommandGroup2 commands) {
1006         mCallbackExecutor.execute(() -> {
1007             mCallback.onAllowedCommandsChanged(mInstance, commands);
1008         });
1009     }
1010 
onCustomLayoutChanged(final List<CommandButton> layout)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
onServiceConnected(ComponentName name, IBinder service)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
onServiceDisconnected(ComponentName name)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
onBindingDied(ComponentName name)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 
PlaybackInfoImpl(int playbackType, AudioAttributes attrs, int controlType, int max, int current)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
getPlaybackType_impl()1086         public int getPlaybackType_impl() {
1087             return mPlaybackType;
1088         }
1089 
1090         @Override
getAudioAttributes_impl()1091         public AudioAttributes getAudioAttributes_impl() {
1092             return mAudioAttrs;
1093         }
1094 
1095         @Override
getControlType_impl()1096         public int getControlType_impl() {
1097             return mControlType;
1098         }
1099 
1100         @Override
getMaxVolume_impl()1101         public int getMaxVolume_impl() {
1102             return mMaxVolume;
1103         }
1104 
1105         @Override
getCurrentVolume_impl()1106         public int getCurrentVolume_impl() {
1107             return mCurrentVolume;
1108         }
1109 
getInstance()1110         PlaybackInfo getInstance() {
1111             return mInstance;
1112         }
1113 
toBundle()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 
createPlaybackInfo(int playbackType, AudioAttributes attrs, int controlType, int max, int current)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 
fromBundle(Bundle bundle)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