• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.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 
MediaPlayerWrapper()57     protected MediaPlayerWrapper() {
58         mCurrentData = new MediaData(null, null, null);
59     }
60 
61     public interface Callback {
mediaUpdatedCallback(MediaData data)62         void mediaUpdatedCallback(MediaData data);
63     }
64 
isReady()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
wrap(MediaController controller, Looper looper)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 
cleanup()105     void cleanup() {
106         unregisterCallback();
107 
108         mMediaController = null;
109         mLooper = null;
110     }
111 
getPackageName()112     String getPackageName() {
113         return mPackageName;
114     }
115 
getQueue()116     protected List<MediaSession.QueueItem> getQueue() {
117         return mMediaController.getQueue();
118     }
119 
getMetadata()120     protected MediaMetadata getMetadata() {
121         return mMediaController.getMetadata();
122     }
123 
getCurrentMetadata()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 
getPlaybackState()138     PlaybackState getPlaybackState() {
139         return mMediaController.getPlaybackState();
140     }
141 
getActiveQueueID()142     long getActiveQueueID() {
143         if (mMediaController.getPlaybackState() == null) return -1;
144         return mMediaController.getPlaybackState().getActiveQueueItemId();
145     }
146 
getCurrentQueue()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.
getCurrentMediaData()152     MediaData getCurrentMediaData() {
153         MediaData data = new MediaData(
154                 getCurrentMetadata(),
155                 getPlaybackState(),
156                 getCurrentQueue());
157         return data;
158     }
159 
playItemFromQueue(long qid)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
isShuffleSupported()174     boolean isShuffleSupported() {
175         return false;
176     }
177 
isRepeatSupported()178     boolean isRepeatSupported() {
179         return false;
180     }
181 
toggleShuffle(boolean on)182     void toggleShuffle(boolean on) {
183         return;
184     }
185 
toggleRepeat(boolean on)186     void toggleRepeat(boolean on) {
187         return;
188     }
189 
190     /**
191      * Return whether the queue, metadata, and queueID are all in sync.
192      */
isMetadataSynced()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      */
registerCallback(Callback callback)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      */
unregisterCallback()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 
updateMediaController(MediaController newController)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 
sendMediaUpdate()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 
TimeoutHandler(Looper looper)313         TimeoutHandler(Looper looper) {
314             super(looper);
315         }
316 
317         @Override
handleMessage(Message msg)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 
MediaControllerListener(Looper newLooper)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 
cleanup()354         void cleanup() {
355             synchronized (mTimeoutHandlerLock) {
356                 mMediaController.unregisterCallback(this);
357                 mTimeoutHandler.removeMessages(TimeoutHandler.MSG_TIMEOUT);
358                 mTimeoutHandler = null;
359             }
360         }
361 
trySendMediaUpdate()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
onMetadataChanged(@ullable MediaMetadata metadata)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
onPlaybackStateChanged(@ullable PlaybackState state)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
onQueueChanged(@ullable List<MediaSession.QueueItem> queue)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
onSessionDestroyed()469         public void onSessionDestroyed() {
470             Log.w(TAG, "The session was destroyed " + mPackageName);
471         }
472 
473         @VisibleForTesting
getTimeoutHandler()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;
playstateEquals(PlaybackState a, PlaybackState b)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 
e(String message)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 
d(String message)509     private void d(String message) {
510         if (DEBUG) Log.d(TAG, mPackageName + ": " + message);
511     }
512 
513     @VisibleForTesting
getTimeoutHandler()514     Handler getTimeoutHandler() {
515         if (mControllerCallbacks == null) return null;
516         return mControllerCallbacks.getTimeoutHandler();
517     }
518 
519     @Override
toString()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