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