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