• 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.NonNull;
20 import android.content.BroadcastReceiver;
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.content.pm.PackageManager;
26 import android.content.pm.ResolveInfo;
27 import android.media.session.MediaSession;
28 import android.media.session.MediaSessionManager;
29 import android.media.session.PlaybackState;
30 import android.os.Handler;
31 import android.os.Looper;
32 import android.util.Log;
33 import android.view.KeyEvent;
34 
35 import com.android.bluetooth.Utils;
36 
37 import java.util.ArrayList;
38 import java.util.Collections;
39 import java.util.HashMap;
40 import java.util.HashSet;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.regex.Matcher;
44 import java.util.regex.Pattern;
45 
46 /**
47  * This class is directly responsible of maintaining the list of Browsable Players as well as
48  * the list of Addressable Players. This variation of the list doesn't actually list all the
49  * available players for a getAvailableMediaPlayers request. Instead it only reports one media
50  * player with ID=0 and all the other browsable players are folders in the root of that player.
51  *
52  * Changing the directory to a browsable player will allow you to traverse that player as normal.
53  * By only having one root player, we never have to send Addressed Player Changed notifications,
54  * UIDs Changed notifications, or Available Players Changed notifications.
55  *
56  * TODO (apanicke): Add non-browsable players as song items to the root folder. Selecting that
57  * player would effectively cause player switch by sending a play command to that player.
58  */
59 public class MediaPlayerList {
60     private static final String TAG = "NewAvrcpMediaPlayerList";
61     private static final boolean DEBUG = true;
62     static boolean sTesting = false;
63 
64     private static final String PACKAGE_SCHEME = "package";
65     private static final int NO_ACTIVE_PLAYER = 0;
66     private static final int BLUETOOTH_PLAYER_ID = 0;
67     private static final String BLUETOOTH_PLAYER_NAME = "Bluetooth Player";
68 
69     // mediaId's for the now playing list will be in the form of "NowPlayingId[XX]" where [XX]
70     // is the Queue ID for the requested item.
71     private static final String NOW_PLAYING_ID_PATTERN = Util.NOW_PLAYING_PREFIX + "([0-9]*)";
72 
73     // mediaId's for folder browsing will be in the form of [XX][mediaid],  where [XX] is a
74     // two digit representation of the player id and [mediaid] is the original media id as a
75     // string.
76     private static final String BROWSE_ID_PATTERN = "\\d\\d.*";
77 
78     private Context mContext;
79     private Looper mLooper; // Thread all media player callbacks and timeouts happen on
80     private PackageManager mPackageManager;
81     private MediaSessionManager mMediaSessionManager;
82 
83     private Map<Integer, MediaPlayerWrapper> mMediaPlayers =
84             Collections.synchronizedMap(new HashMap<Integer, MediaPlayerWrapper>());
85     private Map<String, Integer> mMediaPlayerIds =
86             Collections.synchronizedMap(new HashMap<String, Integer>());
87     private Map<Integer, BrowsedPlayerWrapper> mBrowsablePlayers =
88             Collections.synchronizedMap(new HashMap<Integer, BrowsedPlayerWrapper>());
89     private int mActivePlayerId = NO_ACTIVE_PLAYER;
90 
91     private AvrcpTargetService.ListCallback mCallback;
92 
93     interface MediaUpdateCallback {
run(MediaData data)94         void run(MediaData data);
95     }
96 
97     interface GetPlayerRootCallback {
run(int playerId, boolean success, String rootId, int numItems)98         void run(int playerId, boolean success, String rootId, int numItems);
99     }
100 
101     interface GetFolderItemsCallback {
run(String parentId, List<ListItem> items)102         void run(String parentId, List<ListItem> items);
103     }
104 
105     interface FolderUpdateCallback {
run(boolean availablePlayers, boolean addressedPlayers, boolean uids)106         void run(boolean availablePlayers, boolean addressedPlayers, boolean uids);
107     }
108 
MediaPlayerList(Looper looper, Context context)109     MediaPlayerList(Looper looper, Context context) {
110         Log.v(TAG, "Creating MediaPlayerList");
111 
112         mLooper = looper;
113         mContext = context;
114 
115         // Register for intents where available players might have changed
116         IntentFilter pkgFilter = new IntentFilter();
117         pkgFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
118         pkgFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
119         pkgFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
120         pkgFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
121         pkgFilter.addDataScheme(PACKAGE_SCHEME);
122         context.registerReceiver(mPackageChangedBroadcastReceiver, pkgFilter);
123 
124         mMediaSessionManager =
125                 (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
126         mMediaSessionManager.addOnActiveSessionsChangedListener(
127                 mActiveSessionsChangedListener, null, new Handler(looper));
128         mMediaSessionManager.setCallback(mButtonDispatchCallback, null);
129     }
130 
init(AvrcpTargetService.ListCallback callback)131     void init(AvrcpTargetService.ListCallback callback) {
132         Log.v(TAG, "Initializing MediaPlayerList");
133         mCallback = callback;
134 
135         // Build the list of browsable players and afterwards, build the list of media players
136         Intent intent = new Intent(android.service.media.MediaBrowserService.SERVICE_INTERFACE);
137         List<ResolveInfo> playerList =
138                 mContext
139                     .getApplicationContext()
140                     .getPackageManager()
141                     .queryIntentServices(intent, PackageManager.MATCH_ALL);
142 
143         BrowsablePlayerConnector.connectToPlayers(mContext, mLooper, playerList,
144                 (List<BrowsedPlayerWrapper> players) -> {
145                 Log.i(TAG, "init: Browsable Player list size is " + players.size());
146 
147                 // Check to see if the list has been cleaned up before this completed
148                 if (mMediaSessionManager == null) {
149                     return;
150                 }
151 
152                 for (BrowsedPlayerWrapper wrapper : players) {
153                     // Generate new id and add the browsable player
154                     if (!mMediaPlayerIds.containsKey(wrapper.getPackageName())) {
155                         mMediaPlayerIds.put(wrapper.getPackageName(), mMediaPlayerIds.size() + 1);
156                     }
157 
158                     d("Adding Browser Wrapper for " + wrapper.getPackageName() + " with id "
159                             + mMediaPlayerIds.get(wrapper.getPackageName()));
160 
161                     mBrowsablePlayers.put(mMediaPlayerIds.get(wrapper.getPackageName()), wrapper);
162 
163                     wrapper.getFolderItems(wrapper.getRootId(),
164                             (int status, String mediaId, List<ListItem> results) -> {
165                                 d("Got the contents for: " + mediaId + " : num results="
166                                         + results.size());
167                             });
168                 }
169 
170                 // Construct the list of current players
171                 d("Initializing list of current media players");
172                 List<android.media.session.MediaController> controllers =
173                         mMediaSessionManager.getActiveSessions(null);
174 
175                 for (android.media.session.MediaController controller : controllers) {
176                     addMediaPlayer(controller);
177                 }
178 
179                 // If there were any active players and we don't already have one due to the Media
180                 // Framework Callbacks then set the highest priority one to active
181                 if (mActivePlayerId == 0 && mMediaPlayers.size() > 0) setActivePlayer(1);
182             });
183     }
184 
cleanup()185     void cleanup() {
186         mContext.unregisterReceiver(mPackageChangedBroadcastReceiver);
187 
188         mMediaSessionManager.removeOnActiveSessionsChangedListener(mActiveSessionsChangedListener);
189         mMediaSessionManager.setCallback(null, null);
190         mMediaSessionManager = null;
191 
192         mMediaPlayerIds.clear();
193 
194         for (MediaPlayerWrapper player : mMediaPlayers.values()) {
195             player.cleanup();
196         }
197         mMediaPlayers.clear();
198 
199         for (BrowsedPlayerWrapper player : mBrowsablePlayers.values()) {
200             player.disconnect();
201         }
202         mBrowsablePlayers.clear();
203     }
204 
getCurrentPlayerId()205     int getCurrentPlayerId() {
206         return BLUETOOTH_PLAYER_ID;
207     }
208 
getActivePlayer()209     MediaPlayerWrapper getActivePlayer() {
210         return mMediaPlayers.get(mActivePlayerId);
211     }
212 
213 
214 
215     // In this case the displayed player is the Bluetooth Player, the number of items is equal
216     // to the number of players. The root ID will always be empty string in this case as well.
getPlayerRoot(int playerId, GetPlayerRootCallback cb)217     void getPlayerRoot(int playerId, GetPlayerRootCallback cb) {
218         cb.run(playerId, playerId == BLUETOOTH_PLAYER_ID, "", mBrowsablePlayers.size());
219     }
220 
221     // Return the "Bluetooth Player" as the only player always
getMediaPlayerList()222     List<PlayerInfo> getMediaPlayerList() {
223         PlayerInfo info = new PlayerInfo();
224         info.id = BLUETOOTH_PLAYER_ID;
225         info.name = BLUETOOTH_PLAYER_NAME;
226         info.browsable = true;
227         List<PlayerInfo> ret = new ArrayList<PlayerInfo>();
228         ret.add(info);
229 
230         return ret;
231     }
232 
233     @NonNull
getCurrentMediaId()234     String getCurrentMediaId() {
235         final MediaPlayerWrapper player = getActivePlayer();
236         if (player == null) return "";
237 
238         final PlaybackState state = player.getPlaybackState();
239         final List<Metadata> queue = player.getCurrentQueue();
240 
241         // Disable the now playing list if the player doesn't have a queue or provide an active
242         // queue ID that can be used to determine the active song in the queue.
243         if (state == null
244                 || state.getActiveQueueItemId() == MediaSession.QueueItem.UNKNOWN_ID
245                 || queue.size() == 0) {
246             d("getCurrentMediaId: No active queue item Id sending empty mediaId: PlaybackState="
247                      + state);
248             return "";
249         }
250 
251         return Util.NOW_PLAYING_PREFIX + state.getActiveQueueItemId();
252     }
253 
254     @NonNull
getCurrentSongInfo()255     Metadata getCurrentSongInfo() {
256         final MediaPlayerWrapper player = getActivePlayer();
257         if (player == null) return Util.empty_data();
258 
259         return player.getCurrentMetadata();
260     }
261 
getCurrentPlayStatus()262     PlaybackState getCurrentPlayStatus() {
263         final MediaPlayerWrapper player = getActivePlayer();
264         if (player == null) return null;
265 
266         return player.getPlaybackState();
267     }
268 
269     @NonNull
getNowPlayingList()270     List<Metadata> getNowPlayingList() {
271         // Only send the current song for the now playing if there is no active song. See
272         // |getCurrentMediaId()| for reasons why there might be no active song.
273         if (getCurrentMediaId().equals("")) {
274             List<Metadata> ret = new ArrayList<Metadata>();
275             Metadata data = getCurrentSongInfo();
276             data.mediaId = "";
277             ret.add(data);
278             return ret;
279         }
280 
281         return getActivePlayer().getCurrentQueue();
282     }
283 
playItem(int playerId, boolean nowPlaying, String mediaId)284     void playItem(int playerId, boolean nowPlaying, String mediaId) {
285         if (nowPlaying) {
286             playNowPlayingItem(mediaId);
287         } else {
288             playFolderItem(mediaId);
289         }
290     }
291 
playNowPlayingItem(String mediaId)292     private void playNowPlayingItem(String mediaId) {
293         d("playNowPlayingItem: mediaId=" + mediaId);
294 
295         Pattern regex = Pattern.compile(NOW_PLAYING_ID_PATTERN);
296         Matcher m = regex.matcher(mediaId);
297         if (!m.find()) {
298             // This should never happen since we control the media ID's reported
299             Log.wtf(TAG, "playNowPlayingItem: Couldn't match mediaId to pattern: mediaId="
300                     + mediaId);
301         }
302 
303         long queueItemId = Long.parseLong(m.group(1));
304         if (getActivePlayer() != null) {
305             getActivePlayer().playItemFromQueue(queueItemId);
306         }
307     }
308 
playFolderItem(String mediaId)309     private void playFolderItem(String mediaId) {
310         d("playFolderItem: mediaId=" + mediaId);
311 
312         if (!mediaId.matches(BROWSE_ID_PATTERN)) {
313             // This should never happen since we control the media ID's reported
314             Log.wtf(TAG, "playFolderItem: mediaId didn't match pattern: mediaId=" + mediaId);
315         }
316 
317         int playerIndex = Integer.parseInt(mediaId.substring(0, 2));
318         String itemId = mediaId.substring(2);
319 
320         if (!mBrowsablePlayers.containsKey(playerIndex)) {
321             e("playFolderItem: Do not have the a browsable player with ID " + playerIndex);
322             return;
323         }
324 
325         mBrowsablePlayers.get(playerIndex).playItem(itemId);
326     }
327 
getFolderItemsMediaPlayerList(GetFolderItemsCallback cb)328     void getFolderItemsMediaPlayerList(GetFolderItemsCallback cb) {
329         d("getFolderItemsMediaPlayerList: Sending Media Player list for root directory");
330 
331         ArrayList<ListItem> playerList = new ArrayList<ListItem>();
332         for (BrowsedPlayerWrapper player : mBrowsablePlayers.values()) {
333 
334             String displayName = Util.getDisplayName(mContext, player.getPackageName());
335             int id = mMediaPlayerIds.get(player.getPackageName());
336 
337             d("getFolderItemsMediaPlayerList: Adding player " + displayName);
338             Folder playerFolder = new Folder(String.format("%02d", id), false, displayName);
339             playerList.add(new ListItem(playerFolder));
340         }
341         cb.run("", playerList);
342         return;
343     }
344 
getFolderItems(int playerId, String mediaId, GetFolderItemsCallback cb)345     void getFolderItems(int playerId, String mediaId, GetFolderItemsCallback cb) {
346         // The playerId is unused since we always assume the remote device is using the
347         // Bluetooth Player.
348         d("getFolderItems(): playerId=" + playerId + ", mediaId=" + mediaId);
349 
350         // The device is requesting the content of the root folder. This folder contains a list of
351         // Browsable Media Players displayed as folders with their contents contained within.
352         if (mediaId.equals("")) {
353             getFolderItemsMediaPlayerList(cb);
354             return;
355         }
356 
357         if (!mediaId.matches(BROWSE_ID_PATTERN)) {
358             // This should never happen since we control the media ID's reported
359             Log.wtf(TAG, "getFolderItems: mediaId didn't match pattern: mediaId=" + mediaId);
360         }
361 
362         int playerIndex = Integer.parseInt(mediaId.substring(0, 2));
363         String itemId = mediaId.substring(2);
364 
365         // TODO (apanicke): Add timeouts for looking up folder items since media browsers don't
366         // have to respond.
367         if (mBrowsablePlayers.containsKey(playerIndex)) {
368             BrowsedPlayerWrapper wrapper = mBrowsablePlayers.get(playerIndex);
369             if (itemId.equals("")) {
370                 Log.i(TAG, "Empty media id, getting the root for "
371                         + wrapper.getPackageName());
372                 itemId = wrapper.getRootId();
373             }
374 
375             wrapper.getFolderItems(itemId, (status, id, results) -> {
376                 if (status != BrowsedPlayerWrapper.STATUS_SUCCESS) {
377                     cb.run(mediaId, new ArrayList<ListItem>());
378                     return;
379                 }
380 
381                 String playerPrefix = String.format("%02d", playerIndex);
382                 for (ListItem item : results) {
383                     if (item.isFolder) {
384                         item.folder.mediaId = playerPrefix.concat(item.folder.mediaId);
385                     } else {
386                         item.song.mediaId = playerPrefix.concat(item.song.mediaId);
387                     }
388                 }
389                 cb.run(mediaId, results);
390             });
391             return;
392         } else {
393             cb.run(mediaId, new ArrayList<ListItem>());
394         }
395     }
396 
397     // Adds the controller to the MediaPlayerList or updates the controller if we already had
398     // a controller for a package. Returns the new ID of the controller where its added or its
399     // previous value if it already existed. Returns -1 if the controller passed in is invalid
addMediaPlayer(android.media.session.MediaController controller)400     int addMediaPlayer(android.media.session.MediaController controller) {
401         if (controller == null) return -1;
402 
403         // Each new player has an ID of 1 plus the highest ID. The ID 0 is reserved to signify that
404         // there is no active player. If we already have a browsable player for the package, reuse
405         // that key.
406         String packageName = controller.getPackageName();
407         if (!mMediaPlayerIds.containsKey(packageName)) {
408             mMediaPlayerIds.put(packageName, mMediaPlayerIds.size() + 1);
409         }
410 
411         int playerId = mMediaPlayerIds.get(packageName);
412 
413         // If we already have a controller for the package, then update it with this new controller
414         // as the old controller has probably gone stale.
415         if (mMediaPlayers.containsKey(playerId)) {
416             d("Already have a controller for the player: " + packageName + ", updating instead");
417             MediaPlayerWrapper player = mMediaPlayers.get(playerId);
418             player.updateMediaController(MediaControllerFactory.wrap(controller));
419 
420             // If the media controller we updated was the active player check if the media updated
421             if (playerId == mActivePlayerId) {
422                 sendMediaUpdate(getActivePlayer().getCurrentMediaData());
423             }
424 
425             return playerId;
426         }
427 
428         MediaPlayerWrapper newPlayer = MediaPlayerWrapper.wrap(
429                 MediaControllerFactory.wrap(controller),
430                 mLooper);
431 
432         Log.i(TAG, "Adding wrapped media player: " + packageName + " at key: "
433                 + mMediaPlayerIds.get(controller.getPackageName()));
434 
435         mMediaPlayers.put(playerId, newPlayer);
436         return playerId;
437     }
438 
removeMediaPlayer(int playerId)439     void removeMediaPlayer(int playerId) {
440         if (!mMediaPlayers.containsKey(playerId)) {
441             e("Trying to remove nonexistent media player: " + playerId);
442             return;
443         }
444 
445         // If we removed the active player, set no player as active until the Media Framework
446         // tells us otherwise
447         if (playerId == mActivePlayerId && playerId != NO_ACTIVE_PLAYER) {
448             getActivePlayer().unregisterCallback();
449             mActivePlayerId = NO_ACTIVE_PLAYER;
450         }
451 
452         final MediaPlayerWrapper wrapper = mMediaPlayers.get(playerId);
453         d("Removing media player " + wrapper.getPackageName());
454         mMediaPlayerIds.remove(wrapper.getPackageName());
455         mMediaPlayers.remove(playerId);
456         wrapper.cleanup();
457     }
458 
setActivePlayer(int playerId)459     void setActivePlayer(int playerId) {
460         if (!mMediaPlayers.containsKey(playerId)) {
461             e("Player doesn't exist in list(): " + playerId);
462             return;
463         }
464 
465         if (playerId == mActivePlayerId) {
466             Log.w(TAG, getActivePlayer().getPackageName() + " is already the active player");
467             return;
468         }
469 
470         if (mActivePlayerId != NO_ACTIVE_PLAYER) getActivePlayer().unregisterCallback();
471 
472         mActivePlayerId = playerId;
473         getActivePlayer().registerCallback(mMediaPlayerCallback);
474         Log.i(TAG, "setActivePlayer(): setting player to " + getActivePlayer().getPackageName());
475 
476         // Ensure that metadata is synced on the new player
477         if (!getActivePlayer().isMetadataSynced()) {
478             Log.w(TAG, "setActivePlayer(): Metadata not synced on new player");
479             return;
480         }
481 
482         if (Utils.isPtsTestMode()) {
483             sendFolderUpdate(true, true, false);
484         }
485 
486         sendMediaUpdate(getActivePlayer().getCurrentMediaData());
487     }
488 
489     // TODO (apanicke): Add logging for media key events in dumpsys
sendMediaKeyEvent(int key, boolean pushed)490     void sendMediaKeyEvent(int key, boolean pushed) {
491         d("sendMediaKeyEvent: key=" + key + " pushed=" + pushed);
492         int action = pushed ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP;
493         KeyEvent event = new KeyEvent(action, AvrcpPassthrough.toKeyCode(key));
494         mMediaSessionManager.dispatchMediaKeyEvent(event);
495     }
496 
sendFolderUpdate(boolean availablePlayers, boolean addressedPlayers, boolean uids)497     private void sendFolderUpdate(boolean availablePlayers, boolean addressedPlayers,
498             boolean uids) {
499         d("sendFolderUpdate");
500         if (mCallback == null) {
501             return;
502         }
503 
504         mCallback.run(availablePlayers, addressedPlayers, uids);
505     }
506 
sendMediaUpdate(MediaData data)507     private void sendMediaUpdate(MediaData data) {
508         d("sendMediaUpdate");
509         if (mCallback == null) {
510             return;
511         }
512 
513         // Always have items in the queue
514         if (data.queue.size() == 0) {
515             Log.i(TAG, "sendMediaUpdate: Creating a one item queue for a player with no queue");
516             data.queue.add(data.metadata);
517         }
518 
519         mCallback.run(data);
520     }
521 
522     private final MediaSessionManager.OnActiveSessionsChangedListener
523             mActiveSessionsChangedListener =
524             new MediaSessionManager.OnActiveSessionsChangedListener() {
525         @Override
526         public void onActiveSessionsChanged(
527                 List<android.media.session.MediaController> newControllers) {
528             synchronized (MediaPlayerList.this) {
529                 Log.v(TAG, "onActiveSessionsChanged: number of controllers: "
530                         + newControllers.size());
531                 if (newControllers.size() == 0) return;
532 
533                 // Apps are allowed to have multiple MediaControllers. If an app does have
534                 // multiple controllers then newControllers contains them in highest
535                 // priority order. Since we only want to keep the highest priority one,
536                 // we keep track of which controllers we updated and skip over ones
537                 // we've already looked at.
538                 HashSet<String> addedPackages = new HashSet<String>();
539 
540                 for (int i = 0; i < newControllers.size(); i++) {
541                     Log.d(TAG, "onActiveSessionsChanged: controller: "
542                             + newControllers.get(i).getPackageName());
543                     if (addedPackages.contains(newControllers.get(i).getPackageName())) {
544                         continue;
545                     }
546 
547                     addedPackages.add(newControllers.get(i).getPackageName());
548                     addMediaPlayer(newControllers.get(i));
549                 }
550             }
551         }
552     };
553 
554     // TODO (apanicke): Write a test that tests uninstalling the active session
555     private final BroadcastReceiver mPackageChangedBroadcastReceiver = new BroadcastReceiver() {
556         @Override
557         public void onReceive(Context context, Intent intent) {
558             String action = intent.getAction();
559             Log.v(TAG, "mPackageChangedBroadcastReceiver: action: " + action);
560 
561             if (action.equals(Intent.ACTION_PACKAGE_REMOVED)
562                     || action.equals(Intent.ACTION_PACKAGE_DATA_CLEARED)) {
563                 if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) return;
564 
565                 String packageName = intent.getData().getSchemeSpecificPart();
566                 if (packageName != null && mMediaPlayerIds.containsKey(packageName)) {
567                     removeMediaPlayer(mMediaPlayerIds.get(packageName));
568                 }
569             } else if (action.equals(Intent.ACTION_PACKAGE_ADDED)
570                     || action.equals(Intent.ACTION_PACKAGE_CHANGED)) {
571                 String packageName = intent.getData().getSchemeSpecificPart();
572                 if (packageName != null) {
573                     if (DEBUG) Log.d(TAG, "Name of package changed: " + packageName);
574                     // TODO (apanicke): Handle either updating or adding the new package.
575                     // Check if its browsable and send the UIDS changed to update the
576                     // root folder
577                 }
578             }
579         }
580     };
581 
582     private final MediaPlayerWrapper.Callback mMediaPlayerCallback =
583             new MediaPlayerWrapper.Callback() {
584         @Override
585         public void mediaUpdatedCallback(MediaData data) {
586             if (data.metadata == null) {
587                 Log.d(TAG, "mediaUpdatedCallback(): metadata is null");
588                 return;
589             }
590 
591             if (data.state == null) {
592                 Log.w(TAG, "mediaUpdatedCallback(): Tried to update with null state");
593                 return;
594             }
595 
596             sendMediaUpdate(data);
597         }
598     };
599 
600     private final MediaSessionManager.Callback mButtonDispatchCallback =
601             new MediaSessionManager.Callback() {
602                 @Override
603                 public void onMediaKeyEventDispatched(KeyEvent event, MediaSession.Token token) {
604                     // TODO (apanicke): Add logging for these
605                 }
606 
607                 @Override
608                 public void onMediaKeyEventDispatched(KeyEvent event, ComponentName receiver) {
609                     // TODO (apanicke): Add logging for these
610                 }
611 
612                 @Override
613                 public void onAddressedPlayerChanged(MediaSession.Token token) {
614                     android.media.session.MediaController controller =
615                             new android.media.session.MediaController(mContext, token);
616 
617                     if (!mMediaPlayerIds.containsKey(controller.getPackageName())) {
618                         // Since we have a controller, we can try to to recover by adding the
619                         // player and then setting it as active.
620                         Log.w(TAG, "onAddressedPlayerChanged(Token): Addressed Player "
621                                 + "changed to a player we didn't have a session for");
622                         addMediaPlayer(controller);
623                     }
624 
625                     Log.i(TAG, "onAddressedPlayerChanged: token=" + controller.getPackageName());
626                     setActivePlayer(mMediaPlayerIds.get(controller.getPackageName()));
627                 }
628 
629                 @Override
630                 public void onAddressedPlayerChanged(ComponentName receiver) {
631                     if (receiver == null) {
632                         return;
633                     }
634 
635                     if (!mMediaPlayerIds.containsKey(receiver.getPackageName())) {
636                         e("onAddressedPlayerChanged(Component): Addressed Player "
637                                 + "changed to a player we don't have a session for");
638                         return;
639                     }
640 
641                     Log.i(TAG, "onAddressedPlayerChanged: component=" + receiver.getPackageName());
642                     setActivePlayer(mMediaPlayerIds.get(receiver.getPackageName()));
643                 }
644             };
645 
646 
dump(StringBuilder sb)647     void dump(StringBuilder sb) {
648         sb.append("List of MediaControllers: size=" + mMediaPlayers.size() + "\n");
649         for (int id : mMediaPlayers.keySet()) {
650             if (id == mActivePlayerId) {
651                 sb.append("<Active> ");
652             }
653             MediaPlayerWrapper player = mMediaPlayers.get(id);
654             sb.append("  Media Player " + id + ": " + player.getPackageName() + "\n");
655             sb.append(player.toString().replaceAll("(?m)^", "  "));
656             sb.append("\n");
657         }
658 
659         sb.append("List of Browsers: size=" + mBrowsablePlayers.size() + "\n");
660         for (BrowsedPlayerWrapper player : mBrowsablePlayers.values()) {
661             sb.append(player.toString().replaceAll("(?m)^", "  "));
662             sb.append("\n");
663         }
664         // TODO (apanicke): Add media key events
665         // TODO (apanicke): Add last sent data
666         // TODO (apanicke): Add addressed player history
667     }
668 
e(String message)669     private static void e(String message) {
670         if (sTesting) {
671             Log.wtfStack(TAG, message);
672         } else {
673             Log.e(TAG, message);
674         }
675     }
676 
d(String message)677     private static void d(String message) {
678         if (DEBUG) {
679             Log.d(TAG, message);
680         }
681     }
682 }
683