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 android.app.PendingIntent;
20 import android.content.Context;
21 import android.media.MediaController2;
22 import android.media.MediaItem2;
23 import android.media.MediaLibraryService2.LibraryRoot;
24 import android.media.MediaMetadata2;
25 import android.media.SessionCommand2;
26 import android.media.MediaSession2.CommandButton;
27 import android.media.SessionCommandGroup2;
28 import android.media.MediaSession2.ControllerInfo;
29 import android.media.Rating2;
30 import android.media.VolumeProvider2;
31 import android.net.Uri;
32 import android.os.Binder;
33 import android.os.Bundle;
34 import android.os.DeadObjectException;
35 import android.os.IBinder;
36 import android.os.RemoteException;
37 import android.os.ResultReceiver;
38 import android.support.annotation.GuardedBy;
39 import android.support.annotation.NonNull;
40 import android.text.TextUtils;
41 import android.util.ArrayMap;
42 import android.util.Log;
43 import android.util.SparseArray;
44 
45 import com.android.media.MediaLibraryService2Impl.MediaLibrarySessionImpl;
46 import com.android.media.MediaSession2Impl.CommandButtonImpl;
47 import com.android.media.MediaSession2Impl.CommandGroupImpl;
48 import com.android.media.MediaSession2Impl.ControllerInfoImpl;
49 
50 import java.lang.ref.WeakReference;
51 import java.util.ArrayList;
52 import java.util.HashSet;
53 import java.util.List;
54 import java.util.Set;
55 
56 public class MediaSession2Stub extends IMediaSession2.Stub {
57 
58     static final String ARGUMENT_KEY_POSITION = "android.media.media_session2.key_position";
59     static final String ARGUMENT_KEY_ITEM_INDEX = "android.media.media_session2.key_item_index";
60     static final String ARGUMENT_KEY_PLAYLIST_PARAMS =
61             "android.media.media_session2.key_playlist_params";
62 
63     private static final String TAG = "MediaSession2Stub";
64     private static final boolean DEBUG = true; // TODO(jaewan): Rename.
65 
66     private static final SparseArray<SessionCommand2> sCommandsForOnCommandRequest =
67             new SparseArray<>();
68 
69     private final Object mLock = new Object();
70     private final WeakReference<MediaSession2Impl> mSession;
71 
72     @GuardedBy("mLock")
73     private final ArrayMap<IBinder, ControllerInfo> mControllers = new ArrayMap<>();
74     @GuardedBy("mLock")
75     private final Set<IBinder> mConnectingControllers = new HashSet<>();
76     @GuardedBy("mLock")
77     private final ArrayMap<ControllerInfo, SessionCommandGroup2> mAllowedCommandGroupMap =
78             new ArrayMap<>();
79     @GuardedBy("mLock")
80     private final ArrayMap<ControllerInfo, Set<String>> mSubscriptions = new ArrayMap<>();
81 
MediaSession2Stub(MediaSession2Impl session)82     public MediaSession2Stub(MediaSession2Impl session) {
83         mSession = new WeakReference<>(session);
84 
85         synchronized (sCommandsForOnCommandRequest) {
86             if (sCommandsForOnCommandRequest.size() == 0) {
87                 CommandGroupImpl group = new CommandGroupImpl();
88                 group.addAllPlaybackCommands();
89                 group.addAllPlaylistCommands();
90                 Set<SessionCommand2> commands = group.getCommands();
91                 for (SessionCommand2 command : commands) {
92                     sCommandsForOnCommandRequest.append(command.getCommandCode(), command);
93                 }
94             }
95         }
96     }
97 
destroyNotLocked()98     public void destroyNotLocked() {
99         final List<ControllerInfo> list;
100         synchronized (mLock) {
101             mSession.clear();
102             list = getControllers();
103             mControllers.clear();
104         }
105         for (int i = 0; i < list.size(); i++) {
106             IMediaController2 controllerBinder =
107                     ((ControllerInfoImpl) list.get(i).getProvider()).getControllerBinder();
108             try {
109                 // Should be used without a lock hold to prevent potential deadlock.
110                 controllerBinder.onDisconnected();
111             } catch (RemoteException e) {
112                 // Controller is gone. Should be fine because we're destroying.
113             }
114         }
115     }
116 
getSession()117     private MediaSession2Impl getSession() {
118         final MediaSession2Impl session = mSession.get();
119         if (session == null && DEBUG) {
120             Log.d(TAG, "Session is closed", new IllegalStateException());
121         }
122         return session;
123     }
124 
getLibrarySession()125     private MediaLibrarySessionImpl getLibrarySession() throws IllegalStateException {
126         final MediaSession2Impl session = getSession();
127         if (!(session instanceof MediaLibrarySessionImpl)) {
128             throw new RuntimeException("Session isn't a library session");
129         }
130         return (MediaLibrarySessionImpl) session;
131     }
132 
133     // Get controller if the command from caller to session is able to be handled.
getControllerIfAble(IMediaController2 caller)134     private ControllerInfo getControllerIfAble(IMediaController2 caller) {
135         synchronized (mLock) {
136             final ControllerInfo controllerInfo = mControllers.get(caller.asBinder());
137             if (controllerInfo == null && DEBUG) {
138                 Log.d(TAG, "Controller is disconnected", new IllegalStateException());
139             }
140             return controllerInfo;
141         }
142     }
143 
144     // Get controller if the command from caller to session is able to be handled.
getControllerIfAble(IMediaController2 caller, int commandCode)145     private ControllerInfo getControllerIfAble(IMediaController2 caller, int commandCode) {
146         synchronized (mLock) {
147             final ControllerInfo controllerInfo = getControllerIfAble(caller);
148             if (controllerInfo == null) {
149                 return null;
150             }
151             SessionCommandGroup2 allowedCommands = mAllowedCommandGroupMap.get(controllerInfo);
152             if (allowedCommands == null) {
153                 Log.w(TAG, "Controller with null allowed commands. Ignoring",
154                         new IllegalStateException());
155                 return null;
156             }
157             if (!allowedCommands.hasCommand(commandCode)) {
158                 if (DEBUG) {
159                     Log.d(TAG, "Controller isn't allowed for command " + commandCode);
160                 }
161                 return null;
162             }
163             return controllerInfo;
164         }
165     }
166 
167     // Get controller if the command from caller to session is able to be handled.
getControllerIfAble(IMediaController2 caller, SessionCommand2 command)168     private ControllerInfo getControllerIfAble(IMediaController2 caller, SessionCommand2 command) {
169         synchronized (mLock) {
170             final ControllerInfo controllerInfo = getControllerIfAble(caller);
171             if (controllerInfo == null) {
172                 return null;
173             }
174             SessionCommandGroup2 allowedCommands = mAllowedCommandGroupMap.get(controllerInfo);
175             if (allowedCommands == null) {
176                 Log.w(TAG, "Controller with null allowed commands. Ignoring",
177                         new IllegalStateException());
178                 return null;
179             }
180             if (!allowedCommands.hasCommand(command)) {
181                 if (DEBUG) {
182                     Log.d(TAG, "Controller isn't allowed for command " + command);
183                 }
184                 return null;
185             }
186             return controllerInfo;
187         }
188     }
189 
190     // Return binder if the session is able to send a command to the controller.
getControllerBinderIfAble(ControllerInfo controller)191     private IMediaController2 getControllerBinderIfAble(ControllerInfo controller) {
192         if (getSession() == null) {
193             // getSession() already logged if session is closed.
194             return null;
195         }
196         final ControllerInfoImpl impl = ControllerInfoImpl.from(controller);
197         synchronized (mLock) {
198             if (mControllers.get(impl.getId()) != null
199                     || mConnectingControllers.contains(impl.getId())) {
200                 return impl.getControllerBinder();
201             }
202             if (DEBUG) {
203                 Log.d(TAG, controller + " isn't connected nor connecting",
204                         new IllegalArgumentException());
205             }
206             return null;
207         }
208     }
209 
210     // Return binder if the session is able to send a command to the controller.
getControllerBinderIfAble(ControllerInfo controller, int commandCode)211     private IMediaController2 getControllerBinderIfAble(ControllerInfo controller,
212             int commandCode) {
213         synchronized (mLock) {
214             SessionCommandGroup2 allowedCommands = mAllowedCommandGroupMap.get(controller);
215             if (allowedCommands == null) {
216                 Log.w(TAG, "Controller with null allowed commands. Ignoring");
217                 return null;
218             }
219             if (!allowedCommands.hasCommand(commandCode)) {
220                 if (DEBUG) {
221                     Log.d(TAG, "Controller isn't allowed for command " + commandCode);
222                 }
223                 return null;
224             }
225             return getControllerBinderIfAble(controller);
226         }
227     }
228 
onCommand(@onNull IMediaController2 caller, int commandCode, @NonNull SessionRunnable runnable)229     private void onCommand(@NonNull IMediaController2 caller, int commandCode,
230             @NonNull SessionRunnable runnable) {
231         final MediaSession2Impl session = getSession();
232         final ControllerInfo controller = getControllerIfAble(caller, commandCode);
233         if (session == null || controller == null) {
234             return;
235         }
236         session.getCallbackExecutor().execute(() -> {
237             if (getControllerIfAble(caller, commandCode) == null) {
238                 return;
239             }
240             SessionCommand2 command = sCommandsForOnCommandRequest.get(commandCode);
241             if (command != null) {
242                 boolean accepted = session.getCallback().onCommandRequest(session.getInstance(),
243                         controller, command);
244                 if (!accepted) {
245                     // Don't run rejected command.
246                     if (DEBUG) {
247                         Log.d(TAG, "Command (code=" + commandCode + ") from "
248                                 + controller + " was rejected by " + session);
249                     }
250                     return;
251                 }
252             }
253             runnable.run(session, controller);
254         });
255     }
256 
onBrowserCommand(@onNull IMediaController2 caller, @NonNull LibrarySessionRunnable runnable)257     private void onBrowserCommand(@NonNull IMediaController2 caller,
258             @NonNull LibrarySessionRunnable runnable) {
259         final MediaLibrarySessionImpl session = getLibrarySession();
260         // TODO(jaewan): Consider command code
261         final ControllerInfo controller = getControllerIfAble(caller);
262         if (session == null || controller == null) {
263             return;
264         }
265         session.getCallbackExecutor().execute(() -> {
266             // TODO(jaewan): Consider command code
267             if (getControllerIfAble(caller) == null) {
268                 return;
269             }
270             runnable.run(session, controller);
271         });
272     }
273 
274 
notifyAll(int commandCode, @NonNull NotifyRunnable runnable)275     private void notifyAll(int commandCode, @NonNull NotifyRunnable runnable) {
276         List<ControllerInfo> controllers = getControllers();
277         for (int i = 0; i < controllers.size(); i++) {
278             notifyInternal(controllers.get(i),
279                     getControllerBinderIfAble(controllers.get(i), commandCode), runnable);
280         }
281     }
282 
notifyAll(@onNull NotifyRunnable runnable)283     private void notifyAll(@NonNull NotifyRunnable runnable) {
284         List<ControllerInfo> controllers = getControllers();
285         for (int i = 0; i < controllers.size(); i++) {
286             notifyInternal(controllers.get(i),
287                     getControllerBinderIfAble(controllers.get(i)), runnable);
288         }
289     }
290 
notify(@onNull ControllerInfo controller, @NonNull NotifyRunnable runnable)291     private void notify(@NonNull ControllerInfo controller, @NonNull NotifyRunnable runnable) {
292         notifyInternal(controller, getControllerBinderIfAble(controller), runnable);
293     }
294 
notify(@onNull ControllerInfo controller, int commandCode, @NonNull NotifyRunnable runnable)295     private void notify(@NonNull ControllerInfo controller, int commandCode,
296             @NonNull NotifyRunnable runnable) {
297         notifyInternal(controller, getControllerBinderIfAble(controller, commandCode), runnable);
298     }
299 
300     // Do not call this API directly. Use notify() instead.
notifyInternal(@onNull ControllerInfo controller, @NonNull IMediaController2 iController, @NonNull NotifyRunnable runnable)301     private void notifyInternal(@NonNull ControllerInfo controller,
302             @NonNull IMediaController2 iController, @NonNull NotifyRunnable runnable) {
303         if (controller == null || iController == null) {
304             return;
305         }
306         try {
307             runnable.run(controller, iController);
308         } catch (DeadObjectException e) {
309             if (DEBUG) {
310                 Log.d(TAG, controller.toString() + " is gone", e);
311             }
312             onControllerClosed(iController);
313         } catch (RemoteException e) {
314             // Currently it's TransactionTooLargeException or DeadSystemException.
315             // We'd better to leave log for those cases because
316             //   - TransactionTooLargeException means that we may need to fix our code.
317             //     (e.g. add pagination or special way to deliver Bitmap)
318             //   - DeadSystemException means that errors around it can be ignored.
319             Log.w(TAG, "Exception in " + controller.toString(), e);
320         }
321     }
322 
onControllerClosed(IMediaController2 iController)323     private void onControllerClosed(IMediaController2 iController) {
324         ControllerInfo controller;
325         synchronized (mLock) {
326             controller = mControllers.remove(iController.asBinder());
327             if (DEBUG) {
328                 Log.d(TAG, "releasing " + controller);
329             }
330             mSubscriptions.remove(controller);
331         }
332         final MediaSession2Impl session = getSession();
333         if (session == null || controller == null) {
334             return;
335         }
336         session.getCallbackExecutor().execute(() -> {
337             session.getCallback().onDisconnected(session.getInstance(), controller);
338         });
339     }
340 
341     //////////////////////////////////////////////////////////////////////////////////////////////
342     // AIDL methods for session overrides
343     //////////////////////////////////////////////////////////////////////////////////////////////
344     @Override
connect(final IMediaController2 caller, final String callingPackage)345     public void connect(final IMediaController2 caller, final String callingPackage)
346             throws RuntimeException {
347         final MediaSession2Impl session = getSession();
348         if (session == null) {
349             return;
350         }
351         final Context context = session.getContext();
352         final ControllerInfo controllerInfo = new ControllerInfo(context,
353                 Binder.getCallingUid(), Binder.getCallingPid(), callingPackage, caller);
354         session.getCallbackExecutor().execute(() -> {
355             if (getSession() == null) {
356                 return;
357             }
358             synchronized (mLock) {
359                 // Keep connecting controllers.
360                 // This helps sessions to call APIs in the onConnect() (e.g. setCustomLayout())
361                 // instead of pending them.
362                 mConnectingControllers.add(ControllerInfoImpl.from(controllerInfo).getId());
363             }
364             SessionCommandGroup2 allowedCommands = session.getCallback().onConnect(
365                     session.getInstance(), controllerInfo);
366             // Don't reject connection for the request from trusted app.
367             // Otherwise server will fail to retrieve session's information to dispatch
368             // media keys to.
369             boolean accept = allowedCommands != null || controllerInfo.isTrusted();
370             if (accept) {
371                 ControllerInfoImpl controllerImpl = ControllerInfoImpl.from(controllerInfo);
372                 if (DEBUG) {
373                     Log.d(TAG, "Accepting connection, controllerInfo=" + controllerInfo
374                             + " allowedCommands=" + allowedCommands);
375                 }
376                 if (allowedCommands == null) {
377                     // For trusted apps, send non-null allowed commands to keep connection.
378                     allowedCommands = new SessionCommandGroup2();
379                 }
380                 synchronized (mLock) {
381                     mConnectingControllers.remove(controllerImpl.getId());
382                     mControllers.put(controllerImpl.getId(),  controllerInfo);
383                     mAllowedCommandGroupMap.put(controllerInfo, allowedCommands);
384                 }
385                 // If connection is accepted, notify the current state to the controller.
386                 // It's needed because we cannot call synchronous calls between session/controller.
387                 // Note: We're doing this after the onConnectionChanged(), but there's no guarantee
388                 //       that events here are notified after the onConnected() because
389                 //       IMediaController2 is oneway (i.e. async call) and Stub will
390                 //       use thread poll for incoming calls.
391                 final int playerState = session.getInstance().getPlayerState();
392                 final long positionEventTimeMs = System.currentTimeMillis();
393                 final long positionMs = session.getInstance().getCurrentPosition();
394                 final float playbackSpeed = session.getInstance().getPlaybackSpeed();
395                 final long bufferedPositionMs = session.getInstance().getBufferedPosition();
396                 final Bundle playbackInfoBundle = ((MediaController2Impl.PlaybackInfoImpl)
397                         session.getPlaybackInfo().getProvider()).toBundle();
398                 final int repeatMode = session.getInstance().getRepeatMode();
399                 final int shuffleMode = session.getInstance().getShuffleMode();
400                 final PendingIntent sessionActivity = session.getSessionActivity();
401                 final List<MediaItem2> playlist =
402                         allowedCommands.hasCommand(SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST)
403                                 ? session.getInstance().getPlaylist() : null;
404                 final List<Bundle> playlistBundle;
405                 if (playlist != null) {
406                     playlistBundle = new ArrayList<>();
407                     // TODO(jaewan): Find a way to avoid concurrent modification exception.
408                     for (int i = 0; i < playlist.size(); i++) {
409                         final MediaItem2 item = playlist.get(i);
410                         if (item != null) {
411                             final Bundle itemBundle = item.toBundle();
412                             if (itemBundle != null) {
413                                 playlistBundle.add(itemBundle);
414                             }
415                         }
416                     }
417                 } else {
418                     playlistBundle = null;
419                 }
420 
421                 // Double check if session is still there, because close() can be called in another
422                 // thread.
423                 if (getSession() == null) {
424                     return;
425                 }
426                 try {
427                     caller.onConnected(MediaSession2Stub.this, allowedCommands.toBundle(),
428                             playerState, positionEventTimeMs, positionMs, playbackSpeed,
429                             bufferedPositionMs, playbackInfoBundle, repeatMode, shuffleMode,
430                             playlistBundle, sessionActivity);
431                 } catch (RemoteException e) {
432                     // Controller may be died prematurely.
433                     // TODO(jaewan): Handle here.
434                 }
435             } else {
436                 synchronized (mLock) {
437                     mConnectingControllers.remove(ControllerInfoImpl.from(controllerInfo).getId());
438                 }
439                 if (DEBUG) {
440                     Log.d(TAG, "Rejecting connection, controllerInfo=" + controllerInfo);
441                 }
442                 try {
443                     caller.onDisconnected();
444                 } catch (RemoteException e) {
445                     // Controller may be died prematurely.
446                     // Not an issue because we'll ignore it anyway.
447                 }
448             }
449         });
450     }
451 
452     @Override
release(final IMediaController2 caller)453     public void release(final IMediaController2 caller) throws RemoteException {
454         onControllerClosed(caller);
455     }
456 
457     @Override
setVolumeTo(final IMediaController2 caller, final int value, final int flags)458     public void setVolumeTo(final IMediaController2 caller, final int value, final int flags)
459             throws RuntimeException {
460         onCommand(caller, SessionCommand2.COMMAND_CODE_SET_VOLUME,
461                 (session, controller) -> {
462                     VolumeProvider2 volumeProvider = session.getVolumeProvider();
463                     if (volumeProvider == null) {
464                         // TODO(jaewan): Set local stream volume
465                     } else {
466                         volumeProvider.onSetVolumeTo(value);
467                     }
468                 });
469     }
470 
471     @Override
adjustVolume(IMediaController2 caller, int direction, int flags)472     public void adjustVolume(IMediaController2 caller, int direction, int flags)
473             throws RuntimeException {
474         onCommand(caller, SessionCommand2.COMMAND_CODE_SET_VOLUME,
475                 (session, controller) -> {
476                     VolumeProvider2 volumeProvider = session.getVolumeProvider();
477                     if (volumeProvider == null) {
478                         // TODO(jaewan): Adjust local stream volume
479                     } else {
480                         volumeProvider.onAdjustVolume(direction);
481                     }
482                 });
483     }
484 
485     @Override
sendTransportControlCommand(IMediaController2 caller, int commandCode, Bundle args)486     public void sendTransportControlCommand(IMediaController2 caller,
487             int commandCode, Bundle args) throws RuntimeException {
488         onCommand(caller, commandCode, (session, controller) -> {
489             switch (commandCode) {
490                 case SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY:
491                     session.getInstance().play();
492                     break;
493                 case SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE:
494                     session.getInstance().pause();
495                     break;
496                 case SessionCommand2.COMMAND_CODE_PLAYBACK_STOP:
497                     session.getInstance().stop();
498                     break;
499                 case SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE:
500                     session.getInstance().prepare();
501                     break;
502                 case SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO:
503                     session.getInstance().seekTo(args.getLong(ARGUMENT_KEY_POSITION));
504                     break;
505                 default:
506                     // TODO(jaewan): Resend unknown (new) commands through the custom command.
507             }
508         });
509     }
510 
511     @Override
sendCustomCommand(final IMediaController2 caller, final Bundle commandBundle, final Bundle args, final ResultReceiver receiver)512     public void sendCustomCommand(final IMediaController2 caller, final Bundle commandBundle,
513             final Bundle args, final ResultReceiver receiver) {
514         final MediaSession2Impl session = getSession();
515         if (session == null) {
516             return;
517         }
518         final SessionCommand2 command = SessionCommand2.fromBundle(commandBundle);
519         if (command == null) {
520             Log.w(TAG, "sendCustomCommand(): Ignoring null command from "
521                     + getControllerIfAble(caller));
522             return;
523         }
524         final ControllerInfo controller = getControllerIfAble(caller, command);
525         if (controller == null) {
526             return;
527         }
528         session.getCallbackExecutor().execute(() -> {
529             if (getControllerIfAble(caller, command) == null) {
530                 return;
531             }
532             session.getCallback().onCustomCommand(session.getInstance(),
533                     controller, command, args, receiver);
534         });
535     }
536 
537     @Override
prepareFromUri(final IMediaController2 caller, final Uri uri, final Bundle extras)538     public void prepareFromUri(final IMediaController2 caller, final Uri uri,
539             final Bundle extras) {
540         onCommand(caller, SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI,
541                 (session, controller) -> {
542                     if (uri == null) {
543                         Log.w(TAG, "prepareFromUri(): Ignoring null uri from " + controller);
544                         return;
545                     }
546                     session.getCallback().onPrepareFromUri(session.getInstance(), controller, uri,
547                             extras);
548                 });
549     }
550 
551     @Override
prepareFromSearch(final IMediaController2 caller, final String query, final Bundle extras)552     public void prepareFromSearch(final IMediaController2 caller, final String query,
553             final Bundle extras) {
554         onCommand(caller, SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH,
555                 (session, controller) -> {
556                     if (TextUtils.isEmpty(query)) {
557                         Log.w(TAG, "prepareFromSearch(): Ignoring empty query from " + controller);
558                         return;
559                     }
560                     session.getCallback().onPrepareFromSearch(session.getInstance(),
561                             controller, query, extras);
562                 });
563     }
564 
565     @Override
prepareFromMediaId(final IMediaController2 caller, final String mediaId, final Bundle extras)566     public void prepareFromMediaId(final IMediaController2 caller, final String mediaId,
567             final Bundle extras) {
568         onCommand(caller, SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID,
569                 (session, controller) -> {
570             if (mediaId == null) {
571                 Log.w(TAG, "prepareFromMediaId(): Ignoring null mediaId from " + controller);
572                 return;
573             }
574             session.getCallback().onPrepareFromMediaId(session.getInstance(),
575                     controller, mediaId, extras);
576         });
577     }
578 
579     @Override
playFromUri(final IMediaController2 caller, final Uri uri, final Bundle extras)580     public void playFromUri(final IMediaController2 caller, final Uri uri,
581             final Bundle extras) {
582         onCommand(caller, SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI,
583                 (session, controller) -> {
584                     if (uri == null) {
585                         Log.w(TAG, "playFromUri(): Ignoring null uri from " + controller);
586                         return;
587                     }
588                     session.getCallback().onPlayFromUri(session.getInstance(), controller, uri,
589                             extras);
590                 });
591     }
592 
593     @Override
playFromSearch(final IMediaController2 caller, final String query, final Bundle extras)594     public void playFromSearch(final IMediaController2 caller, final String query,
595             final Bundle extras) {
596         onCommand(caller, SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH,
597                 (session, controller) -> {
598                     if (TextUtils.isEmpty(query)) {
599                         Log.w(TAG, "playFromSearch(): Ignoring empty query from " + controller);
600                         return;
601                     }
602                     session.getCallback().onPlayFromSearch(session.getInstance(),
603                             controller, query, extras);
604                 });
605     }
606 
607     @Override
playFromMediaId(final IMediaController2 caller, final String mediaId, final Bundle extras)608     public void playFromMediaId(final IMediaController2 caller, final String mediaId,
609             final Bundle extras) {
610         onCommand(caller, SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID,
611                 (session, controller) -> {
612                     if (mediaId == null) {
613                         Log.w(TAG, "playFromMediaId(): Ignoring null mediaId from " + controller);
614                         return;
615                     }
616                     session.getCallback().onPlayFromMediaId(session.getInstance(), controller,
617                             mediaId, extras);
618                 });
619     }
620 
621     @Override
setRating(final IMediaController2 caller, final String mediaId, final Bundle ratingBundle)622     public void setRating(final IMediaController2 caller, final String mediaId,
623             final Bundle ratingBundle) {
624         onCommand(caller, SessionCommand2.COMMAND_CODE_SESSION_SET_RATING,
625                 (session, controller) -> {
626                     if (mediaId == null) {
627                         Log.w(TAG, "setRating(): Ignoring null mediaId from " + controller);
628                         return;
629                     }
630                     if (ratingBundle == null) {
631                         Log.w(TAG, "setRating(): Ignoring null ratingBundle from " + controller);
632                         return;
633                     }
634                     Rating2 rating = Rating2.fromBundle(ratingBundle);
635                     if (rating == null) {
636                         if (ratingBundle == null) {
637                             Log.w(TAG, "setRating(): Ignoring null rating from " + controller);
638                             return;
639                         }
640                         return;
641                     }
642                     session.getCallback().onSetRating(session.getInstance(), controller, mediaId,
643                             rating);
644                 });
645     }
646 
647     @Override
setPlaylist(final IMediaController2 caller, final List<Bundle> playlist, final Bundle metadata)648     public void setPlaylist(final IMediaController2 caller, final List<Bundle> playlist,
649             final Bundle metadata) {
650         onCommand(caller, SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST, (session, controller) -> {
651             if (playlist == null) {
652                 Log.w(TAG, "setPlaylist(): Ignoring null playlist from " + controller);
653                 return;
654             }
655             List<MediaItem2> list = new ArrayList<>();
656             for (int i = 0; i < playlist.size(); i++) {
657                 // Recreates UUID in the playlist
658                 MediaItem2 item = MediaItem2Impl.fromBundle(playlist.get(i), null);
659                 if (item != null) {
660                     list.add(item);
661                 }
662             }
663             session.getInstance().setPlaylist(list, MediaMetadata2.fromBundle(metadata));
664         });
665     }
666 
667     @Override
updatePlaylistMetadata(final IMediaController2 caller, final Bundle metadata)668     public void updatePlaylistMetadata(final IMediaController2 caller, final Bundle metadata) {
669         onCommand(caller, SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST_METADATA,
670                 (session, controller) -> {
671             session.getInstance().updatePlaylistMetadata(MediaMetadata2.fromBundle(metadata));
672         });
673     }
674 
675     @Override
addPlaylistItem(IMediaController2 caller, int index, Bundle mediaItem)676     public void addPlaylistItem(IMediaController2 caller, int index, Bundle mediaItem) {
677         onCommand(caller, SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM,
678                 (session, controller) -> {
679                     // Resets the UUID from the incoming media id, so controller may reuse a media
680                     // item multiple times for addPlaylistItem.
681                     session.getInstance().addPlaylistItem(index,
682                             MediaItem2Impl.fromBundle(mediaItem, null));
683                 });
684     }
685 
686     @Override
removePlaylistItem(IMediaController2 caller, Bundle mediaItem)687     public void removePlaylistItem(IMediaController2 caller, Bundle mediaItem) {
688         onCommand(caller, SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM,
689                 (session, controller) -> {
690             MediaItem2 item = MediaItem2.fromBundle(mediaItem);
691             // Note: MediaItem2 has hidden UUID to identify it across the processes.
692             session.getInstance().removePlaylistItem(item);
693         });
694     }
695 
696     @Override
replacePlaylistItem(IMediaController2 caller, int index, Bundle mediaItem)697     public void replacePlaylistItem(IMediaController2 caller, int index, Bundle mediaItem) {
698         onCommand(caller, SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM,
699                 (session, controller) -> {
700                     // Resets the UUID from the incoming media id, so controller may reuse a media
701                     // item multiple times for replacePlaylistItem.
702                     session.getInstance().replacePlaylistItem(index,
703                             MediaItem2Impl.fromBundle(mediaItem, null));
704                 });
705     }
706 
707     @Override
skipToPlaylistItem(IMediaController2 caller, Bundle mediaItem)708     public void skipToPlaylistItem(IMediaController2 caller, Bundle mediaItem) {
709         onCommand(caller, SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM,
710                 (session, controller) -> {
711                     if (mediaItem == null) {
712                         Log.w(TAG, "skipToPlaylistItem(): Ignoring null mediaItem from "
713                                 + controller);
714                     }
715                     // Note: MediaItem2 has hidden UUID to identify it across the processes.
716                     session.getInstance().skipToPlaylistItem(MediaItem2.fromBundle(mediaItem));
717                 });
718     }
719 
720     @Override
skipToPreviousItem(IMediaController2 caller)721     public void skipToPreviousItem(IMediaController2 caller) {
722         onCommand(caller, SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_PREV_ITEM,
723                 (session, controller) -> {
724                     session.getInstance().skipToPreviousItem();
725                 });
726     }
727 
728     @Override
skipToNextItem(IMediaController2 caller)729     public void skipToNextItem(IMediaController2 caller) {
730         onCommand(caller, SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_NEXT_ITEM,
731                 (session, controller) -> {
732                     session.getInstance().skipToNextItem();
733                 });
734     }
735 
736     @Override
setRepeatMode(IMediaController2 caller, int repeatMode)737     public void setRepeatMode(IMediaController2 caller, int repeatMode) {
738         onCommand(caller, SessionCommand2.COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE,
739                 (session, controller) -> {
740                     session.getInstance().setRepeatMode(repeatMode);
741                 });
742     }
743 
744     @Override
setShuffleMode(IMediaController2 caller, int shuffleMode)745     public void setShuffleMode(IMediaController2 caller, int shuffleMode) {
746         onCommand(caller, SessionCommand2.COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE,
747                 (session, controller) -> {
748                     session.getInstance().setShuffleMode(shuffleMode);
749                 });
750     }
751 
752     //////////////////////////////////////////////////////////////////////////////////////////////
753     // AIDL methods for LibrarySession overrides
754     //////////////////////////////////////////////////////////////////////////////////////////////
755 
756     @Override
getLibraryRoot(final IMediaController2 caller, final Bundle rootHints)757     public void getLibraryRoot(final IMediaController2 caller, final Bundle rootHints)
758             throws RuntimeException {
759         onBrowserCommand(caller, (session, controller) -> {
760             final LibraryRoot root = session.getCallback().onGetLibraryRoot(session.getInstance(),
761                     controller, rootHints);
762             notify(controller, (unused, iController) -> {
763                 iController.onGetLibraryRootDone(rootHints,
764                         root == null ? null : root.getRootId(),
765                         root == null ? null : root.getExtras());
766             });
767         });
768     }
769 
770     @Override
getItem(final IMediaController2 caller, final String mediaId)771     public void getItem(final IMediaController2 caller, final String mediaId)
772             throws RuntimeException {
773         onBrowserCommand(caller, (session, controller) -> {
774             if (mediaId == null) {
775                 if (DEBUG) {
776                     Log.d(TAG, "mediaId shouldn't be null");
777                 }
778                 return;
779             }
780             final MediaItem2 result = session.getCallback().onGetItem(session.getInstance(),
781                     controller, mediaId);
782             notify(controller, (unused, iController) -> {
783                 iController.onGetItemDone(mediaId, result == null ? null : result.toBundle());
784             });
785         });
786     }
787 
788     @Override
getChildren(final IMediaController2 caller, final String parentId, final int page, final int pageSize, final Bundle extras)789     public void getChildren(final IMediaController2 caller, final String parentId,
790             final int page, final int pageSize, final Bundle extras) throws RuntimeException {
791         onBrowserCommand(caller, (session, controller) -> {
792             if (parentId == null) {
793                 if (DEBUG) {
794                     Log.d(TAG, "parentId shouldn't be null");
795                 }
796                 return;
797             }
798             if (page < 1 || pageSize < 1) {
799                 if (DEBUG) {
800                     Log.d(TAG, "Neither page nor pageSize should be less than 1");
801                 }
802                 return;
803             }
804             List<MediaItem2> result = session.getCallback().onGetChildren(session.getInstance(),
805                     controller, parentId, page, pageSize, extras);
806             if (result != null && result.size() > pageSize) {
807                 throw new IllegalArgumentException("onGetChildren() shouldn't return media items "
808                         + "more than pageSize. result.size()=" + result.size() + " pageSize="
809                         + pageSize);
810             }
811             final List<Bundle> bundleList;
812             if (result != null) {
813                 bundleList = new ArrayList<>();
814                 for (MediaItem2 item : result) {
815                     bundleList.add(item == null ? null : item.toBundle());
816                 }
817             } else {
818                 bundleList = null;
819             }
820             notify(controller, (unused, iController) -> {
821                 iController.onGetChildrenDone(parentId, page, pageSize, bundleList, extras);
822             });
823         });
824     }
825 
826     @Override
search(IMediaController2 caller, String query, Bundle extras)827     public void search(IMediaController2 caller, String query, Bundle extras) {
828         onBrowserCommand(caller, (session, controller) -> {
829             if (TextUtils.isEmpty(query)) {
830                 Log.w(TAG, "search(): Ignoring empty query from " + controller);
831                 return;
832             }
833             session.getCallback().onSearch(session.getInstance(), controller, query, extras);
834         });
835     }
836 
837     @Override
getSearchResult(final IMediaController2 caller, final String query, final int page, final int pageSize, final Bundle extras)838     public void getSearchResult(final IMediaController2 caller, final String query,
839             final int page, final int pageSize, final Bundle extras) {
840         onBrowserCommand(caller, (session, controller) -> {
841             if (TextUtils.isEmpty(query)) {
842                 Log.w(TAG, "getSearchResult(): Ignoring empty query from " + controller);
843                 return;
844             }
845             if (page < 1 || pageSize < 1) {
846                 Log.w(TAG, "getSearchResult(): Ignoring negative page / pageSize."
847                         + " page=" + page + " pageSize=" + pageSize + " from " + controller);
848                 return;
849             }
850             List<MediaItem2> result = session.getCallback().onGetSearchResult(session.getInstance(),
851                     controller, query, page, pageSize, extras);
852             if (result != null && result.size() > pageSize) {
853                 throw new IllegalArgumentException("onGetSearchResult() shouldn't return media "
854                         + "items more than pageSize. result.size()=" + result.size() + " pageSize="
855                         + pageSize);
856             }
857             final List<Bundle> bundleList;
858             if (result != null) {
859                 bundleList = new ArrayList<>();
860                 for (MediaItem2 item : result) {
861                     bundleList.add(item == null ? null : item.toBundle());
862                 }
863             } else {
864                 bundleList = null;
865             }
866             notify(controller, (unused, iController) -> {
867                 iController.onGetSearchResultDone(query, page, pageSize, bundleList, extras);
868             });
869         });
870     }
871 
872     @Override
subscribe(final IMediaController2 caller, final String parentId, final Bundle option)873     public void subscribe(final IMediaController2 caller, final String parentId,
874             final Bundle option) {
875         onBrowserCommand(caller, (session, controller) -> {
876             if (parentId == null) {
877                 Log.w(TAG, "subscribe(): Ignoring null parentId from " + controller);
878                 return;
879             }
880             session.getCallback().onSubscribe(session.getInstance(),
881                     controller, parentId, option);
882             synchronized (mLock) {
883                 Set<String> subscription = mSubscriptions.get(controller);
884                 if (subscription == null) {
885                     subscription = new HashSet<>();
886                     mSubscriptions.put(controller, subscription);
887                 }
888                 subscription.add(parentId);
889             }
890         });
891     }
892 
893     @Override
unsubscribe(final IMediaController2 caller, final String parentId)894     public void unsubscribe(final IMediaController2 caller, final String parentId) {
895         onBrowserCommand(caller, (session, controller) -> {
896             if (parentId == null) {
897                 Log.w(TAG, "unsubscribe(): Ignoring null parentId from " + controller);
898                 return;
899             }
900             session.getCallback().onUnsubscribe(session.getInstance(), controller, parentId);
901             synchronized (mLock) {
902                 mSubscriptions.remove(controller);
903             }
904         });
905     }
906 
907     //////////////////////////////////////////////////////////////////////////////////////////////
908     // APIs for MediaSession2Impl
909     //////////////////////////////////////////////////////////////////////////////////////////////
910 
911     // TODO(jaewan): (Can be Post-P) Need a way to get controller with permissions
getControllers()912     public List<ControllerInfo> getControllers() {
913         ArrayList<ControllerInfo> controllers = new ArrayList<>();
914         synchronized (mLock) {
915             for (int i = 0; i < mControllers.size(); i++) {
916                 controllers.add(mControllers.valueAt(i));
917             }
918         }
919         return controllers;
920     }
921 
922     // Should be used without a lock to prevent potential deadlock.
notifyPlayerStateChangedNotLocked(int state)923     public void notifyPlayerStateChangedNotLocked(int state) {
924         notifyAll((controller, iController) -> {
925             iController.onPlayerStateChanged(state);
926         });
927     }
928 
929     // TODO(jaewan): Rename
notifyPositionChangedNotLocked(long eventTimeMs, long positionMs)930     public void notifyPositionChangedNotLocked(long eventTimeMs, long positionMs) {
931         notifyAll((controller, iController) -> {
932             iController.onPositionChanged(eventTimeMs, positionMs);
933         });
934     }
935 
notifyPlaybackSpeedChangedNotLocked(float speed)936     public void notifyPlaybackSpeedChangedNotLocked(float speed) {
937         notifyAll((controller, iController) -> {
938             iController.onPlaybackSpeedChanged(speed);
939         });
940     }
941 
notifyBufferedPositionChangedNotLocked(long bufferedPositionMs)942     public void notifyBufferedPositionChangedNotLocked(long bufferedPositionMs) {
943         notifyAll((controller, iController) -> {
944             iController.onBufferedPositionChanged(bufferedPositionMs);
945         });
946     }
947 
notifyCustomLayoutNotLocked(ControllerInfo controller, List<CommandButton> layout)948     public void notifyCustomLayoutNotLocked(ControllerInfo controller, List<CommandButton> layout) {
949         notify(controller, (unused, iController) -> {
950             List<Bundle> layoutBundles = new ArrayList<>();
951             for (int i = 0; i < layout.size(); i++) {
952                 Bundle bundle = ((CommandButtonImpl) layout.get(i).getProvider()).toBundle();
953                 if (bundle != null) {
954                     layoutBundles.add(bundle);
955                 }
956             }
957             iController.onCustomLayoutChanged(layoutBundles);
958         });
959     }
960 
notifyPlaylistChangedNotLocked(List<MediaItem2> playlist, MediaMetadata2 metadata)961     public void notifyPlaylistChangedNotLocked(List<MediaItem2> playlist, MediaMetadata2 metadata) {
962         final List<Bundle> bundleList;
963         if (playlist != null) {
964             bundleList = new ArrayList<>();
965             for (int i = 0; i < playlist.size(); i++) {
966                 if (playlist.get(i) != null) {
967                     Bundle bundle = playlist.get(i).toBundle();
968                     if (bundle != null) {
969                         bundleList.add(bundle);
970                     }
971                 }
972             }
973         } else {
974             bundleList = null;
975         }
976         final Bundle metadataBundle = (metadata == null) ? null : metadata.toBundle();
977         notifyAll((controller, iController) -> {
978             if (getControllerBinderIfAble(controller,
979                     SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST) != null) {
980                 iController.onPlaylistChanged(bundleList, metadataBundle);
981             } else if (getControllerBinderIfAble(controller,
982                     SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST_METADATA) != null) {
983                 iController.onPlaylistMetadataChanged(metadataBundle);
984             }
985         });
986     }
987 
notifyPlaylistMetadataChangedNotLocked(MediaMetadata2 metadata)988     public void notifyPlaylistMetadataChangedNotLocked(MediaMetadata2 metadata) {
989         final Bundle metadataBundle = (metadata == null) ? null : metadata.toBundle();
990         notifyAll(SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST_METADATA,
991                 (unused, iController) -> {
992                     iController.onPlaylistMetadataChanged(metadataBundle);
993                 });
994     }
995 
notifyRepeatModeChangedNotLocked(int repeatMode)996     public void notifyRepeatModeChangedNotLocked(int repeatMode) {
997         notifyAll((unused, iController) -> {
998             iController.onRepeatModeChanged(repeatMode);
999         });
1000     }
1001 
notifyShuffleModeChangedNotLocked(int shuffleMode)1002     public void notifyShuffleModeChangedNotLocked(int shuffleMode) {
1003         notifyAll((unused, iController) -> {
1004             iController.onShuffleModeChanged(shuffleMode);
1005         });
1006     }
1007 
notifyPlaybackInfoChanged(MediaController2.PlaybackInfo playbackInfo)1008     public void notifyPlaybackInfoChanged(MediaController2.PlaybackInfo playbackInfo) {
1009         final Bundle playbackInfoBundle =
1010                 ((MediaController2Impl.PlaybackInfoImpl) playbackInfo.getProvider()).toBundle();
1011         notifyAll((unused, iController) -> {
1012             iController.onPlaybackInfoChanged(playbackInfoBundle);
1013         });
1014     }
1015 
setAllowedCommands(ControllerInfo controller, SessionCommandGroup2 commands)1016     public void setAllowedCommands(ControllerInfo controller, SessionCommandGroup2 commands) {
1017         synchronized (mLock) {
1018             mAllowedCommandGroupMap.put(controller, commands);
1019         }
1020         notify(controller, (unused, iController) -> {
1021             iController.onAllowedCommandsChanged(commands.toBundle());
1022         });
1023     }
1024 
sendCustomCommand(ControllerInfo controller, SessionCommand2 command, Bundle args, ResultReceiver receiver)1025     public void sendCustomCommand(ControllerInfo controller, SessionCommand2 command, Bundle args,
1026             ResultReceiver receiver) {
1027         if (receiver != null && controller == null) {
1028             throw new IllegalArgumentException("Controller shouldn't be null if result receiver is"
1029                     + " specified");
1030         }
1031         if (command == null) {
1032             throw new IllegalArgumentException("command shouldn't be null");
1033         }
1034         notify(controller, (unused, iController) -> {
1035             Bundle commandBundle = command.toBundle();
1036             iController.onCustomCommand(commandBundle, args, null);
1037         });
1038     }
1039 
sendCustomCommand(SessionCommand2 command, Bundle args)1040     public void sendCustomCommand(SessionCommand2 command, Bundle args) {
1041         if (command == null) {
1042             throw new IllegalArgumentException("command shouldn't be null");
1043         }
1044         Bundle commandBundle = command.toBundle();
1045         notifyAll((unused, iController) -> {
1046             iController.onCustomCommand(commandBundle, args, null);
1047         });
1048     }
1049 
notifyError(int errorCode, Bundle extras)1050     public void notifyError(int errorCode, Bundle extras) {
1051         notifyAll((unused, iController) -> {
1052             iController.onError(errorCode, extras);
1053         });
1054     }
1055 
1056     //////////////////////////////////////////////////////////////////////////////////////////////
1057     // APIs for MediaLibrarySessionImpl
1058     //////////////////////////////////////////////////////////////////////////////////////////////
1059 
notifySearchResultChanged(ControllerInfo controller, String query, int itemCount, Bundle extras)1060     public void notifySearchResultChanged(ControllerInfo controller, String query, int itemCount,
1061             Bundle extras) {
1062         notify(controller, (unused, iController) -> {
1063             iController.onSearchResultChanged(query, itemCount, extras);
1064         });
1065     }
1066 
notifyChildrenChangedNotLocked(ControllerInfo controller, String parentId, int itemCount, Bundle extras)1067     public void notifyChildrenChangedNotLocked(ControllerInfo controller, String parentId,
1068             int itemCount, Bundle extras) {
1069         notify(controller, (unused, iController) -> {
1070             if (isSubscribed(controller, parentId)) {
1071                 iController.onChildrenChanged(parentId, itemCount, extras);
1072             }
1073         });
1074     }
1075 
notifyChildrenChangedNotLocked(String parentId, int itemCount, Bundle extras)1076     public void notifyChildrenChangedNotLocked(String parentId, int itemCount, Bundle extras) {
1077         notifyAll((controller, iController) -> {
1078             if (isSubscribed(controller, parentId)) {
1079                 iController.onChildrenChanged(parentId, itemCount, extras);
1080             }
1081         });
1082     }
1083 
isSubscribed(ControllerInfo controller, String parentId)1084     private boolean isSubscribed(ControllerInfo controller, String parentId) {
1085         synchronized (mLock) {
1086             Set<String> subscriptions = mSubscriptions.get(controller);
1087             if (subscriptions == null || !subscriptions.contains(parentId)) {
1088                 return false;
1089             }
1090         }
1091         return true;
1092     }
1093 
1094     //////////////////////////////////////////////////////////////////////////////////////////////
1095     // Misc
1096     //////////////////////////////////////////////////////////////////////////////////////////////
1097 
1098     @FunctionalInterface
1099     private interface SessionRunnable {
run(final MediaSession2Impl session, final ControllerInfo controller)1100         void run(final MediaSession2Impl session, final ControllerInfo controller);
1101     }
1102 
1103     @FunctionalInterface
1104     private interface LibrarySessionRunnable {
run(final MediaLibrarySessionImpl session, final ControllerInfo controller)1105         void run(final MediaLibrarySessionImpl session, final ControllerInfo controller);
1106     }
1107 
1108     @FunctionalInterface
1109     private interface NotifyRunnable {
run(final ControllerInfo controller, final IMediaController2 iController)1110         void run(final ControllerInfo controller,
1111                 final IMediaController2 iController) throws RemoteException;
1112     }
1113 }
1114