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