• 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.NonNull;
20 import android.content.BroadcastReceiver;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.IntentFilter;
24 import android.content.pm.PackageManager;
25 import android.content.pm.ResolveInfo;
26 import android.media.AudioAttributes;
27 import android.media.AudioManager;
28 import android.media.AudioPlaybackConfiguration;
29 import android.media.session.MediaSession;
30 import android.media.session.MediaSessionManager;
31 import android.media.session.PlaybackState;
32 import android.os.Handler;
33 import android.os.Looper;
34 import android.os.SystemProperties;
35 import android.text.TextUtils;
36 import android.util.Log;
37 import android.view.KeyEvent;
38 
39 import com.android.bluetooth.BluetoothEventLogger;
40 import com.android.bluetooth.Utils;
41 import com.android.bluetooth.avrcp.AvrcpPassthrough;
42 import com.android.bluetooth.flags.Flags;
43 import com.android.internal.annotations.VisibleForTesting;
44 
45 import java.util.ArrayList;
46 import java.util.Collections;
47 import java.util.HashMap;
48 import java.util.HashSet;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.regex.Matcher;
52 import java.util.regex.Pattern;
53 
54 /**
55  * This class is directly responsible of maintaining the list of Browsable Players as well as the
56  * list of Addressable Players. This variation of the list doesn't actually list all the available
57  * players for a getAvailableMediaPlayers request. Instead it only reports one media player with
58  * ID=0 and all the other browsable players are folders in the root of that player.
59  *
60  * <p>Changing the directory to a browsable player will allow you to traverse that player as normal.
61  * By only having one root player, we never have to send Addressed Player Changed notifications,
62  * UIDs Changed notifications, or Available Players Changed notifications.
63  *
64  * <p>TODO (apanicke): Add non-browsable players as song items to the root folder. Selecting that
65  * player would effectively cause player switch by sending a play command to that player.
66  */
67 public class MediaPlayerList {
68     private static final String TAG = MediaPlayerList.class.getSimpleName();
69     static boolean sTesting = false;
70 
71     private static final String PACKAGE_SCHEME = "package";
72     private static final int NO_ACTIVE_PLAYER = 0;
73     private static final int BLUETOOTH_PLAYER_ID = 0;
74     private static final String BLUETOOTH_PLAYER_NAME = "Bluetooth Player";
75     private static final int ACTIVE_PLAYER_LOGGER_SIZE = 5;
76     private static final String ACTIVE_PLAYER_LOGGER_TITLE = "BTAudio Active Player Events";
77     private static final int AUDIO_PLAYBACK_STATE_LOGGER_SIZE = 15;
78     private static final String AUDIO_PLAYBACK_STATE_LOGGER_TITLE =
79             "BTAudio Audio Playback State Events";
80 
81     // mediaId's for the now playing list will be in the form of "NowPlayingId[XX]" where [XX]
82     // is the Queue ID for the requested item.
83     private static final String NOW_PLAYING_ID_PATTERN = Util.NOW_PLAYING_PREFIX + "([0-9]*)";
84 
85     // mediaId's for folder browsing will be in the form of [XX][mediaid],  where [XX] is a
86     // two digit representation of the player id and [mediaid] is the original media id as a
87     // string.
88     private static final String BROWSE_ID_PATTERN = "\\d\\d.*";
89 
90     private Context mContext;
91     private Looper mLooper; // Thread all media player callbacks and timeouts happen on
92     private MediaSessionManager mMediaSessionManager;
93     private MediaData mCurrMediaData = null;
94     private final AudioManager mAudioManager;
95 
96     private final BluetoothEventLogger mActivePlayerLogger =
97             new BluetoothEventLogger(ACTIVE_PLAYER_LOGGER_SIZE, ACTIVE_PLAYER_LOGGER_TITLE);
98     private final BluetoothEventLogger mAudioPlaybackStateLogger =
99             new BluetoothEventLogger(
100                     AUDIO_PLAYBACK_STATE_LOGGER_SIZE, AUDIO_PLAYBACK_STATE_LOGGER_TITLE);
101 
102     private Map<Integer, MediaPlayerWrapper> mMediaPlayers =
103             Collections.synchronizedMap(new HashMap<Integer, MediaPlayerWrapper>());
104     private Map<String, Integer> mMediaPlayerIds =
105             Collections.synchronizedMap(new HashMap<String, Integer>());
106     private Map<Integer, BrowsedPlayerWrapper> mBrowsablePlayers =
107             Collections.synchronizedMap(new HashMap<Integer, BrowsedPlayerWrapper>());
108     private int mActivePlayerId = NO_ACTIVE_PLAYER;
109 
110     private MediaUpdateCallback mCallback;
111     private boolean mAudioPlaybackIsActive = false;
112 
113     private BrowsablePlayerConnector mBrowsablePlayerConnector;
114 
115     private MediaPlayerSettingsEventListener mPlayerSettingsListener;
116 
117     public interface MediaUpdateCallback {
run(MediaData data)118         void run(MediaData data);
119 
run(boolean availablePlayers, boolean addressedPlayers, boolean uids)120         void run(boolean availablePlayers, boolean addressedPlayers, boolean uids);
121     }
122 
123     public interface GetPlayerRootCallback {
run(int playerId, boolean success, String rootId, int numItems)124         void run(int playerId, boolean success, String rootId, int numItems);
125     }
126 
127     public interface GetFolderItemsCallback {
run(String parentId, List<ListItem> items)128         void run(String parentId, List<ListItem> items);
129     }
130 
131     /** Listener for PlayerSettingsManager. */
132     public interface MediaPlayerSettingsEventListener {
133         /** Called when the active player has changed. */
onActivePlayerChanged(MediaPlayerWrapper player)134         void onActivePlayerChanged(MediaPlayerWrapper player);
135     }
136 
MediaPlayerList(Looper looper, Context context)137     public MediaPlayerList(Looper looper, Context context) {
138         Log.v(TAG, "Creating MediaPlayerList");
139 
140         mLooper = looper;
141         mContext = context;
142 
143         // Register for intents where available players might have changed
144         IntentFilter pkgFilter = new IntentFilter();
145         pkgFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
146         pkgFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
147         pkgFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
148         pkgFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
149         pkgFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
150         pkgFilter.addDataScheme(PACKAGE_SCHEME);
151         context.registerReceiver(mPackageChangedBroadcastReceiver, pkgFilter);
152 
153         mAudioManager = context.getSystemService(AudioManager.class);
154         mAudioManager.registerAudioPlaybackCallback(mAudioPlaybackCallback, new Handler(mLooper));
155 
156         mMediaSessionManager = context.getSystemService(MediaSessionManager.class);
157         mMediaSessionManager.addOnActiveSessionsChangedListener(
158                 mActiveSessionsChangedListener, null, new Handler(looper));
159         mMediaSessionManager.addOnMediaKeyEventSessionChangedListener(
160                 mContext.getMainExecutor(), mMediaKeyEventSessionChangedListener);
161     }
162 
163     /**
164      * Retrieves the list of active {@link android.media.session.MediaController}, convert them to
165      * local {@link MediaController}, converts them again to {@link MediaPlayerWrapper} and add them
166      * to the local list ({@link #mMediaPlayers}).
167      *
168      * <p>If we already received an onActiveSessionsChanged callback, set this player as active,
169      * otherwise set the highest priority one as active (first in the list).
170      */
constructCurrentPlayers()171     private void constructCurrentPlayers() {
172         // Construct the list of current players
173         d("Initializing list of current media players");
174         List<android.media.session.MediaController> controllers =
175                 mMediaSessionManager.getActiveSessions(null);
176 
177         for (android.media.session.MediaController controller : controllers) {
178             if ((controller.getFlags() & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0) {
179                 // GLOBAL_PRIORITY session is created by Telecom to handle call control key events
180                 // but Bluetooth Headset profile handles the key events for calls so we don't have
181                 // to handle these sessions in AVRCP.
182                 continue;
183             }
184             addMediaPlayer(controller);
185         }
186 
187         // If there were any active players and we don't already have one due to the Media
188         // Framework Callbacks then set the highest priority one to active
189         if (mActivePlayerId == 0 && mMediaPlayers.size() > 0) {
190             String packageName = mMediaSessionManager.getMediaKeyEventSessionPackageName();
191             if (!TextUtils.isEmpty(packageName) && haveMediaPlayer(packageName)) {
192                 Log.i(TAG, "Set active player to MediaKeyEvent session = " + packageName);
193                 setActivePlayer(mMediaPlayerIds.get(packageName));
194             } else {
195                 Log.i(TAG, "Set active player to first default");
196                 setActivePlayer(1);
197             }
198         }
199     }
200 
201     /** Initiates browsable players and calls {@link #constructCurrentPlayers}. */
init(MediaUpdateCallback callback)202     public void init(MediaUpdateCallback callback) {
203         Log.v(TAG, "Initializing MediaPlayerList");
204         mCallback = callback;
205 
206         if (!SystemProperties.getBoolean("bluetooth.avrcp.browsable_media_player.enabled", true)) {
207             // Allow to disable BrowsablePlayerConnector with systemproperties.
208             // This is useful when for watches because:
209             //   1. It is not a regular use case
210             //   2. Registering to all players is a very loading task
211 
212             Log.i(TAG, "init: without Browsable Player");
213             constructCurrentPlayers();
214             return;
215         }
216 
217         // Build the list of browsable players and afterwards, build the list of media players
218         Intent intent = new Intent(android.service.media.MediaBrowserService.SERVICE_INTERFACE);
219         if (Flags.keepStoppedMediaBrowserService()) {
220             // Don't query stopped apps, that would end up unstopping them
221             intent.addFlags(Intent.FLAG_EXCLUDE_STOPPED_PACKAGES);
222         }
223         List<ResolveInfo> playerList =
224                 mContext.getApplicationContext()
225                         .getPackageManager()
226                         .queryIntentServices(intent, PackageManager.MATCH_ALL);
227 
228         mBrowsablePlayerConnector =
229                 BrowsablePlayerConnector.connectToPlayers(
230                         mContext,
231                         mLooper,
232                         playerList,
233                         (List<BrowsedPlayerWrapper> players) -> {
234                             Log.i(TAG, "init: Browsable Player list size is " + players.size());
235 
236                             // Check to see if the list has been cleaned up before this completed
237                             if (mMediaSessionManager == null) {
238                                 return;
239                             }
240 
241                             for (BrowsedPlayerWrapper wrapper : players) {
242                                 // Generate new id and add the browsable player
243                                 if (!havePlayerId(wrapper.getPackageName())) {
244                                     mMediaPlayerIds.put(
245                                             wrapper.getPackageName(), getFreeMediaPlayerId());
246                                 }
247 
248                                 d(
249                                         "Adding Browser Wrapper for "
250                                                 + wrapper.getPackageName()
251                                                 + " with id "
252                                                 + mMediaPlayerIds.get(wrapper.getPackageName()));
253 
254                                 mBrowsablePlayers.put(
255                                         mMediaPlayerIds.get(wrapper.getPackageName()), wrapper);
256 
257                                 wrapper.getFolderItems(
258                                         wrapper.getRootId(),
259                                         (int status, String mediaId, List<ListItem> results) -> {
260                                             d(
261                                                     "Got the contents for: "
262                                                             + mediaId
263                                                             + " : num results="
264                                                             + results.size());
265                                         });
266                             }
267 
268                             constructCurrentPlayers();
269                         });
270     }
271 
cleanup()272     public void cleanup() {
273         mCallback = null;
274         mContext.unregisterReceiver(mPackageChangedBroadcastReceiver);
275 
276         mActivePlayerId = NO_ACTIVE_PLAYER;
277 
278         mMediaSessionManager.removeOnActiveSessionsChangedListener(mActiveSessionsChangedListener);
279         mMediaSessionManager.removeOnMediaKeyEventSessionChangedListener(
280                 mMediaKeyEventSessionChangedListener);
281         mMediaSessionManager = null;
282 
283         mAudioManager.unregisterAudioPlaybackCallback(mAudioPlaybackCallback);
284 
285         mMediaPlayerIds.clear();
286 
287         for (MediaPlayerWrapper player : mMediaPlayers.values()) {
288             player.cleanup();
289         }
290         mMediaPlayers.clear();
291 
292         if (mBrowsablePlayerConnector != null) {
293             mBrowsablePlayerConnector.cleanup();
294         }
295         for (BrowsedPlayerWrapper player : mBrowsablePlayers.values()) {
296             player.disconnect();
297         }
298         mBrowsablePlayers.clear();
299     }
300 
301     /**
302      * Current player ID is always Bluetooth player ID.
303      *
304      * <p>All browsable players are subdirectories of Bluetooth player.
305      */
getCurrentPlayerId()306     public int getCurrentPlayerId() {
307         return BLUETOOTH_PLAYER_ID;
308     }
309 
310     /** Get the next ID available in the IDs map. */
getFreeMediaPlayerId()311     int getFreeMediaPlayerId() {
312         int id = 1;
313         while (mMediaPlayerIds.containsValue(id)) {
314             id++;
315         }
316         return id;
317     }
318 
319     /** Returns the {@link #MediaPlayerWrapper} with ID matching {@link #mActivePlayerId}. */
getActivePlayer()320     public MediaPlayerWrapper getActivePlayer() {
321         return mMediaPlayers.get(mActivePlayerId);
322     }
323 
324     /** This is used to send passthrough command to media session */
sendMediaKeyEvent(int key, boolean pushed)325     public void sendMediaKeyEvent(int key, boolean pushed) {
326         if (mMediaSessionManager == null) {
327             Log.d(TAG, "Bluetooth is turning off, ignore it");
328             return;
329         }
330         int action = pushed ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP;
331         KeyEvent event = new KeyEvent(action, AvrcpPassthrough.toKeyCode(key));
332         mMediaSessionManager.dispatchMediaKeyEvent(event, false);
333     }
334 
335     /**
336      * This is used by setBrowsedPlayer as the browsed player is always the Bluetooth player.
337      *
338      * <p>If the requested player ID is not {@link #BLUETOOTH_PLAYER_ID}, success will be false.
339      *
340      * <p>The number of items will be the number of browsable players as they all are direct
341      * subdirectories of the Bluetooth player ID.
342      *
343      * <p>The root ID will always be an empty string to correspond to bluetooth player ID.
344      */
getPlayerRoot(int playerId, GetPlayerRootCallback cb)345     public void getPlayerRoot(int playerId, GetPlayerRootCallback cb) {
346         /** M: Fix PTS AVRCP/TG/MCN/CB/BI-02-C fail @{ */
347         if (Utils.isPtsTestMode()) {
348             d("PTS test mode: getPlayerRoot");
349             BrowsedPlayerWrapper wrapper = mBrowsablePlayers.get(BLUETOOTH_PLAYER_ID + 1);
350             String itemId = wrapper.getRootId();
351 
352             wrapper.getFolderItems(
353                     itemId,
354                     (status, id, results) -> {
355                         if (status != BrowsedPlayerWrapper.STATUS_SUCCESS) {
356                             cb.run(playerId, playerId == BLUETOOTH_PLAYER_ID, "", 0);
357                             return;
358                         }
359                         cb.run(playerId, playerId == BLUETOOTH_PLAYER_ID, "", results.size());
360                     });
361             return;
362         }
363         /**
364          * @}
365          */
366         cb.run(playerId, playerId == BLUETOOTH_PLAYER_ID, "", mBrowsablePlayers.size());
367     }
368 
369     /**
370      * Returns a list containing only the Bluetooth player.
371      *
372      * <p>See class documentation.
373      */
getMediaPlayerList()374     public List<PlayerInfo> getMediaPlayerList() {
375         PlayerInfo info = new PlayerInfo();
376         info.id = BLUETOOTH_PLAYER_ID;
377         info.name = BLUETOOTH_PLAYER_NAME;
378         info.browsable = true;
379         if (mBrowsablePlayers.size() == 0) {
380             // Set Bluetooth Player as non-browable if there is not browsable player exist.
381             info.browsable = false;
382         }
383         List<PlayerInfo> ret = new ArrayList<PlayerInfo>();
384         ret.add(info);
385 
386         return ret;
387     }
388 
389     /**
390      * Returns the active queue item id of the active player.
391      *
392      * <p>If the player's queue is empty, if the active queue item id is unknown or if the {@link
393      * android.media.session.PlaybackState} is null, returns an empty string.
394      */
395     @NonNull
getCurrentMediaId()396     public String getCurrentMediaId() {
397         final MediaPlayerWrapper player = getActivePlayer();
398         if (player == null) return "";
399 
400         final PlaybackState state = player.getPlaybackState();
401         final List<Metadata> queue = player.getCurrentQueue();
402 
403         // Disable the now playing list if the player doesn't have a queue or provide an active
404         // queue ID that can be used to determine the active song in the queue.
405         if (state == null
406                 || state.getActiveQueueItemId() == MediaSession.QueueItem.UNKNOWN_ID
407                 || queue.size() == 0) {
408             d(
409                     "getCurrentMediaId: No active queue item Id sending empty mediaId:"
410                             + " PlaybackState="
411                             + state);
412             return "";
413         }
414 
415         return Util.NOW_PLAYING_PREFIX + state.getActiveQueueItemId();
416     }
417 
418     /**
419      * Returns the active {@link android.media.session.MediaController}'s metadata, converted to
420      * {@link Metadata}.
421      */
422     @NonNull
getCurrentSongInfo()423     public Metadata getCurrentSongInfo() {
424         final MediaPlayerWrapper player = getActivePlayer();
425         if (player == null) return Util.empty_data();
426 
427         return player.getCurrentMetadata();
428     }
429 
430     /**
431      * Returns the current playing state of the active player.
432      *
433      * <p>If {@link #mAudioPlaybackIsActive} is true and the returned state is different from {@link
434      * android.media.session.PlaybackState.STATE_PLAYING}, returns a copy of the state with playing
435      * state {@link android.media.session.PlaybackState.STATE_PLAYING}.
436      */
getCurrentPlayStatus()437     public PlaybackState getCurrentPlayStatus() {
438         final MediaPlayerWrapper player = getActivePlayer();
439         if (player == null) return null;
440 
441         PlaybackState state = player.getPlaybackState();
442         if (mAudioPlaybackIsActive
443                 && (state == null || state.getState() != PlaybackState.STATE_PLAYING)) {
444             return new PlaybackState.Builder()
445                     .setState(
446                             PlaybackState.STATE_PLAYING,
447                             state == null ? 0 : state.getPosition(),
448                             1.0f)
449                     .build();
450         }
451         return state;
452     }
453 
454     /**
455      * Returns the current queue of the active player.
456      *
457      * <p>If there is no queue, returns a list containing only the active player's Metadata.
458      *
459      * <p>See {@link #getCurrentSongInfo} and {@link #getCurrentMediaId}.
460      */
461     @NonNull
getNowPlayingList()462     public List<Metadata> getNowPlayingList() {
463         // Only send the current song for the now playing if there is no active song. See
464         // |getCurrentMediaId()| for reasons why there might be no active song.
465         if (getCurrentMediaId().equals("")) {
466             List<Metadata> ret = new ArrayList<Metadata>();
467             Metadata data = getCurrentSongInfo();
468             data.mediaId = "";
469             ret.add(data);
470             return ret;
471         }
472 
473         return getActivePlayer().getCurrentQueue();
474     }
475 
476     /**
477      * Informs Media that there is a new request to play {@code mediaId}.
478      *
479      * <p>If the {@code nowPlaying} parameter is true, this will try to select the item from the
480      * current active player's queue. Otherwise this means that the item is from a browsable player
481      * and this calls {@link BrowsedPlayerWrapper} to handle the change.
482      */
playItem(int playerId, boolean nowPlaying, String mediaId)483     public void playItem(int playerId, boolean nowPlaying, String mediaId) {
484         if (nowPlaying) {
485             playNowPlayingItem(mediaId);
486         } else {
487             playFolderItem(mediaId);
488         }
489     }
490 
491     /**
492      * Retrieves the active player and plays item from queue.
493      *
494      * <p>See {@link #playItem}.
495      */
playNowPlayingItem(String mediaId)496     private void playNowPlayingItem(String mediaId) {
497         d("playNowPlayingItem: mediaId=" + mediaId);
498 
499         Pattern regex = Pattern.compile(NOW_PLAYING_ID_PATTERN);
500         Matcher m = regex.matcher(mediaId);
501         if (!m.find()) {
502             // This should never happen since we control the media ID's reported
503             Log.wtf(
504                     TAG,
505                     "playNowPlayingItem: Couldn't match mediaId to pattern: mediaId=" + mediaId);
506         }
507 
508         long queueItemId = Long.parseLong(m.group(1));
509         if (getActivePlayer() != null) {
510             getActivePlayer().playItemFromQueue(queueItemId);
511         }
512     }
513 
514     /**
515      * Retrieves the {@link BrowsedPlayerWrapper} corresponding to the {@code mediaId} and plays it.
516      *
517      * <p>See {@link #playItem}.
518      */
playFolderItem(String mediaId)519     private void playFolderItem(String mediaId) {
520         d("playFolderItem: mediaId=" + mediaId);
521 
522         if (!mediaId.matches(BROWSE_ID_PATTERN)) {
523             // This should never happen since we control the media ID's reported
524             Log.wtf(TAG, "playFolderItem: mediaId didn't match pattern: mediaId=" + mediaId);
525         }
526 
527         int playerIndex = Integer.parseInt(mediaId.substring(0, 2));
528         if (!haveMediaBrowser(playerIndex)) {
529             e("playFolderItem: Do not have the a browsable player with ID " + playerIndex);
530             return;
531         }
532 
533         BrowsedPlayerWrapper wrapper = mBrowsablePlayers.get(playerIndex);
534         String itemId = mediaId.substring(2);
535         if (TextUtils.isEmpty(itemId)) {
536             itemId = wrapper.getRootId();
537             if (TextUtils.isEmpty(itemId)) {
538                 e("playFolderItem: Failed to start playback with an empty media id.");
539                 return;
540             }
541             Log.i(
542                     TAG,
543                     "playFolderItem: Empty media id, trying with the root id for "
544                             + wrapper.getPackageName());
545         }
546         wrapper.playItem(itemId);
547     }
548 
549     /**
550      * Calls {@code cb} with the list of browsable players as folder items.
551      *
552      * <p>As browsable players are subdirectories of the root Bluetooth player, the list always
553      * contains all the browsable players.
554      *
555      * <p>See {@link #getFolderItems}.
556      */
getFolderItemsMediaPlayerList(GetFolderItemsCallback cb)557     void getFolderItemsMediaPlayerList(GetFolderItemsCallback cb) {
558         d("getFolderItemsMediaPlayerList: Sending Media Player list for root directory");
559 
560         ArrayList<ListItem> playerList = new ArrayList<ListItem>();
561         for (BrowsedPlayerWrapper player : mBrowsablePlayers.values()) {
562 
563             String displayName = Util.getDisplayName(mContext, player.getPackageName());
564             int id = mMediaPlayerIds.get(player.getPackageName());
565 
566             d("getFolderItemsMediaPlayerList: Adding player " + displayName);
567             Folder playerFolder = new Folder(String.format("%02d", id), false, displayName);
568             playerList.add(new ListItem(playerFolder));
569         }
570         cb.run("", playerList);
571     }
572 
573     /**
574      * Calls {@code cb} with a list of browsable folders.
575      *
576      * <p>If {@code mediaId} is empty, {@code cb} will be called with all the browsable players as
577      * they are subdirectories of the root Bluetooth player.
578      *
579      * <p>If {@code mediaId} corresponds to a known {@link BrowsedPlayerWrapper}, {@code cb} will be
580      * called with the folder items list of the {@link BrowsedPlayerWrapper}.
581      */
getFolderItems(int playerId, String mediaId, GetFolderItemsCallback cb)582     public void getFolderItems(int playerId, String mediaId, GetFolderItemsCallback cb) {
583         // The playerId is unused since we always assume the remote device is using the
584         // Bluetooth Player.
585         d("getFolderItems(): playerId=" + playerId + ", mediaId=" + mediaId);
586         /** M: Fix PTS AVRCP/TG/MCN/CB/BI-02-C fail @{ */
587         if (Utils.isPtsTestMode()) {
588             d("PTS test mode: getFolderItems");
589             BrowsedPlayerWrapper wrapper = mBrowsablePlayers.get(BLUETOOTH_PLAYER_ID + 1);
590             String itemId = mediaId;
591             if (mediaId.equals("")) {
592                 itemId = wrapper.getRootId();
593             }
594 
595             wrapper.getFolderItems(
596                     itemId,
597                     (status, id, results) -> {
598                         if (status != BrowsedPlayerWrapper.STATUS_SUCCESS) {
599                             cb.run(mediaId, new ArrayList<ListItem>());
600                             return;
601                         }
602                         cb.run(mediaId, results);
603                     });
604             return;
605         }
606         /**
607          * @}
608          */
609 
610         // The device is requesting the content of the root folder. This folder contains a list of
611         // Browsable Media Players displayed as folders with their contents contained within.
612         if (mediaId.equals("")) {
613             getFolderItemsMediaPlayerList(cb);
614             return;
615         }
616 
617         if (!mediaId.matches(BROWSE_ID_PATTERN)) {
618             // This should never happen since we control the media ID's reported
619             Log.wtf(TAG, "getFolderItems: mediaId didn't match pattern: mediaId=" + mediaId);
620         }
621 
622         int playerIndex = Integer.parseInt(mediaId.substring(0, 2));
623         String itemId = mediaId.substring(2);
624 
625         // TODO (apanicke): Add timeouts for looking up folder items since media browsers don't
626         // have to respond.
627         if (haveMediaBrowser(playerIndex)) {
628             BrowsedPlayerWrapper wrapper = mBrowsablePlayers.get(playerIndex);
629             if (itemId.equals("")) {
630                 Log.i(TAG, "Empty media id, getting the root for " + wrapper.getPackageName());
631                 itemId = wrapper.getRootId();
632             }
633 
634             wrapper.getFolderItems(
635                     itemId,
636                     (status, id, results) -> {
637                         if (status != BrowsedPlayerWrapper.STATUS_SUCCESS) {
638                             cb.run(mediaId, new ArrayList<ListItem>());
639                             return;
640                         }
641 
642                         String playerPrefix = String.format("%02d", playerIndex);
643                         for (ListItem item : results) {
644                             if (item.isFolder) {
645                                 item.folder.mediaId = playerPrefix.concat(item.folder.mediaId);
646                             } else {
647                                 item.song.mediaId = playerPrefix.concat(item.song.mediaId);
648                             }
649                         }
650                         cb.run(mediaId, results);
651                     });
652             return;
653         } else {
654             cb.run(mediaId, new ArrayList<ListItem>());
655         }
656     }
657 
658     /**
659      * Adds a {@link MediaController} to the {@link #mMediaPlayers} map and returns its ID.
660      *
661      * <p>Each {@link MediaController} is mapped to an ID and each ID is mapped to a package name.
662      * If the new {@link MediaController}'s package name is already present in {@link
663      * #mMediaPlayerIds}, we keep the ID and replace the {@link MediaController}. Otherwise, we add
664      * an entry in both {@link #mMediaPlayerIds} and {@link #mMediaPlayers} maps.
665      *
666      * <p>Also sends the new {@link MediaData} to the AVRCP service.
667      */
668     @VisibleForTesting
addMediaPlayer(MediaController controller)669     int addMediaPlayer(MediaController controller) {
670         // Each new player has an ID of 1 plus the highest ID. The ID 0 is reserved to signify that
671         // there is no active player. If we already have a browsable player for the package, reuse
672         // that key.
673         String packageName = controller.getPackageName();
674         if (!havePlayerId(packageName)) {
675             mMediaPlayerIds.put(packageName, getFreeMediaPlayerId());
676         }
677 
678         int playerId = mMediaPlayerIds.get(packageName);
679 
680         // If we already have a controller for the package, then update it with this new controller
681         // as the old controller has probably gone stale.
682         if (haveMediaPlayer(playerId)) {
683             d("Already have a controller for the player: " + packageName + ", updating instead");
684             MediaPlayerWrapper player = mMediaPlayers.get(playerId);
685             player.updateMediaController(controller);
686 
687             // If the media controller we updated was the active player check if the media updated
688             if (playerId == mActivePlayerId) {
689                 sendMediaUpdate(getActivePlayer().getCurrentMediaData());
690             }
691 
692             return playerId;
693         }
694 
695         MediaPlayerWrapper newPlayer =
696                 MediaPlayerWrapperFactory.wrap(mContext, controller, mLooper);
697 
698         Log.i(
699                 TAG,
700                 "Adding wrapped media player: "
701                         + packageName
702                         + " at key: "
703                         + mMediaPlayerIds.get(controller.getPackageName()));
704 
705         mMediaPlayers.put(playerId, newPlayer);
706         return playerId;
707     }
708 
709     /**
710      * Adds a {@link android.media.session.MediaController} to the {@link #mMediaPlayers} map and
711      * returns its ID. If the {@link android.media.session.MediaController} is null, returns -1.
712      *
713      * <p>See {@link #addMediaPlayer(MediaController)}.
714      */
addMediaPlayer(android.media.session.MediaController controller)715     int addMediaPlayer(android.media.session.MediaController controller) {
716         if (controller == null) {
717             e("Trying to add a null MediaController");
718             return -1;
719         }
720 
721         return addMediaPlayer(MediaControllerFactory.wrap(controller));
722     }
723 
724     /** Returns true if {@code packageName} is present in {@link #mMediaPlayerIds}. */
havePlayerId(String packageName)725     boolean havePlayerId(String packageName) {
726         if (packageName == null) return false;
727         return mMediaPlayerIds.containsKey(packageName);
728     }
729 
730     /**
731      * Returns true if {@code packageName} is present in {@link #mMediaPlayerIds} and the
732      * corresponding ID has a {@link MediaPlayerWrapper} set in {@link #mMediaPlayers}.
733      */
haveMediaPlayer(String packageName)734     boolean haveMediaPlayer(String packageName) {
735         if (!havePlayerId(packageName)) return false;
736         int playerId = mMediaPlayerIds.get(packageName);
737         return mMediaPlayers.containsKey(playerId);
738     }
739 
740     /** Returns true if {@code playerId} is present in {@link #mMediaPlayers}. */
haveMediaPlayer(int playerId)741     boolean haveMediaPlayer(int playerId) {
742         return mMediaPlayers.containsKey(playerId);
743     }
744 
745     /** Returns true if {@code playerId} is present in {@link #mBrowsablePlayers}. */
haveMediaBrowser(int playerId)746     boolean haveMediaBrowser(int playerId) {
747         return mBrowsablePlayers.containsKey(playerId);
748     }
749 
750     /**
751      * Removes the entry corresponding to {@code playerId} from {@link #mMediaPlayers} and {@link
752      * #mMediaPlayerIds}.
753      *
754      * <p>If the removed player was the active one, we consider that there is no more active players
755      * until we receive a {@link MediaSessionManager.OnActiveSessionsChangedListener} callback
756      * saying otherwise.
757      *
758      * <p>Also sends empty {@link MediaData} to the AVRCP service.
759      */
removeMediaPlayer(int playerId)760     void removeMediaPlayer(int playerId) {
761         if (!haveMediaPlayer(playerId)) {
762             e("Trying to remove nonexistent media player: " + playerId);
763             return;
764         }
765 
766         // If we removed the active player, set no player as active until the Media Framework
767         // tells us otherwise
768         if (playerId == mActivePlayerId && playerId != NO_ACTIVE_PLAYER) {
769             getActivePlayer().unregisterCallback();
770             mActivePlayerId = NO_ACTIVE_PLAYER;
771             List<Metadata> queue = new ArrayList<Metadata>();
772             queue.add(Util.empty_data());
773             MediaData newData = new MediaData(Util.empty_data(), null, queue);
774 
775             sendMediaUpdate(newData);
776         }
777 
778         final MediaPlayerWrapper wrapper = mMediaPlayers.get(playerId);
779         d("Removing media player " + wrapper.getPackageName());
780         mMediaPlayers.remove(playerId);
781         if (!haveMediaBrowser(playerId)) {
782             d(wrapper.getPackageName() + " doesn't have a browse service. Recycle player ID.");
783             mMediaPlayerIds.remove(wrapper.getPackageName());
784         }
785         wrapper.cleanup();
786     }
787 
788     /**
789      * Sets {@code playerId} as the new active player and sends the new player's {@link Mediadata}
790      * to the AVRCP service.
791      *
792      * <p>Also informs the {@link #PlayerSettingsManager} about the change of active player.
793      */
setActivePlayer(int playerId)794     void setActivePlayer(int playerId) {
795         if (!haveMediaPlayer(playerId)) {
796             e("Player doesn't exist in list(): " + playerId);
797             return;
798         }
799 
800         if (playerId == mActivePlayerId) {
801             Log.w(TAG, getActivePlayer().getPackageName() + " is already the active player");
802             return;
803         }
804 
805         if (mActivePlayerId != NO_ACTIVE_PLAYER) getActivePlayer().unregisterCallback();
806 
807         mActivePlayerId = playerId;
808         getActivePlayer().registerCallback(mMediaPlayerCallback);
809         mActivePlayerLogger.logd(
810                 TAG, "setActivePlayer(): setting player to " + getActivePlayer().getPackageName());
811 
812         if (mPlayerSettingsListener != null) {
813             mPlayerSettingsListener.onActivePlayerChanged(getActivePlayer());
814         }
815 
816         // Ensure that metadata is synced on the new player
817         if (!getActivePlayer().isMetadataSynced()) {
818             Log.w(TAG, "setActivePlayer(): Metadata not synced on new player");
819             return;
820         }
821 
822         if (Utils.isPtsTestMode()) {
823             sendFolderUpdate(true, true, false);
824         }
825 
826         MediaData data = getActivePlayer().getCurrentMediaData();
827         if (mAudioPlaybackIsActive) {
828             data.state = mCurrMediaData.state;
829             Log.d(TAG, "setActivePlayer mAudioPlaybackIsActive=true, state=" + data.state);
830         }
831         sendMediaUpdate(data);
832     }
833 
834     /** Informs AVRCP service that there has been an update in browsable players. */
sendFolderUpdate( boolean availablePlayers, boolean addressedPlayers, boolean uids)835     private void sendFolderUpdate(
836             boolean availablePlayers, boolean addressedPlayers, boolean uids) {
837         d("sendFolderUpdate");
838         if (mCallback == null) {
839             return;
840         }
841 
842         mCallback.run(availablePlayers, addressedPlayers, uids);
843     }
844 
845     /** Indicates that there have been a {@link MediaData} update for the current active player. */
sendMediaUpdate(MediaData data)846     private void sendMediaUpdate(MediaData data) {
847         d("sendMediaUpdate");
848         if (mCallback == null || data == null) {
849             return;
850         }
851 
852         // Always have items in the queue
853         if (data.queue.size() == 0) {
854             Log.i(TAG, "sendMediaUpdate: Creating a one item queue for a player with no queue");
855             data.queue.add(data.metadata);
856         }
857 
858         Log.d(TAG, "sendMediaUpdate state=" + data.state);
859         mCurrMediaData = data;
860         mCallback.run(data);
861     }
862 
863     /**
864      * Callback from Media Framework to indicate that the active session changed.
865      *
866      * <p>As sessions can have multiple {@link android.media.session.MediaController}, we add all
867      * the new players, keeping only the highest priority {@link
868      * android.media.session.MediaController} per package name (priority is defined by order in the
869      * list).
870      *
871      * <p>Note: This does not set the current active player, only adds the new {@link
872      * MediaController} to the {@link #mMediaPlayerIds} and {@link mMediaPlayers} maps.
873      *
874      * <p>See {@link #onMediaKeyEventSessionChanged}.
875      */
876     @VisibleForTesting
877     final MediaSessionManager.OnActiveSessionsChangedListener mActiveSessionsChangedListener =
878             new MediaSessionManager.OnActiveSessionsChangedListener() {
879                 @Override
880                 public void onActiveSessionsChanged(
881                         List<android.media.session.MediaController> newControllers) {
882                     synchronized (MediaPlayerList.this) {
883                         Log.v(
884                                 TAG,
885                                 "onActiveSessionsChanged: number of controllers: "
886                                         + newControllers.size());
887                         if (newControllers.size() == 0) {
888                             if (mPlayerSettingsListener != null) {
889                                 mPlayerSettingsListener.onActivePlayerChanged(null);
890                             }
891                             return;
892                         }
893 
894                         // Apps are allowed to have multiple MediaControllers. If an app does have
895                         // multiple controllers then newControllers contains them in highest
896                         // priority order. Since we only want to keep the highest priority one,
897                         // we keep track of which controllers we updated and skip over ones
898                         // we've already looked at.
899                         HashSet<String> addedPackages = new HashSet<String>();
900 
901                         for (int i = 0; i < newControllers.size(); i++) {
902                             if ((newControllers.get(i).getFlags()
903                                             & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY)
904                                     != 0) {
905                                 Log.d(
906                                         TAG,
907                                         "onActiveSessionsChanged: controller: "
908                                                 + newControllers.get(i).getPackageName()
909                                                 + " ignored due to global priority flag");
910                                 continue;
911                             }
912                             Log.d(
913                                     TAG,
914                                     "onActiveSessionsChanged: controller: "
915                                             + newControllers.get(i).getPackageName());
916                             if (addedPackages.contains(newControllers.get(i).getPackageName())) {
917                                 continue;
918                             }
919 
920                             addedPackages.add(newControllers.get(i).getPackageName());
921                             addMediaPlayer(newControllers.get(i));
922                         }
923                     }
924                 }
925             };
926 
927     /**
928      * {@link android.content.BroadcastReceiver} to catch intents indicating package add, change and
929      * removal.
930      *
931      * <p>If a package is removed while its corresponding {@link MediaPlayerWrapper} is present in
932      * the {@link #mMediaPlayerIds} and {@link mMediaPlayers} maps, remove it.
933      *
934      * <p>If a package is added or changed, currently nothing is done. Ideally, this should add it
935      * to the {@link #mMediaPlayerIds} and {@link mMediaPlayers} maps.
936      *
937      * <p>See {@link #removeMediaPlayer} and {@link
938      * #addMediaPlayer(android.media.session.MediaController)}
939      */
940     private final BroadcastReceiver mPackageChangedBroadcastReceiver =
941             new BroadcastReceiver() {
942                 @Override
943                 public void onReceive(Context context, Intent intent) {
944                     String action = intent.getAction();
945                     Log.v(TAG, "mPackageChangedBroadcastReceiver: action: " + action);
946 
947                     if (action.equals(Intent.ACTION_PACKAGE_REMOVED)
948                             || action.equals(Intent.ACTION_PACKAGE_DATA_CLEARED)) {
949                         if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) return;
950 
951                         String packageName = intent.getData().getSchemeSpecificPart();
952                         if (haveMediaPlayer(packageName)) {
953                             removeMediaPlayer(mMediaPlayerIds.get(packageName));
954                         }
955                     } else if (action.equals(Intent.ACTION_PACKAGE_ADDED)
956                             || action.equals(Intent.ACTION_PACKAGE_CHANGED)) {
957                         String packageName = intent.getData().getSchemeSpecificPart();
958                         if (packageName != null) {
959                             Log.d(TAG, "Name of package changed: " + packageName);
960                             // TODO (apanicke): Handle either updating or adding the new package.
961                             // Check if its browsable and send the UIDS changed to update the
962                             // root folder
963                         }
964                     }
965                 }
966             };
967 
968     /**
969      * Retrieves and sends the current {@link MediaData} of the active player (if present) to the
970      * AVRCP service or if there is no active player, sends an empty {@link MediaData}.
971      *
972      * <p>See {@link #sendMediaUpdate}.
973      */
updateMediaForAudioPlayback()974     void updateMediaForAudioPlayback() {
975         MediaData currMediaData = null;
976         PlaybackState currState = null;
977         if (getActivePlayer() == null) {
978             Log.d(TAG, "updateMediaForAudioPlayback: no active player");
979             PlaybackState.Builder builder =
980                     new PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f);
981             List<Metadata> queue = new ArrayList<Metadata>();
982             queue.add(Util.empty_data());
983             currMediaData = new MediaData(Util.empty_data(), builder.build(), queue);
984         } else {
985             currMediaData = getActivePlayer().getCurrentMediaData();
986             currState = currMediaData.state;
987         }
988 
989         if (currState != null && currState.getState() == PlaybackState.STATE_PLAYING) {
990             Log.i(TAG, "updateMediaForAudioPlayback: Active player is playing, drop it");
991             return;
992         }
993 
994         if (mAudioPlaybackIsActive) {
995             PlaybackState.Builder builder =
996                     new PlaybackState.Builder()
997                             .setState(
998                                     PlaybackState.STATE_PLAYING,
999                                     currState == null ? 0 : currState.getPosition(),
1000                                     1.0f);
1001             currMediaData.state = builder.build();
1002         }
1003         mAudioPlaybackStateLogger.logd(
1004                 TAG, "updateMediaForAudioPlayback: update state=" + currMediaData.state);
1005         sendMediaUpdate(currMediaData);
1006     }
1007 
1008     /** For testing purposes only, sets the {@link #mAudioPlaybackIsActive} flag. */
1009     @VisibleForTesting
injectAudioPlaybacActive(boolean isActive)1010     void injectAudioPlaybacActive(boolean isActive) {
1011         mAudioPlaybackIsActive = isActive;
1012         updateMediaForAudioPlayback();
1013     }
1014 
1015     /**
1016      * Saves the reference to {@link MediaPlayerSettingsEventListener} to be called when the active
1017      * player changed, so that {@link #PlayerSettingsManager} always has the right player.
1018      */
setPlayerSettingsCallback(MediaPlayerSettingsEventListener listener)1019     void setPlayerSettingsCallback(MediaPlayerSettingsEventListener listener) {
1020         mPlayerSettingsListener = listener;
1021     }
1022 
1023     /**
1024      * Listens to playback configurations changes, to set the {@link #mAudioPlaybackIsActive} flag.
1025      */
1026     private final AudioManager.AudioPlaybackCallback mAudioPlaybackCallback =
1027             new AudioManager.AudioPlaybackCallback() {
1028                 @Override
1029                 public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs) {
1030                     if (configs == null) {
1031                         return;
1032                     }
1033                     boolean isActive = false;
1034                     AudioPlaybackConfiguration activeConfig = null;
1035                     for (AudioPlaybackConfiguration config : configs) {
1036                         if (config.isActive()
1037                                 && (config.getAudioAttributes().getUsage()
1038                                         == AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
1039                                 && (config.getAudioAttributes().getContentType()
1040                                         == AudioAttributes.CONTENT_TYPE_SPEECH)) {
1041                             activeConfig = config;
1042                             isActive = true;
1043                         }
1044                     }
1045                     if (isActive != mAudioPlaybackIsActive) {
1046                         mAudioPlaybackStateLogger.logd(
1047                                 TAG,
1048                                 "onPlaybackConfigChanged: "
1049                                         + (mAudioPlaybackIsActive ? "Active" : "Non-active")
1050                                         + " -> "
1051                                         + (isActive ? "Active" : "Non-active"));
1052                         if (isActive) {
1053                             mAudioPlaybackStateLogger.logd(
1054                                     TAG,
1055                                     "onPlaybackConfigChanged: " + "active config: " + activeConfig);
1056                         }
1057                         mAudioPlaybackIsActive = isActive;
1058                         updateMediaForAudioPlayback();
1059                     }
1060                 }
1061             };
1062 
1063     /**
1064      * Callback from {@link MediaPlayerWrapper}.
1065      *
1066      * <p>{@link #mediaUpdatedCallback} listens for {@link #MediaData} changes on the active player.
1067      *
1068      * <p>{@link #sessionUpdatedCallback} is called when the active session is destroyed so we need
1069      * to remove the media player from the {@link #mMediaPlayerIds} and {@link mMediaPlayers} maps.
1070      */
1071     private final MediaPlayerWrapper.Callback mMediaPlayerCallback =
1072             new MediaPlayerWrapper.Callback() {
1073                 @Override
1074                 public void mediaUpdatedCallback(MediaData data) {
1075                     if (data.metadata == null) {
1076                         Log.d(TAG, "mediaUpdatedCallback(): metadata is null");
1077                         return;
1078                     }
1079 
1080                     if (data.state == null) {
1081                         Log.w(TAG, "mediaUpdatedCallback(): Tried to update with null state");
1082                         return;
1083                     }
1084 
1085                     if (mAudioPlaybackIsActive
1086                             && (data.state.getState() != PlaybackState.STATE_PLAYING)) {
1087                         Log.d(TAG, "Some audio playbacks are still active, drop it");
1088                         return;
1089                     }
1090                     sendMediaUpdate(data);
1091                 }
1092 
1093                 @Override
1094                 public void sessionUpdatedCallback(String packageName) {
1095                     if (haveMediaPlayer(packageName)) {
1096                         Log.d(TAG, "sessionUpdatedCallback(): packageName: " + packageName);
1097                         removeMediaPlayer(mMediaPlayerIds.get(packageName));
1098                     }
1099                 }
1100             };
1101 
1102     /**
1103      * Listens for Media key events session changes.
1104      *
1105      * <p>The Media session that listens to key events is considered the active session.
1106      *
1107      * <p>This will retrieve the {@link android.media.session.MediaController} for this session with
1108      * the {@code token} provided and set it as the active one.
1109      *
1110      * <p>If the {@link android.media.session.MediaController} flags include the {@link
1111      * MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY}, the session change shall be ignored as this
1112      * flag is used only by Telecom to handle wired headsets key events.
1113      *
1114      * <p>It can happen that {@code token} is null, in such case wecan still check if we have a
1115      * {@link MediaController} corresponding to {@code packageName} and set it as active.
1116      */
1117     @VisibleForTesting
1118     final MediaSessionManager.OnMediaKeyEventSessionChangedListener
1119             mMediaKeyEventSessionChangedListener =
1120                     new MediaSessionManager.OnMediaKeyEventSessionChangedListener() {
1121                         @Override
1122                         public void onMediaKeyEventSessionChanged(
1123                                 String packageName, MediaSession.Token token) {
1124                             if (mMediaSessionManager == null) {
1125                                 Log.w(
1126                                         TAG,
1127                                         "onMediaKeyEventSessionChanged(): Unexpected callback "
1128                                                 + "from the MediaSessionManager, pkg"
1129                                                 + packageName);
1130                                 return;
1131                             }
1132                             if (TextUtils.isEmpty(packageName)) {
1133                                 return;
1134                             }
1135                             if (token != null) {
1136                                 android.media.session.MediaController controller =
1137                                         new android.media.session.MediaController(mContext, token);
1138                                 if ((controller.getFlags()
1139                                                 & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY)
1140                                         != 0) {
1141                                     // Skip adding controller for GLOBAL_PRIORITY session.
1142                                     Log.i(
1143                                             TAG,
1144                                             "onMediaKeyEventSessionChanged,"
1145                                                     + " ignoring global priority session");
1146                                     return;
1147                                 }
1148                                 if (!haveMediaPlayer(controller.getPackageName())) {
1149                                     // Since we have a controller, we can try to to recover by
1150                                     // adding the
1151                                     // player and then setting it as active.
1152                                     Log.w(
1153                                             TAG,
1154                                             "onMediaKeyEventSessionChanged(Token): Addressed Player"
1155                                                 + " changed to a player we didn't have a session"
1156                                                 + " for");
1157                                     addMediaPlayer(controller);
1158                                 }
1159 
1160                                 Log.i(
1161                                         TAG,
1162                                         "onMediaKeyEventSessionChanged: token="
1163                                                 + controller.getPackageName());
1164                                 setActivePlayer(mMediaPlayerIds.get(controller.getPackageName()));
1165                             } else {
1166                                 if (!haveMediaPlayer(packageName)) {
1167                                     e(
1168                                             "onMediaKeyEventSessionChanged(PackageName): Media key"
1169                                                 + " event session changed to a player we don't have"
1170                                                 + " a session for");
1171                                     return;
1172                                 }
1173 
1174                                 Log.i(
1175                                         TAG,
1176                                         "onMediaKeyEventSessionChanged: packageName="
1177                                                 + packageName);
1178                                 setActivePlayer(mMediaPlayerIds.get(packageName));
1179                             }
1180                         }
1181                     };
1182 
1183     /** Dumps all players and browsable players currently listed in this class. */
dump(StringBuilder sb)1184     public void dump(StringBuilder sb) {
1185         sb.append("List of MediaControllers: size=" + mMediaPlayers.size() + "\n");
1186         for (int id : mMediaPlayers.keySet()) {
1187             if (id == mActivePlayerId) {
1188                 sb.append("<Active> ");
1189             }
1190             MediaPlayerWrapper player = mMediaPlayers.get(id);
1191             sb.append("  Media Player " + id + ": " + player.getPackageName() + "\n");
1192             sb.append(player.toString().replaceAll("(?m)^", "  "));
1193             sb.append("\n");
1194         }
1195 
1196         sb.append("List of Browsers: size=" + mBrowsablePlayers.size() + "\n");
1197         for (BrowsedPlayerWrapper player : mBrowsablePlayers.values()) {
1198             sb.append(player.toString().replaceAll("(?m)^", "  "));
1199             sb.append("\n");
1200         }
1201 
1202         mActivePlayerLogger.dump(sb);
1203         sb.append("\n");
1204         mAudioPlaybackStateLogger.dump(sb);
1205         sb.append("\n");
1206     }
1207 
e(String message)1208     private static void e(String message) {
1209         if (sTesting) {
1210             Log.wtf(TAG, message);
1211         } else {
1212             Log.e(TAG, message);
1213         }
1214     }
1215 
d(String message)1216     private static void d(String message) {
1217         Log.d(TAG, message);
1218     }
1219 }
1220