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