Home | History | Annotate | Download | only in media
      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 
     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 
     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 
    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 
    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.
    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.
    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.
    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.
    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.
    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 
    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 
    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 
    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 
    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 
    291     private void notify(@NonNull ControllerInfo controller, @NonNull NotifyRunnable runnable) {
    292         notifyInternal(controller, getControllerBinderIfAble(controller), runnable);
    293     }
    294 
    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.
    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 
    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
    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
    453     public void release(final IMediaController2 caller) throws RemoteException {
    454         onControllerClosed(caller);
    455     }
    456 
    457     @Override
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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.
    923     public void notifyPlayerStateChangedNotLocked(int state) {
    924         notifyAll((controller, iController) -> {
    925             iController.onPlayerStateChanged(state);
    926         });
    927     }
    928 
    929     // TODO(jaewan): Rename
    930     public void notifyPositionChangedNotLocked(long eventTimeMs, long positionMs) {
    931         notifyAll((controller, iController) -> {
    932             iController.onPositionChanged(eventTimeMs, positionMs);
    933         });
    934     }
    935 
    936     public void notifyPlaybackSpeedChangedNotLocked(float speed) {
    937         notifyAll((controller, iController) -> {
    938             iController.onPlaybackSpeedChanged(speed);
    939         });
    940     }
    941 
    942     public void notifyBufferedPositionChangedNotLocked(long bufferedPositionMs) {
    943         notifyAll((controller, iController) -> {
    944             iController.onBufferedPositionChanged(bufferedPositionMs);
    945         });
    946     }
    947 
    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 
    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 
    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 
    996     public void notifyRepeatModeChangedNotLocked(int repeatMode) {
    997         notifyAll((unused, iController) -> {
    998             iController.onRepeatModeChanged(repeatMode);
    999         });
   1000     }
   1001 
   1002     public void notifyShuffleModeChangedNotLocked(int shuffleMode) {
   1003         notifyAll((unused, iController) -> {
   1004             iController.onShuffleModeChanged(shuffleMode);
   1005         });
   1006     }
   1007 
   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 
   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 
   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 
   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 
   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 
   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 
   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 
   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 
   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 {
   1100         void run(final MediaSession2Impl session, final ControllerInfo controller);
   1101     }
   1102 
   1103     @FunctionalInterface
   1104     private interface LibrarySessionRunnable {
   1105         void run(final MediaLibrarySessionImpl session, final ControllerInfo controller);
   1106     }
   1107 
   1108     @FunctionalInterface
   1109     private interface NotifyRunnable {
   1110         void run(final ControllerInfo controller,
   1111                 final IMediaController2 iController) throws RemoteException;
   1112     }
   1113 }
   1114