Home | History | Annotate | Download | only in newavrcp
      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.bluetooth.avrcp;
     18 
     19 import android.annotation.Nullable;
     20 import android.media.MediaMetadata;
     21 import android.media.session.MediaSession;
     22 import android.media.session.PlaybackState;
     23 import android.os.Handler;
     24 import android.os.Looper;
     25 import android.os.Message;
     26 import android.support.annotation.GuardedBy;
     27 import android.support.annotation.VisibleForTesting;
     28 import android.util.Log;
     29 
     30 import java.util.List;
     31 import java.util.Objects;
     32 
     33 /*
     34  * A class to synchronize Media Controller Callbacks and only pass through
     35  * an update once all the relevant information is current.
     36  *
     37  * TODO (apanicke): Once MediaPlayer2 is supported better, replace this class
     38  * with that.
     39  */
     40 class MediaPlayerWrapper {
     41     private static final String TAG = "NewAvrcpMediaPlayerWrapper";
     42     private static final boolean DEBUG = true;
     43     static boolean sTesting = false;
     44 
     45     private MediaController mMediaController;
     46     private String mPackageName;
     47     private Looper mLooper;
     48 
     49     private MediaData mCurrentData;
     50 
     51     @GuardedBy("mCallbackLock")
     52     private MediaControllerListener mControllerCallbacks = null;
     53     private final Object mCallbackLock = new Object();
     54     private Callback mRegisteredCallback = null;
     55 
     56 
     57     protected MediaPlayerWrapper() {
     58         mCurrentData = new MediaData(null, null, null);
     59     }
     60 
     61     public interface Callback {
     62         void mediaUpdatedCallback(MediaData data);
     63     }
     64 
     65     boolean isReady() {
     66         if (getPlaybackState() == null) {
     67             d("isReady(): PlaybackState is null");
     68             return false;
     69         }
     70 
     71         if (getMetadata() == null) {
     72             d("isReady(): Metadata is null");
     73             return false;
     74         }
     75 
     76         return true;
     77     }
     78 
     79     // TODO (apanicke): Implement a factory to make testing and creating interop wrappers easier
     80     static MediaPlayerWrapper wrap(MediaController controller, Looper looper) {
     81         if (controller == null || looper == null) {
     82             e("MediaPlayerWrapper.wrap(): Null parameter - Controller: " + controller
     83                     + " | Looper: " + looper);
     84             return null;
     85         }
     86 
     87         MediaPlayerWrapper newWrapper;
     88         if (controller.getPackageName().equals("com.google.android.music")) {
     89             Log.v(TAG, "Creating compatibility wrapper for Google Play Music");
     90             newWrapper = new GPMWrapper();
     91         } else {
     92             newWrapper = new MediaPlayerWrapper();
     93         }
     94 
     95         newWrapper.mMediaController = controller;
     96         newWrapper.mPackageName = controller.getPackageName();
     97         newWrapper.mLooper = looper;
     98 
     99         newWrapper.mCurrentData.queue = Util.toMetadataList(newWrapper.getQueue());
    100         newWrapper.mCurrentData.metadata = Util.toMetadata(newWrapper.getMetadata());
    101         newWrapper.mCurrentData.state = newWrapper.getPlaybackState();
    102         return newWrapper;
    103     }
    104 
    105     void cleanup() {
    106         unregisterCallback();
    107 
    108         mMediaController = null;
    109         mLooper = null;
    110     }
    111 
    112     String getPackageName() {
    113         return mPackageName;
    114     }
    115 
    116     protected List<MediaSession.QueueItem> getQueue() {
    117         return mMediaController.getQueue();
    118     }
    119 
    120     protected MediaMetadata getMetadata() {
    121         return mMediaController.getMetadata();
    122     }
    123 
    124     Metadata getCurrentMetadata() {
    125         // Try to use the now playing list if the information exists.
    126         if (getActiveQueueID() != -1) {
    127             for (Metadata data : getCurrentQueue()) {
    128                 if (data.mediaId.equals(Util.NOW_PLAYING_PREFIX + getActiveQueueID())) {
    129                     d("getCurrentMetadata: Using playlist data: " + data.toString());
    130                     return data.clone();
    131                 }
    132             }
    133         }
    134 
    135         return Util.toMetadata(getMetadata());
    136     }
    137 
    138     PlaybackState getPlaybackState() {
    139         return mMediaController.getPlaybackState();
    140     }
    141 
    142     long getActiveQueueID() {
    143         if (mMediaController.getPlaybackState() == null) return -1;
    144         return mMediaController.getPlaybackState().getActiveQueueItemId();
    145     }
    146 
    147     List<Metadata> getCurrentQueue() {
    148         return mCurrentData.queue;
    149     }
    150 
    151     // We don't return the cached info here in order to always provide the freshest data.
    152     MediaData getCurrentMediaData() {
    153         MediaData data = new MediaData(
    154                 getCurrentMetadata(),
    155                 getPlaybackState(),
    156                 getCurrentQueue());
    157         return data;
    158     }
    159 
    160     void playItemFromQueue(long qid) {
    161         // Return immediately if no queue exists.
    162         if (getQueue() == null) {
    163             Log.w(TAG, "playItemFromQueue: Trying to play item for player that has no queue: "
    164                     + mPackageName);
    165             return;
    166         }
    167 
    168         MediaController.TransportControls controller = mMediaController.getTransportControls();
    169         controller.skipToQueueItem(qid);
    170     }
    171 
    172     // TODO (apanicke): Implement shuffle and repeat support. Right now these use custom actions
    173     // and it may only be possible to do this with Google Play Music
    174     boolean isShuffleSupported() {
    175         return false;
    176     }
    177 
    178     boolean isRepeatSupported() {
    179         return false;
    180     }
    181 
    182     void toggleShuffle(boolean on) {
    183         return;
    184     }
    185 
    186     void toggleRepeat(boolean on) {
    187         return;
    188     }
    189 
    190     /**
    191      * Return whether the queue, metadata, and queueID are all in sync.
    192      */
    193     boolean isMetadataSynced() {
    194         if (getQueue() != null && getActiveQueueID() != -1) {
    195             // Check if currentPlayingQueueId is in the current Queue
    196             MediaSession.QueueItem currItem = null;
    197 
    198             for (MediaSession.QueueItem item : getQueue()) {
    199                 if (item.getQueueId()
    200                         == getActiveQueueID()) { // The item exists in the current queue
    201                     currItem = item;
    202                     break;
    203                 }
    204             }
    205 
    206             // Check if current playing song in Queue matches current Metadata
    207             Metadata qitem = Util.toMetadata(currItem);
    208             Metadata mdata = Util.toMetadata(getMetadata());
    209             if (currItem == null || !qitem.equals(mdata)) {
    210                 if (DEBUG) {
    211                     Log.d(TAG, "Metadata currently out of sync for " + mPackageName);
    212                     Log.d(TAG, "   Current queueItem: " + qitem);
    213                     Log.d(TAG, "   Current metadata : " + mdata);
    214                 }
    215                 return false;
    216             }
    217         }
    218 
    219         return true;
    220     }
    221 
    222     /**
    223      * Register a callback which gets called when media updates happen. The callbacks are
    224      * called on the same Looper that was passed in to create this object.
    225      */
    226     void registerCallback(Callback callback) {
    227         if (callback == null) {
    228             e("Cannot register null callbacks for " + mPackageName);
    229             return;
    230         }
    231 
    232         synchronized (mCallbackLock) {
    233             mRegisteredCallback = callback;
    234         }
    235 
    236         // Update the current data since it could have changed while we weren't registered for
    237         // updates
    238         mCurrentData = new MediaData(
    239                 Util.toMetadata(getMetadata()),
    240                 getPlaybackState(),
    241                 Util.toMetadataList(getQueue()));
    242 
    243         mControllerCallbacks = new MediaControllerListener(mLooper);
    244     }
    245 
    246     /**
    247      * Unregisters from updates. Note, this doesn't require the looper to be shut down.
    248      */
    249     void unregisterCallback() {
    250         // Prevent a race condition where a callback could be called while shutting down
    251         synchronized (mCallbackLock) {
    252             mRegisteredCallback = null;
    253         }
    254 
    255         if (mControllerCallbacks == null) return;
    256         mControllerCallbacks.cleanup();
    257         mControllerCallbacks = null;
    258     }
    259 
    260     void updateMediaController(MediaController newController) {
    261         if (newController == mMediaController) return;
    262 
    263         synchronized (mCallbackLock) {
    264             if (mRegisteredCallback == null || mControllerCallbacks == null) {
    265                 return;
    266             }
    267         }
    268 
    269         mControllerCallbacks.cleanup();
    270         mMediaController = newController;
    271 
    272         // Update the current data since it could be different on the new controller for the player
    273         mCurrentData = new MediaData(
    274                 Util.toMetadata(getMetadata()),
    275                 getPlaybackState(),
    276                 Util.toMetadataList(getQueue()));
    277 
    278         mControllerCallbacks = new MediaControllerListener(mLooper);
    279         d("Controller for " + mPackageName + " was updated.");
    280     }
    281 
    282     private void sendMediaUpdate() {
    283         MediaData newData = new MediaData(
    284                 Util.toMetadata(getMetadata()),
    285                 getPlaybackState(),
    286                 Util.toMetadataList(getQueue()));
    287 
    288         if (newData.equals(mCurrentData)) {
    289             // This may happen if the controller is fully synced by the time the
    290             // first update is completed
    291             Log.v(TAG, "Trying to update with last sent metadata");
    292             return;
    293         }
    294 
    295         synchronized (mCallbackLock) {
    296             if (mRegisteredCallback == null) {
    297                 Log.e(TAG, mPackageName
    298                         + "Trying to send an update with no registered callback");
    299                 return;
    300             }
    301 
    302             Log.v(TAG, "trySendMediaUpdate(): Metadata has been updated for " + mPackageName);
    303             mRegisteredCallback.mediaUpdatedCallback(newData);
    304         }
    305 
    306         mCurrentData = newData;
    307     }
    308 
    309     class TimeoutHandler extends Handler {
    310         private static final int MSG_TIMEOUT = 0;
    311         private static final long CALLBACK_TIMEOUT_MS = 2000;
    312 
    313         TimeoutHandler(Looper looper) {
    314             super(looper);
    315         }
    316 
    317         @Override
    318         public void handleMessage(Message msg) {
    319             if (msg.what != MSG_TIMEOUT) {
    320                 Log.wtf(TAG, "Unknown message on timeout handler: " + msg.what);
    321                 return;
    322             }
    323 
    324             Log.e(TAG, "Timeout while waiting for metadata to sync for " + mPackageName);
    325             Log.e(TAG, "   Current Metadata: " +  Util.toMetadata(getMetadata()));
    326             Log.e(TAG, "   Current Playstate: " + getPlaybackState());
    327             List<Metadata> current_queue = Util.toMetadataList(getQueue());
    328             for (int i = 0; i < current_queue.size(); i++) {
    329                 Log.e(TAG, "   QueueItem(" + i + "): " + current_queue.get(i));
    330             }
    331 
    332             sendMediaUpdate();
    333 
    334             // TODO(apanicke): Add metric collection here.
    335 
    336             if (sTesting) Log.wtfStack(TAG, "Crashing the stack");
    337         }
    338     }
    339 
    340     class MediaControllerListener extends MediaController.Callback {
    341         private final Object mTimeoutHandlerLock = new Object();
    342         private Handler mTimeoutHandler;
    343 
    344         MediaControllerListener(Looper newLooper) {
    345             synchronized (mTimeoutHandlerLock) {
    346                 mTimeoutHandler = new TimeoutHandler(newLooper);
    347 
    348                 // Register the callbacks to execute on the same thread as the timeout thread. This
    349                 // prevents a race condition where a timeout happens at the same time as an update.
    350                 mMediaController.registerCallback(this, mTimeoutHandler);
    351             }
    352         }
    353 
    354         void cleanup() {
    355             synchronized (mTimeoutHandlerLock) {
    356                 mMediaController.unregisterCallback(this);
    357                 mTimeoutHandler.removeMessages(TimeoutHandler.MSG_TIMEOUT);
    358                 mTimeoutHandler = null;
    359             }
    360         }
    361 
    362         void trySendMediaUpdate() {
    363             synchronized (mTimeoutHandlerLock) {
    364                 if (mTimeoutHandler == null) return;
    365                 mTimeoutHandler.removeMessages(TimeoutHandler.MSG_TIMEOUT);
    366 
    367                 if (!isMetadataSynced()) {
    368                     d("trySendMediaUpdate(): Starting media update timeout");
    369                     mTimeoutHandler.sendEmptyMessageDelayed(TimeoutHandler.MSG_TIMEOUT,
    370                             TimeoutHandler.CALLBACK_TIMEOUT_MS);
    371                     return;
    372                 }
    373             }
    374 
    375             sendMediaUpdate();
    376         }
    377 
    378         @Override
    379         public void onMetadataChanged(@Nullable MediaMetadata metadata) {
    380             if (!isReady()) {
    381                 Log.v(TAG, "onMetadataChanged(): " + mPackageName
    382                         + " tried to update with no queue");
    383                 return;
    384             }
    385 
    386             Log.v(TAG, "onMetadataChanged(): " + mPackageName + " : " + Util.toMetadata(metadata));
    387 
    388             if (!Objects.equals(metadata, getMetadata())) {
    389                 e("The callback metadata doesn't match controller metadata");
    390             }
    391 
    392             // TODO: Certain players update different metadata fields as they load, such as Album
    393             // Art. For track changed updates we only care about the song information like title
    394             // and album and duration. In the future we can use this to know when Album art is
    395             // loaded.
    396 
    397             // TODO: Spotify needs a metadata update debouncer as it sometimes updates the metadata
    398             // twice in a row with the only difference being that the song duration is rounded to
    399             // the nearest second.
    400             if (Objects.equals(metadata, mCurrentData.metadata)) {
    401                 Log.w(TAG, "onMetadataChanged(): " + mPackageName
    402                         + " tried to update with no new data");
    403                 return;
    404             }
    405 
    406             trySendMediaUpdate();
    407         }
    408 
    409         @Override
    410         public void onPlaybackStateChanged(@Nullable PlaybackState state) {
    411             if (!isReady()) {
    412                 Log.v(TAG, "onPlaybackStateChanged(): " + mPackageName
    413                         + " tried to update with no queue");
    414                 return;
    415             }
    416 
    417             Log.v(TAG, "onPlaybackStateChanged(): " + mPackageName + " : " + state.toString());
    418 
    419             if (!playstateEquals(state, getPlaybackState())) {
    420                 e("The callback playback state doesn't match the current state");
    421             }
    422 
    423             if (playstateEquals(state, mCurrentData.state)) {
    424                 Log.w(TAG, "onPlaybackStateChanged(): " + mPackageName
    425                         + " tried to update with no new data");
    426                 return;
    427             }
    428 
    429             // If there is no playstate, ignore the update.
    430             if (state.getState() == PlaybackState.STATE_NONE) {
    431                 Log.v(TAG, "Waiting to send update as controller has no playback state");
    432                 return;
    433             }
    434 
    435             trySendMediaUpdate();
    436         }
    437 
    438         @Override
    439         public void onQueueChanged(@Nullable List<MediaSession.QueueItem> queue) {
    440             if (!isReady()) {
    441                 Log.v(TAG, "onQueueChanged(): " + mPackageName
    442                         + " tried to update with no queue");
    443                 return;
    444             }
    445 
    446             Log.v(TAG, "onQueueChanged(): " + mPackageName);
    447 
    448             if (!Objects.equals(queue, getQueue())) {
    449                 e("The callback queue isn't the current queue");
    450             }
    451 
    452             List<Metadata> current_queue = Util.toMetadataList(queue);
    453             if (current_queue.equals(mCurrentData.queue)) {
    454                 Log.w(TAG, "onQueueChanged(): " + mPackageName
    455                         + " tried to update with no new data");
    456                 return;
    457             }
    458 
    459             if (DEBUG) {
    460                 for (int i = 0; i < current_queue.size(); i++) {
    461                     Log.d(TAG, "   QueueItem(" + i + "): " + current_queue.get(i));
    462                 }
    463             }
    464 
    465             trySendMediaUpdate();
    466         }
    467 
    468         @Override
    469         public void onSessionDestroyed() {
    470             Log.w(TAG, "The session was destroyed " + mPackageName);
    471         }
    472 
    473         @VisibleForTesting
    474         Handler getTimeoutHandler() {
    475             return mTimeoutHandler;
    476         }
    477     }
    478 
    479     /**
    480      * Checks wheter the core information of two PlaybackStates match. This function allows a
    481      * certain amount of deviation between the position fields of the PlaybackStates. This is to
    482      * prevent matches from failing when updates happen in quick succession.
    483      *
    484      * The maximum allowed deviation is defined by PLAYSTATE_BOUNCE_IGNORE_PERIOD and is measured
    485      * in milliseconds.
    486      */
    487     private static final long PLAYSTATE_BOUNCE_IGNORE_PERIOD = 500;
    488     static boolean playstateEquals(PlaybackState a, PlaybackState b) {
    489         if (a == b) return true;
    490 
    491         if (a != null && b != null
    492                 && a.getState() == b.getState()
    493                 && a.getActiveQueueItemId() == b.getActiveQueueItemId()
    494                 && Math.abs(a.getPosition() - b.getPosition()) < PLAYSTATE_BOUNCE_IGNORE_PERIOD) {
    495             return true;
    496         }
    497 
    498         return false;
    499     }
    500 
    501     private static void e(String message) {
    502         if (sTesting) {
    503             Log.wtfStack(TAG, message);
    504         } else {
    505             Log.e(TAG, message);
    506         }
    507     }
    508 
    509     private void d(String message) {
    510         if (DEBUG) Log.d(TAG, mPackageName + ": " + message);
    511     }
    512 
    513     @VisibleForTesting
    514     Handler getTimeoutHandler() {
    515         if (mControllerCallbacks == null) return null;
    516         return mControllerCallbacks.getTimeoutHandler();
    517     }
    518 
    519     @Override
    520     public String toString() {
    521         StringBuilder sb = new StringBuilder();
    522         sb.append(mMediaController.toString() + "\n");
    523         sb.append("Current Data:\n");
    524         sb.append("  Song: " + mCurrentData.metadata + "\n");
    525         sb.append("  PlayState: " + mCurrentData.state + "\n");
    526         sb.append("  Queue: size=" + mCurrentData.queue.size() + "\n");
    527         for (Metadata data : mCurrentData.queue) {
    528             sb.append("    " + data + "\n");
    529         }
    530         return sb.toString();
    531     }
    532 }
    533