• 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.text.TextUtils;
35 import android.util.Log;
36 import android.view.KeyEvent;
37 
38 import com.android.bluetooth.Utils;
39 import com.android.internal.annotations.VisibleForTesting;
40 
41 import java.util.ArrayList;
42 import java.util.Collections;
43 import java.util.HashMap;
44 import java.util.HashSet;
45 import java.util.List;
46 import java.util.Map;
47 import java.util.regex.Matcher;
48 import java.util.regex.Pattern;
49 
50 /**
51  * This class is directly responsible of maintaining the list of Browsable Players as well as
52  * the list of Addressable Players. This variation of the list doesn't actually list all the
53  * available players for a getAvailableMediaPlayers request. Instead it only reports one media
54  * player with ID=0 and all the other browsable players are folders in the root of that player.
55  *
56  * Changing the directory to a browsable player will allow you to traverse that player as normal.
57  * By only having one root player, we never have to send Addressed Player Changed notifications,
58  * UIDs Changed notifications, or Available Players Changed notifications.
59  *
60  * TODO (apanicke): Add non-browsable players as song items to the root folder. Selecting that
61  * player would effectively cause player switch by sending a play command to that player.
62  */
63 public class MediaPlayerList {
64     private static final String TAG = "MediaPlayerList";
65     private static final boolean DEBUG = true;
66     static boolean sTesting = false;
67 
68     private static final String PACKAGE_SCHEME = "package";
69     private static final int NO_ACTIVE_PLAYER = 0;
70     private static final int BLUETOOTH_PLAYER_ID = 0;
71     private static final String BLUETOOTH_PLAYER_NAME = "Bluetooth Player";
72     private static final int ACTIVE_PLAYER_LOGGER_SIZE = 5;
73     private static final String ACTIVE_PLAYER_LOGGER_TITLE = "Active Player Events";
74     private static final int AUDIO_PLAYBACK_STATE_LOGGER_SIZE = 15;
75     private static final String AUDIO_PLAYBACK_STATE_LOGGER_TITLE = "Audio Playback State Events";
76 
77     // mediaId's for the now playing list will be in the form of "NowPlayingId[XX]" where [XX]
78     // is the Queue ID for the requested item.
79     private static final String NOW_PLAYING_ID_PATTERN = Util.NOW_PLAYING_PREFIX + "([0-9]*)";
80 
81     // mediaId's for folder browsing will be in the form of [XX][mediaid],  where [XX] is a
82     // two digit representation of the player id and [mediaid] is the original media id as a
83     // string.
84     private static final String BROWSE_ID_PATTERN = "\\d\\d.*";
85 
86     private Context mContext;
87     private Looper mLooper; // Thread all media player callbacks and timeouts happen on
88     private PackageManager mPackageManager;
89     private MediaSessionManager mMediaSessionManager;
90     private MediaData mCurrMediaData = null;
91     private final AudioManager mAudioManager;
92 
93     private final BTAudioEventLogger mActivePlayerLogger = new BTAudioEventLogger(
94         ACTIVE_PLAYER_LOGGER_SIZE, ACTIVE_PLAYER_LOGGER_TITLE);
95     private final BTAudioEventLogger mAudioPlaybackStateLogger = new BTAudioEventLogger(
96         AUDIO_PLAYBACK_STATE_LOGGER_SIZE, AUDIO_PLAYBACK_STATE_LOGGER_TITLE);
97 
98     private Map<Integer, MediaPlayerWrapper> mMediaPlayers =
99             Collections.synchronizedMap(new HashMap<Integer, MediaPlayerWrapper>());
100     private Map<String, Integer> mMediaPlayerIds =
101             Collections.synchronizedMap(new HashMap<String, Integer>());
102     private Map<Integer, BrowsedPlayerWrapper> mBrowsablePlayers =
103             Collections.synchronizedMap(new HashMap<Integer, BrowsedPlayerWrapper>());
104     private int mActivePlayerId = NO_ACTIVE_PLAYER;
105 
106     private MediaUpdateCallback mCallback;
107     private boolean mAudioPlaybackIsActive = false;
108 
109     private BrowsablePlayerConnector mBrowsablePlayerConnector;
110 
111     public interface MediaUpdateCallback {
run(MediaData data)112         void run(MediaData data);
run(boolean availablePlayers, boolean addressedPlayers, boolean uids)113         void run(boolean availablePlayers, boolean addressedPlayers, boolean uids);
114     }
115 
116     public interface GetPlayerRootCallback {
run(int playerId, boolean success, String rootId, int numItems)117         void run(int playerId, boolean success, String rootId, int numItems);
118     }
119 
120     public interface GetFolderItemsCallback {
run(String parentId, List<ListItem> items)121         void run(String parentId, List<ListItem> items);
122     }
123 
MediaPlayerList(Looper looper, Context context)124     public MediaPlayerList(Looper looper, Context context) {
125         Log.v(TAG, "Creating MediaPlayerList");
126 
127         mLooper = looper;
128         mContext = context;
129 
130         // Register for intents where available players might have changed
131         IntentFilter pkgFilter = new IntentFilter();
132         pkgFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
133         pkgFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
134         pkgFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
135         pkgFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
136         pkgFilter.addDataScheme(PACKAGE_SCHEME);
137         context.registerReceiver(mPackageChangedBroadcastReceiver, pkgFilter);
138 
139         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
140         mAudioManager.registerAudioPlaybackCallback(mAudioPlaybackCallback, new Handler(mLooper));
141 
142         mMediaSessionManager =
143                 (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
144         mMediaSessionManager.addOnActiveSessionsChangedListener(
145                 mActiveSessionsChangedListener, null, new Handler(looper));
146         mMediaSessionManager.addOnMediaKeyEventSessionChangedListener(
147                 mContext.getMainExecutor(), mMediaKeyEventSessionChangedListener);
148     }
149 
init(MediaUpdateCallback callback)150     public void init(MediaUpdateCallback callback) {
151         Log.v(TAG, "Initializing MediaPlayerList");
152         mCallback = callback;
153 
154         // Build the list of browsable players and afterwards, build the list of media players
155         Intent intent = new Intent(android.service.media.MediaBrowserService.SERVICE_INTERFACE);
156         List<ResolveInfo> playerList =
157                 mContext
158                     .getApplicationContext()
159                     .getPackageManager()
160                     .queryIntentServices(intent, PackageManager.MATCH_ALL);
161 
162         mBrowsablePlayerConnector = BrowsablePlayerConnector.connectToPlayers(mContext, mLooper,
163                 playerList, (List<BrowsedPlayerWrapper> players) -> {
164                 Log.i(TAG, "init: Browsable Player list size is " + players.size());
165 
166                 // Check to see if the list has been cleaned up before this completed
167                 if (mMediaSessionManager == null) {
168                     return;
169                 }
170 
171                 for (BrowsedPlayerWrapper wrapper : players) {
172                     // Generate new id and add the browsable player
173                     if (!havePlayerId(wrapper.getPackageName())) {
174                         mMediaPlayerIds.put(wrapper.getPackageName(), getFreeMediaPlayerId());
175                     }
176 
177                     d("Adding Browser Wrapper for " + wrapper.getPackageName() + " with id "
178                             + mMediaPlayerIds.get(wrapper.getPackageName()));
179 
180                     mBrowsablePlayers.put(mMediaPlayerIds.get(wrapper.getPackageName()), wrapper);
181 
182                     wrapper.getFolderItems(wrapper.getRootId(),
183                             (int status, String mediaId, List<ListItem> results) -> {
184                                 d("Got the contents for: " + mediaId + " : num results="
185                                         + results.size());
186                             });
187                 }
188 
189                 // Construct the list of current players
190                 d("Initializing list of current media players");
191                 List<android.media.session.MediaController> controllers =
192                         mMediaSessionManager.getActiveSessions(null);
193 
194                 for (android.media.session.MediaController controller : controllers) {
195                     addMediaPlayer(controller);
196                 }
197 
198                 // If there were any active players and we don't already have one due to the Media
199                 // Framework Callbacks then set the highest priority one to active
200                 if (mActivePlayerId == 0 && mMediaPlayers.size() > 0) setActivePlayer(1);
201             });
202     }
203 
cleanup()204     public void cleanup() {
205         mContext.unregisterReceiver(mPackageChangedBroadcastReceiver);
206 
207         mActivePlayerId = NO_ACTIVE_PLAYER;
208 
209         mMediaSessionManager.removeOnActiveSessionsChangedListener(mActiveSessionsChangedListener);
210         mMediaSessionManager.removeOnMediaKeyEventSessionChangedListener(
211                 mMediaKeyEventSessionChangedListener);
212         mMediaSessionManager = null;
213 
214         mAudioManager.unregisterAudioPlaybackCallback(mAudioPlaybackCallback);
215 
216         mMediaPlayerIds.clear();
217 
218         for (MediaPlayerWrapper player : mMediaPlayers.values()) {
219             player.cleanup();
220         }
221         mMediaPlayers.clear();
222 
223         if (mBrowsablePlayerConnector != null) {
224             mBrowsablePlayerConnector.cleanup();
225         }
226         for (BrowsedPlayerWrapper player : mBrowsablePlayers.values()) {
227             player.disconnect();
228         }
229         mBrowsablePlayers.clear();
230     }
231 
getCurrentPlayerId()232     public int getCurrentPlayerId() {
233         return BLUETOOTH_PLAYER_ID;
234     }
235 
getFreeMediaPlayerId()236     int getFreeMediaPlayerId() {
237         int id = 1;
238         while (mMediaPlayerIds.containsValue(id)) {
239             id++;
240         }
241         return id;
242     }
243 
getActivePlayer()244     public MediaPlayerWrapper getActivePlayer() {
245         return mMediaPlayers.get(mActivePlayerId);
246     }
247 
248     // In this case the displayed player is the Bluetooth Player, the number of items is equal
249     // to the number of players. The root ID will always be empty string in this case as well.
getPlayerRoot(int playerId, GetPlayerRootCallback cb)250     public void getPlayerRoot(int playerId, GetPlayerRootCallback cb) {
251         /** M: Fix PTS AVRCP/TG/MCN/CB/BI-02-C fail @{ */
252         if (Utils.isPtsTestMode()) {
253             d("PTS test mode: getPlayerRoot");
254             BrowsedPlayerWrapper wrapper = mBrowsablePlayers.get(BLUETOOTH_PLAYER_ID + 1);
255             String itemId = wrapper.getRootId();
256 
257             wrapper.getFolderItems(itemId, (status, id, results) -> {
258                 if (status != BrowsedPlayerWrapper.STATUS_SUCCESS) {
259                     cb.run(playerId, playerId == BLUETOOTH_PLAYER_ID, "", 0);
260                     return;
261                 }
262                 cb.run(playerId, playerId == BLUETOOTH_PLAYER_ID, "", results.size());
263             });
264             return;
265         }
266         /** @} */
267         cb.run(playerId, playerId == BLUETOOTH_PLAYER_ID, "", mBrowsablePlayers.size());
268     }
269 
270     // Return the "Bluetooth Player" as the only player always
getMediaPlayerList()271     public List<PlayerInfo> getMediaPlayerList() {
272         PlayerInfo info = new PlayerInfo();
273         info.id = BLUETOOTH_PLAYER_ID;
274         info.name = BLUETOOTH_PLAYER_NAME;
275         info.browsable = true;
276         if (mBrowsablePlayers.size() == 0) {
277             // Set Bluetooth Player as non-browable if there is not browsable player exist.
278             info.browsable = false;
279         }
280         List<PlayerInfo> ret = new ArrayList<PlayerInfo>();
281         ret.add(info);
282 
283         return ret;
284     }
285 
286     @NonNull
getCurrentMediaId()287     public String getCurrentMediaId() {
288         final MediaPlayerWrapper player = getActivePlayer();
289         if (player == null) return "";
290 
291         final PlaybackState state = player.getPlaybackState();
292         final List<Metadata> queue = player.getCurrentQueue();
293 
294         // Disable the now playing list if the player doesn't have a queue or provide an active
295         // queue ID that can be used to determine the active song in the queue.
296         if (state == null
297                 || state.getActiveQueueItemId() == MediaSession.QueueItem.UNKNOWN_ID
298                 || queue.size() == 0) {
299             d("getCurrentMediaId: No active queue item Id sending empty mediaId: PlaybackState="
300                      + state);
301             return "";
302         }
303 
304         return Util.NOW_PLAYING_PREFIX + state.getActiveQueueItemId();
305     }
306 
307     @NonNull
getCurrentSongInfo()308     public Metadata getCurrentSongInfo() {
309         final MediaPlayerWrapper player = getActivePlayer();
310         if (player == null) return Util.empty_data();
311 
312         return player.getCurrentMetadata();
313     }
314 
getCurrentPlayStatus()315     public PlaybackState getCurrentPlayStatus() {
316         final MediaPlayerWrapper player = getActivePlayer();
317         if (player == null) return null;
318 
319         PlaybackState state = player.getPlaybackState();
320         if (mAudioPlaybackIsActive
321                 && (state == null || state.getState() != PlaybackState.STATE_PLAYING)) {
322             return new PlaybackState.Builder()
323                 .setState(PlaybackState.STATE_PLAYING,
324                           state == null ? 0 : state.getPosition(),
325                           1.0f)
326                 .build();
327         }
328         return state;
329     }
330 
331     @NonNull
getNowPlayingList()332     public List<Metadata> getNowPlayingList() {
333         // Only send the current song for the now playing if there is no active song. See
334         // |getCurrentMediaId()| for reasons why there might be no active song.
335         if (getCurrentMediaId().equals("")) {
336             List<Metadata> ret = new ArrayList<Metadata>();
337             Metadata data = getCurrentSongInfo();
338             data.mediaId = "";
339             ret.add(data);
340             return ret;
341         }
342 
343         return getActivePlayer().getCurrentQueue();
344     }
345 
playItem(int playerId, boolean nowPlaying, String mediaId)346     public void playItem(int playerId, boolean nowPlaying, String mediaId) {
347         if (nowPlaying) {
348             playNowPlayingItem(mediaId);
349         } else {
350             playFolderItem(mediaId);
351         }
352     }
353 
playNowPlayingItem(String mediaId)354     private void playNowPlayingItem(String mediaId) {
355         d("playNowPlayingItem: mediaId=" + mediaId);
356 
357         Pattern regex = Pattern.compile(NOW_PLAYING_ID_PATTERN);
358         Matcher m = regex.matcher(mediaId);
359         if (!m.find()) {
360             // This should never happen since we control the media ID's reported
361             Log.wtf(TAG, "playNowPlayingItem: Couldn't match mediaId to pattern: mediaId="
362                     + mediaId);
363         }
364 
365         long queueItemId = Long.parseLong(m.group(1));
366         if (getActivePlayer() != null) {
367             getActivePlayer().playItemFromQueue(queueItemId);
368         }
369     }
370 
playFolderItem(String mediaId)371     private void playFolderItem(String mediaId) {
372         d("playFolderItem: mediaId=" + mediaId);
373 
374         if (!mediaId.matches(BROWSE_ID_PATTERN)) {
375             // This should never happen since we control the media ID's reported
376             Log.wtf(TAG, "playFolderItem: mediaId didn't match pattern: mediaId=" + mediaId);
377         }
378 
379         int playerIndex = Integer.parseInt(mediaId.substring(0, 2));
380         String itemId = mediaId.substring(2);
381 
382         if (!haveMediaBrowser(playerIndex)) {
383             e("playFolderItem: Do not have the a browsable player with ID " + playerIndex);
384             return;
385         }
386 
387         mBrowsablePlayers.get(playerIndex).playItem(itemId);
388     }
389 
getFolderItemsMediaPlayerList(GetFolderItemsCallback cb)390     void getFolderItemsMediaPlayerList(GetFolderItemsCallback cb) {
391         d("getFolderItemsMediaPlayerList: Sending Media Player list for root directory");
392 
393         ArrayList<ListItem> playerList = new ArrayList<ListItem>();
394         for (BrowsedPlayerWrapper player : mBrowsablePlayers.values()) {
395 
396             String displayName = Util.getDisplayName(mContext, player.getPackageName());
397             int id = mMediaPlayerIds.get(player.getPackageName());
398 
399             d("getFolderItemsMediaPlayerList: Adding player " + displayName);
400             Folder playerFolder = new Folder(String.format("%02d", id), false, displayName);
401             playerList.add(new ListItem(playerFolder));
402         }
403         cb.run("", playerList);
404         return;
405     }
406 
getFolderItems(int playerId, String mediaId, GetFolderItemsCallback cb)407     public void getFolderItems(int playerId, String mediaId, GetFolderItemsCallback cb) {
408         // The playerId is unused since we always assume the remote device is using the
409         // Bluetooth Player.
410         d("getFolderItems(): playerId=" + playerId + ", mediaId=" + mediaId);
411         /** M: Fix PTS AVRCP/TG/MCN/CB/BI-02-C fail @{ */
412         if (Utils.isPtsTestMode()) {
413             d("PTS test mode: getFolderItems");
414             BrowsedPlayerWrapper wrapper = mBrowsablePlayers.get(BLUETOOTH_PLAYER_ID + 1);
415             String itemId = mediaId;
416             if (mediaId.equals("")) {
417                 itemId = wrapper.getRootId();
418             }
419 
420             wrapper.getFolderItems(itemId, (status, id, results) -> {
421                 if (status != BrowsedPlayerWrapper.STATUS_SUCCESS) {
422                     cb.run(mediaId, new ArrayList<ListItem>());
423                     return;
424                 }
425                 cb.run(mediaId, results);
426             });
427             return;
428         }
429         /** @} */
430 
431         // The device is requesting the content of the root folder. This folder contains a list of
432         // Browsable Media Players displayed as folders with their contents contained within.
433         if (mediaId.equals("")) {
434             getFolderItemsMediaPlayerList(cb);
435             return;
436         }
437 
438         if (!mediaId.matches(BROWSE_ID_PATTERN)) {
439             // This should never happen since we control the media ID's reported
440             Log.wtf(TAG, "getFolderItems: mediaId didn't match pattern: mediaId=" + mediaId);
441         }
442 
443         int playerIndex = Integer.parseInt(mediaId.substring(0, 2));
444         String itemId = mediaId.substring(2);
445 
446         // TODO (apanicke): Add timeouts for looking up folder items since media browsers don't
447         // have to respond.
448         if (haveMediaBrowser(playerIndex)) {
449             BrowsedPlayerWrapper wrapper = mBrowsablePlayers.get(playerIndex);
450             if (itemId.equals("")) {
451                 Log.i(TAG, "Empty media id, getting the root for "
452                         + wrapper.getPackageName());
453                 itemId = wrapper.getRootId();
454             }
455 
456             wrapper.getFolderItems(itemId, (status, id, results) -> {
457                 if (status != BrowsedPlayerWrapper.STATUS_SUCCESS) {
458                     cb.run(mediaId, new ArrayList<ListItem>());
459                     return;
460                 }
461 
462                 String playerPrefix = String.format("%02d", playerIndex);
463                 for (ListItem item : results) {
464                     if (item.isFolder) {
465                         item.folder.mediaId = playerPrefix.concat(item.folder.mediaId);
466                     } else {
467                         item.song.mediaId = playerPrefix.concat(item.song.mediaId);
468                     }
469                 }
470                 cb.run(mediaId, results);
471             });
472             return;
473         } else {
474             cb.run(mediaId, new ArrayList<ListItem>());
475         }
476     }
477 
478     @VisibleForTesting
addMediaPlayer(MediaController controller)479     int addMediaPlayer(MediaController controller) {
480         // Each new player has an ID of 1 plus the highest ID. The ID 0 is reserved to signify that
481         // there is no active player. If we already have a browsable player for the package, reuse
482         // that key.
483         String packageName = controller.getPackageName();
484         if (!havePlayerId(packageName)) {
485             mMediaPlayerIds.put(packageName, getFreeMediaPlayerId());
486         }
487 
488         int playerId = mMediaPlayerIds.get(packageName);
489 
490         // If we already have a controller for the package, then update it with this new controller
491         // as the old controller has probably gone stale.
492         if (haveMediaPlayer(playerId)) {
493             d("Already have a controller for the player: " + packageName + ", updating instead");
494             MediaPlayerWrapper player = mMediaPlayers.get(playerId);
495             player.updateMediaController(controller);
496 
497             // If the media controller we updated was the active player check if the media updated
498             if (playerId == mActivePlayerId) {
499                 sendMediaUpdate(getActivePlayer().getCurrentMediaData());
500             }
501 
502             return playerId;
503         }
504 
505         MediaPlayerWrapper newPlayer = MediaPlayerWrapperFactory.wrap(
506                 mContext,
507                 controller,
508                 mLooper);
509 
510         Log.i(TAG, "Adding wrapped media player: " + packageName + " at key: "
511                 + mMediaPlayerIds.get(controller.getPackageName()));
512 
513         mMediaPlayers.put(playerId, newPlayer);
514         return playerId;
515     }
516 
517     // Adds the controller to the MediaPlayerList or updates the controller if we already had
518     // a controller for a package. Returns the new ID of the controller where its added or its
519     // previous value if it already existed. Returns -1 if the controller passed in is invalid
addMediaPlayer(android.media.session.MediaController controller)520     int addMediaPlayer(android.media.session.MediaController controller) {
521         if (controller == null) {
522             e("Trying to add a null MediaController");
523             return -1;
524         }
525 
526         return addMediaPlayer(MediaControllerFactory.wrap(controller));
527     }
528 
havePlayerId(String packageName)529     boolean havePlayerId(String packageName) {
530         if (packageName == null) return false;
531         return mMediaPlayerIds.containsKey(packageName);
532     }
533 
haveMediaPlayer(String packageName)534     boolean haveMediaPlayer(String packageName) {
535         if (!havePlayerId(packageName)) return false;
536         int playerId = mMediaPlayerIds.get(packageName);
537         return mMediaPlayers.containsKey(playerId);
538     }
539 
haveMediaPlayer(int playerId)540     boolean haveMediaPlayer(int playerId) {
541         return mMediaPlayers.containsKey(playerId);
542     }
543 
haveMediaBrowser(int playerId)544     boolean haveMediaBrowser(int playerId) {
545         return mBrowsablePlayers.containsKey(playerId);
546     }
547 
removeMediaPlayer(int playerId)548     void removeMediaPlayer(int playerId) {
549         if (!haveMediaPlayer(playerId)) {
550             e("Trying to remove nonexistent media player: " + playerId);
551             return;
552         }
553 
554         // If we removed the active player, set no player as active until the Media Framework
555         // tells us otherwise
556         if (playerId == mActivePlayerId && playerId != NO_ACTIVE_PLAYER) {
557             getActivePlayer().unregisterCallback();
558             mActivePlayerId = NO_ACTIVE_PLAYER;
559             List<Metadata> queue = new ArrayList<Metadata>();
560             queue.add(Util.empty_data());
561             MediaData newData = new MediaData(
562                     Util.empty_data(),
563                     null,
564                     queue
565                 );
566 
567             sendMediaUpdate(newData);
568         }
569 
570         final MediaPlayerWrapper wrapper = mMediaPlayers.get(playerId);
571         d("Removing media player " + wrapper.getPackageName());
572         mMediaPlayers.remove(playerId);
573         if (!haveMediaBrowser(playerId)) {
574             d(wrapper.getPackageName() + " doesn't have a browse service. Recycle player ID.");
575             mMediaPlayerIds.remove(wrapper.getPackageName());
576         }
577         wrapper.cleanup();
578     }
579 
setActivePlayer(int playerId)580     void setActivePlayer(int playerId) {
581         if (!haveMediaPlayer(playerId)) {
582             e("Player doesn't exist in list(): " + playerId);
583             return;
584         }
585 
586         if (playerId == mActivePlayerId) {
587             Log.w(TAG, getActivePlayer().getPackageName() + " is already the active player");
588             return;
589         }
590 
591         if (mActivePlayerId != NO_ACTIVE_PLAYER) getActivePlayer().unregisterCallback();
592 
593         mActivePlayerId = playerId;
594         getActivePlayer().registerCallback(mMediaPlayerCallback);
595         mActivePlayerLogger.logd(TAG, "setActivePlayer(): setting player to "
596                 + getActivePlayer().getPackageName());
597 
598         // Ensure that metadata is synced on the new player
599         if (!getActivePlayer().isMetadataSynced()) {
600             Log.w(TAG, "setActivePlayer(): Metadata not synced on new player");
601             return;
602         }
603 
604         if (Utils.isPtsTestMode()) {
605             sendFolderUpdate(true, true, false);
606         }
607 
608         MediaData data = getActivePlayer().getCurrentMediaData();
609         if (mAudioPlaybackIsActive) {
610             data.state = mCurrMediaData.state;
611             Log.d(TAG, "setActivePlayer mAudioPlaybackIsActive=true, state=" + data.state);
612         }
613         sendMediaUpdate(data);
614     }
615 
616     // TODO (apanicke): Add logging for media key events in dumpsys
sendMediaKeyEvent(int key, boolean pushed)617     public void sendMediaKeyEvent(int key, boolean pushed) {
618         d("sendMediaKeyEvent: key=" + key + " pushed=" + pushed);
619         int action = pushed ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP;
620         KeyEvent event = new KeyEvent(action, AvrcpPassthrough.toKeyCode(key));
621         mAudioManager.dispatchMediaKeyEvent(event);
622     }
623 
sendFolderUpdate(boolean availablePlayers, boolean addressedPlayers, boolean uids)624     private void sendFolderUpdate(boolean availablePlayers, boolean addressedPlayers,
625             boolean uids) {
626         d("sendFolderUpdate");
627         if (mCallback == null) {
628             return;
629         }
630 
631         mCallback.run(availablePlayers, addressedPlayers, uids);
632     }
633 
sendMediaUpdate(MediaData data)634     private void sendMediaUpdate(MediaData data) {
635         d("sendMediaUpdate");
636         if (mCallback == null) {
637             return;
638         }
639 
640         // Always have items in the queue
641         if (data.queue.size() == 0) {
642             Log.i(TAG, "sendMediaUpdate: Creating a one item queue for a player with no queue");
643             data.queue.add(data.metadata);
644         }
645 
646         Log.d(TAG, "sendMediaUpdate state=" + data.state);
647         mCurrMediaData = data;
648         mCallback.run(data);
649     }
650 
651     private final MediaSessionManager.OnActiveSessionsChangedListener
652             mActiveSessionsChangedListener =
653             new MediaSessionManager.OnActiveSessionsChangedListener() {
654         @Override
655         public void onActiveSessionsChanged(
656                 List<android.media.session.MediaController> newControllers) {
657             synchronized (MediaPlayerList.this) {
658                 Log.v(TAG, "onActiveSessionsChanged: number of controllers: "
659                         + newControllers.size());
660                 if (newControllers.size() == 0) return;
661 
662                 // Apps are allowed to have multiple MediaControllers. If an app does have
663                 // multiple controllers then newControllers contains them in highest
664                 // priority order. Since we only want to keep the highest priority one,
665                 // we keep track of which controllers we updated and skip over ones
666                 // we've already looked at.
667                 HashSet<String> addedPackages = new HashSet<String>();
668 
669                 for (int i = 0; i < newControllers.size(); i++) {
670                     Log.d(TAG, "onActiveSessionsChanged: controller: "
671                             + newControllers.get(i).getPackageName());
672                     if (addedPackages.contains(newControllers.get(i).getPackageName())) {
673                         continue;
674                     }
675 
676                     addedPackages.add(newControllers.get(i).getPackageName());
677                     addMediaPlayer(newControllers.get(i));
678                 }
679             }
680         }
681     };
682 
683     // TODO (apanicke): Write a test that tests uninstalling the active session
684     private final BroadcastReceiver mPackageChangedBroadcastReceiver = new BroadcastReceiver() {
685         @Override
686         public void onReceive(Context context, Intent intent) {
687             String action = intent.getAction();
688             Log.v(TAG, "mPackageChangedBroadcastReceiver: action: " + action);
689 
690             if (action.equals(Intent.ACTION_PACKAGE_REMOVED)
691                     || action.equals(Intent.ACTION_PACKAGE_DATA_CLEARED)) {
692                 if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) return;
693 
694                 String packageName = intent.getData().getSchemeSpecificPart();
695                 if (haveMediaPlayer(packageName)) {
696                     removeMediaPlayer(mMediaPlayerIds.get(packageName));
697                 }
698             } else if (action.equals(Intent.ACTION_PACKAGE_ADDED)
699                     || action.equals(Intent.ACTION_PACKAGE_CHANGED)) {
700                 String packageName = intent.getData().getSchemeSpecificPart();
701                 if (packageName != null) {
702                     if (DEBUG) Log.d(TAG, "Name of package changed: " + packageName);
703                     // TODO (apanicke): Handle either updating or adding the new package.
704                     // Check if its browsable and send the UIDS changed to update the
705                     // root folder
706                 }
707             }
708         }
709     };
710 
updateMediaForAudioPlayback()711     void updateMediaForAudioPlayback() {
712         MediaData currMediaData = null;
713         PlaybackState currState = null;
714         if (getActivePlayer() == null) {
715             Log.d(TAG, "updateMediaForAudioPlayback: no active player");
716             PlaybackState.Builder builder = new PlaybackState.Builder()
717                     .setState(PlaybackState.STATE_STOPPED, 0L, 0L);
718             List<Metadata> queue = new ArrayList<Metadata>();
719             queue.add(Util.empty_data());
720             currMediaData = new MediaData(
721                     Util.empty_data(),
722                     builder.build(),
723                     queue
724                 );
725         } else {
726             currMediaData = getActivePlayer().getCurrentMediaData();
727             currState = currMediaData.state;
728         }
729 
730         if (currState != null
731                 && currState.getState() == PlaybackState.STATE_PLAYING) {
732             Log.i(TAG, "updateMediaForAudioPlayback: Active player is playing, drop it");
733             return;
734         }
735 
736         if (mAudioPlaybackIsActive) {
737             PlaybackState.Builder builder = new PlaybackState.Builder()
738                     .setState(PlaybackState.STATE_PLAYING,
739                         currState == null ? 0 : currState.getPosition(),
740                         1.0f);
741             currMediaData.state = builder.build();
742         }
743         mAudioPlaybackStateLogger.logd(TAG, "updateMediaForAudioPlayback: update state="
744                 + currMediaData.state);
745         sendMediaUpdate(currMediaData);
746     }
747 
748     @VisibleForTesting
injectAudioPlaybacActive(boolean isActive)749     void injectAudioPlaybacActive(boolean isActive) {
750         mAudioPlaybackIsActive = isActive;
751         updateMediaForAudioPlayback();
752     }
753 
754     private final AudioManager.AudioPlaybackCallback mAudioPlaybackCallback =
755             new AudioManager.AudioPlaybackCallback() {
756         @Override
757         public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs) {
758             if (configs == null) {
759                 return;
760             }
761             boolean isActive = false;
762             AudioPlaybackConfiguration activeConfig = null;
763             for (AudioPlaybackConfiguration config : configs) {
764                 if (config.isActive() && (config.getAudioAttributes().getUsage()
765                             == AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
766                         && (config.getAudioAttributes().getContentType()
767                             == AudioAttributes.CONTENT_TYPE_SPEECH)) {
768                     activeConfig = config;
769                     isActive = true;
770                 }
771             }
772             if (isActive != mAudioPlaybackIsActive) {
773                 mAudioPlaybackStateLogger.logd(DEBUG, TAG, "onPlaybackConfigChanged: "
774                         + (mAudioPlaybackIsActive ? "Active" : "Non-active") + " -> "
775                         + (isActive ? "Active" : "Non-active"));
776                 if (isActive) {
777                     mAudioPlaybackStateLogger.logd(DEBUG, TAG, "onPlaybackConfigChanged: "
778                             + "active config: " + activeConfig);
779                 }
780                 mAudioPlaybackIsActive = isActive;
781                 updateMediaForAudioPlayback();
782             }
783         }
784     };
785 
786     private final MediaPlayerWrapper.Callback mMediaPlayerCallback =
787             new MediaPlayerWrapper.Callback() {
788         @Override
789         public void mediaUpdatedCallback(MediaData data) {
790             if (data.metadata == null) {
791                 Log.d(TAG, "mediaUpdatedCallback(): metadata is null");
792                 return;
793             }
794 
795             if (data.state == null) {
796                 Log.w(TAG, "mediaUpdatedCallback(): Tried to update with null state");
797                 return;
798             }
799 
800             if (mAudioPlaybackIsActive && (data.state.getState() != PlaybackState.STATE_PLAYING)) {
801                 Log.d(TAG, "Some audio playbacks are still active, drop it");
802                 return;
803             }
804             sendMediaUpdate(data);
805         }
806 
807         @Override
808         public void sessionUpdatedCallback(String packageName) {
809             if (haveMediaPlayer(packageName)) {
810                 Log.d(TAG, "sessionUpdatedCallback(): packageName: " + packageName);
811                 removeMediaPlayer(mMediaPlayerIds.get(packageName));
812             }
813         }
814     };
815 
816     private final MediaSessionManager.OnMediaKeyEventSessionChangedListener
817             mMediaKeyEventSessionChangedListener =
818             new MediaSessionManager.OnMediaKeyEventSessionChangedListener() {
819                 @Override
820                 public void onMediaKeyEventSessionChanged(String packageName,
821                         MediaSession.Token token) {
822                     if (mMediaSessionManager == null) {
823                         Log.w(TAG, "onMediaKeyEventSessionChanged(): Unexpected callback "
824                                 + "from the MediaSessionManager, pkg" + packageName + ", token="
825                                 + token);
826                         return;
827                     }
828                     if (TextUtils.isEmpty(packageName)) {
829                         return;
830                     }
831                     if (token != null) {
832                         android.media.session.MediaController controller =
833                                 new android.media.session.MediaController(mContext, token);
834                         if (!haveMediaPlayer(controller.getPackageName())) {
835                             // Since we have a controller, we can try to to recover by adding the
836                             // player and then setting it as active.
837                             Log.w(TAG, "onMediaKeyEventSessionChanged(Token): Addressed Player "
838                                     + "changed to a player we didn't have a session for");
839                             addMediaPlayer(controller);
840                         }
841 
842                         Log.i(TAG, "onMediaKeyEventSessionChanged: token="
843                                 + controller.getPackageName());
844                         setActivePlayer(mMediaPlayerIds.get(controller.getPackageName()));
845                     } else {
846                         if (!haveMediaPlayer(packageName)) {
847                             e("onMediaKeyEventSessionChanged(PackageName): Media key event session "
848                                     + "changed to a player we don't have a session for");
849                             return;
850                         }
851 
852                         Log.i(TAG, "onMediaKeyEventSessionChanged: packageName=" + packageName);
853                         setActivePlayer(mMediaPlayerIds.get(packageName));
854                     }
855                 }
856             };
857 
858 
dump(StringBuilder sb)859     public void dump(StringBuilder sb) {
860         sb.append("List of MediaControllers: size=" + mMediaPlayers.size() + "\n");
861         for (int id : mMediaPlayers.keySet()) {
862             if (id == mActivePlayerId) {
863                 sb.append("<Active> ");
864             }
865             MediaPlayerWrapper player = mMediaPlayers.get(id);
866             sb.append("  Media Player " + id + ": " + player.getPackageName() + "\n");
867             sb.append(player.toString().replaceAll("(?m)^", "  "));
868             sb.append("\n");
869         }
870 
871         sb.append("List of Browsers: size=" + mBrowsablePlayers.size() + "\n");
872         for (BrowsedPlayerWrapper player : mBrowsablePlayers.values()) {
873             sb.append(player.toString().replaceAll("(?m)^", "  "));
874             sb.append("\n");
875         }
876 
877         mActivePlayerLogger.dump(sb);
878         sb.append("\n");
879         mAudioPlaybackStateLogger.dump(sb);
880         sb.append("\n");
881         // TODO (apanicke): Add last sent data
882     }
883 
e(String message)884     private static void e(String message) {
885         if (sTesting) {
886             Log.wtf(TAG, message);
887         } else {
888             Log.e(TAG, message);
889         }
890     }
891 
d(String message)892     private static void d(String message) {
893         if (DEBUG) {
894             Log.d(TAG, message);
895         }
896     }
897 }
898