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