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