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