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