| /* |
| * Copyright 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.bluetooth.avrcp; |
| |
| import android.annotation.NonNull; |
| import android.content.BroadcastReceiver; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.PackageManager; |
| import android.content.pm.ResolveInfo; |
| import android.media.session.MediaSession; |
| import android.media.session.MediaSessionManager; |
| import android.media.session.PlaybackState; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.util.Log; |
| import android.view.KeyEvent; |
| |
| import com.android.bluetooth.Utils; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * This class is directly responsible of maintaining the list of Browsable Players as well as |
| * the list of Addressable Players. This variation of the list doesn't actually list all the |
| * available players for a getAvailableMediaPlayers request. Instead it only reports one media |
| * player with ID=0 and all the other browsable players are folders in the root of that player. |
| * |
| * Changing the directory to a browsable player will allow you to traverse that player as normal. |
| * By only having one root player, we never have to send Addressed Player Changed notifications, |
| * UIDs Changed notifications, or Available Players Changed notifications. |
| * |
| * TODO (apanicke): Add non-browsable players as song items to the root folder. Selecting that |
| * player would effectively cause player switch by sending a play command to that player. |
| */ |
| public class MediaPlayerList { |
| private static final String TAG = "NewAvrcpMediaPlayerList"; |
| private static final boolean DEBUG = true; |
| static boolean sTesting = false; |
| |
| private static final String PACKAGE_SCHEME = "package"; |
| private static final int NO_ACTIVE_PLAYER = 0; |
| private static final int BLUETOOTH_PLAYER_ID = 0; |
| private static final String BLUETOOTH_PLAYER_NAME = "Bluetooth Player"; |
| |
| // mediaId's for the now playing list will be in the form of "NowPlayingId[XX]" where [XX] |
| // is the Queue ID for the requested item. |
| private static final String NOW_PLAYING_ID_PATTERN = Util.NOW_PLAYING_PREFIX + "([0-9]*)"; |
| |
| // mediaId's for folder browsing will be in the form of [XX][mediaid], where [XX] is a |
| // two digit representation of the player id and [mediaid] is the original media id as a |
| // string. |
| private static final String BROWSE_ID_PATTERN = "\\d\\d.*"; |
| |
| private Context mContext; |
| private Looper mLooper; // Thread all media player callbacks and timeouts happen on |
| private PackageManager mPackageManager; |
| private MediaSessionManager mMediaSessionManager; |
| |
| private Map<Integer, MediaPlayerWrapper> mMediaPlayers = |
| Collections.synchronizedMap(new HashMap<Integer, MediaPlayerWrapper>()); |
| private Map<String, Integer> mMediaPlayerIds = |
| Collections.synchronizedMap(new HashMap<String, Integer>()); |
| private Map<Integer, BrowsedPlayerWrapper> mBrowsablePlayers = |
| Collections.synchronizedMap(new HashMap<Integer, BrowsedPlayerWrapper>()); |
| private int mActivePlayerId = NO_ACTIVE_PLAYER; |
| |
| private AvrcpTargetService.ListCallback mCallback; |
| |
| interface MediaUpdateCallback { |
| void run(MediaData data); |
| } |
| |
| interface GetPlayerRootCallback { |
| void run(int playerId, boolean success, String rootId, int numItems); |
| } |
| |
| interface GetFolderItemsCallback { |
| void run(String parentId, List<ListItem> items); |
| } |
| |
| interface FolderUpdateCallback { |
| void run(boolean availablePlayers, boolean addressedPlayers, boolean uids); |
| } |
| |
| MediaPlayerList(Looper looper, Context context) { |
| Log.v(TAG, "Creating MediaPlayerList"); |
| |
| mLooper = looper; |
| mContext = context; |
| |
| // Register for intents where available players might have changed |
| IntentFilter pkgFilter = new IntentFilter(); |
| pkgFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); |
| pkgFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED); |
| pkgFilter.addAction(Intent.ACTION_PACKAGE_ADDED); |
| pkgFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); |
| pkgFilter.addDataScheme(PACKAGE_SCHEME); |
| context.registerReceiver(mPackageChangedBroadcastReceiver, pkgFilter); |
| |
| mMediaSessionManager = |
| (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE); |
| mMediaSessionManager.addOnActiveSessionsChangedListener( |
| mActiveSessionsChangedListener, null, new Handler(looper)); |
| mMediaSessionManager.setCallback(mButtonDispatchCallback, null); |
| } |
| |
| void init(AvrcpTargetService.ListCallback callback) { |
| Log.v(TAG, "Initializing MediaPlayerList"); |
| mCallback = callback; |
| |
| // Build the list of browsable players and afterwards, build the list of media players |
| Intent intent = new Intent(android.service.media.MediaBrowserService.SERVICE_INTERFACE); |
| List<ResolveInfo> playerList = |
| mContext |
| .getApplicationContext() |
| .getPackageManager() |
| .queryIntentServices(intent, PackageManager.MATCH_ALL); |
| |
| BrowsablePlayerConnector.connectToPlayers(mContext, mLooper, playerList, |
| (List<BrowsedPlayerWrapper> players) -> { |
| Log.i(TAG, "init: Browsable Player list size is " + players.size()); |
| |
| // Check to see if the list has been cleaned up before this completed |
| if (mMediaSessionManager == null) { |
| return; |
| } |
| |
| for (BrowsedPlayerWrapper wrapper : players) { |
| // Generate new id and add the browsable player |
| if (!mMediaPlayerIds.containsKey(wrapper.getPackageName())) { |
| mMediaPlayerIds.put(wrapper.getPackageName(), getFreeMediaPlayerId()); |
| } |
| |
| d("Adding Browser Wrapper for " + wrapper.getPackageName() + " with id " |
| + mMediaPlayerIds.get(wrapper.getPackageName())); |
| |
| mBrowsablePlayers.put(mMediaPlayerIds.get(wrapper.getPackageName()), wrapper); |
| |
| wrapper.getFolderItems(wrapper.getRootId(), |
| (int status, String mediaId, List<ListItem> results) -> { |
| d("Got the contents for: " + mediaId + " : num results=" |
| + results.size()); |
| }); |
| } |
| |
| // Construct the list of current players |
| d("Initializing list of current media players"); |
| List<android.media.session.MediaController> controllers = |
| mMediaSessionManager.getActiveSessions(null); |
| |
| for (android.media.session.MediaController controller : controllers) { |
| addMediaPlayer(controller); |
| } |
| |
| // If there were any active players and we don't already have one due to the Media |
| // Framework Callbacks then set the highest priority one to active |
| if (mActivePlayerId == 0 && mMediaPlayers.size() > 0) setActivePlayer(1); |
| }); |
| } |
| |
| void cleanup() { |
| mContext.unregisterReceiver(mPackageChangedBroadcastReceiver); |
| |
| mMediaSessionManager.removeOnActiveSessionsChangedListener(mActiveSessionsChangedListener); |
| mMediaSessionManager.setCallback(null, null); |
| mMediaSessionManager = null; |
| |
| mMediaPlayerIds.clear(); |
| |
| for (MediaPlayerWrapper player : mMediaPlayers.values()) { |
| player.cleanup(); |
| } |
| mMediaPlayers.clear(); |
| |
| for (BrowsedPlayerWrapper player : mBrowsablePlayers.values()) { |
| player.disconnect(); |
| } |
| mBrowsablePlayers.clear(); |
| } |
| |
| int getCurrentPlayerId() { |
| return BLUETOOTH_PLAYER_ID; |
| } |
| |
| int getFreeMediaPlayerId() { |
| int id = 0; |
| while (mMediaPlayerIds.containsValue(++id)) {} |
| return id; |
| } |
| |
| MediaPlayerWrapper getActivePlayer() { |
| return mMediaPlayers.get(mActivePlayerId); |
| } |
| |
| |
| |
| // In this case the displayed player is the Bluetooth Player, the number of items is equal |
| // to the number of players. The root ID will always be empty string in this case as well. |
| void getPlayerRoot(int playerId, GetPlayerRootCallback cb) { |
| cb.run(playerId, playerId == BLUETOOTH_PLAYER_ID, "", mBrowsablePlayers.size()); |
| } |
| |
| // Return the "Bluetooth Player" as the only player always |
| List<PlayerInfo> getMediaPlayerList() { |
| PlayerInfo info = new PlayerInfo(); |
| info.id = BLUETOOTH_PLAYER_ID; |
| info.name = BLUETOOTH_PLAYER_NAME; |
| info.browsable = true; |
| List<PlayerInfo> ret = new ArrayList<PlayerInfo>(); |
| ret.add(info); |
| |
| return ret; |
| } |
| |
| @NonNull |
| String getCurrentMediaId() { |
| final MediaPlayerWrapper player = getActivePlayer(); |
| if (player == null) return ""; |
| |
| final PlaybackState state = player.getPlaybackState(); |
| final List<Metadata> queue = player.getCurrentQueue(); |
| |
| // Disable the now playing list if the player doesn't have a queue or provide an active |
| // queue ID that can be used to determine the active song in the queue. |
| if (state == null |
| || state.getActiveQueueItemId() == MediaSession.QueueItem.UNKNOWN_ID |
| || queue.size() == 0) { |
| d("getCurrentMediaId: No active queue item Id sending empty mediaId: PlaybackState=" |
| + state); |
| return ""; |
| } |
| |
| return Util.NOW_PLAYING_PREFIX + state.getActiveQueueItemId(); |
| } |
| |
| @NonNull |
| Metadata getCurrentSongInfo() { |
| final MediaPlayerWrapper player = getActivePlayer(); |
| if (player == null) return Util.empty_data(); |
| |
| return player.getCurrentMetadata(); |
| } |
| |
| PlaybackState getCurrentPlayStatus() { |
| final MediaPlayerWrapper player = getActivePlayer(); |
| if (player == null) return null; |
| |
| return player.getPlaybackState(); |
| } |
| |
| @NonNull |
| List<Metadata> getNowPlayingList() { |
| // Only send the current song for the now playing if there is no active song. See |
| // |getCurrentMediaId()| for reasons why there might be no active song. |
| if (getCurrentMediaId().equals("")) { |
| List<Metadata> ret = new ArrayList<Metadata>(); |
| Metadata data = getCurrentSongInfo(); |
| data.mediaId = ""; |
| ret.add(data); |
| return ret; |
| } |
| |
| return getActivePlayer().getCurrentQueue(); |
| } |
| |
| void playItem(int playerId, boolean nowPlaying, String mediaId) { |
| if (nowPlaying) { |
| playNowPlayingItem(mediaId); |
| } else { |
| playFolderItem(mediaId); |
| } |
| } |
| |
| private void playNowPlayingItem(String mediaId) { |
| d("playNowPlayingItem: mediaId=" + mediaId); |
| |
| Pattern regex = Pattern.compile(NOW_PLAYING_ID_PATTERN); |
| Matcher m = regex.matcher(mediaId); |
| if (!m.find()) { |
| // This should never happen since we control the media ID's reported |
| Log.wtf(TAG, "playNowPlayingItem: Couldn't match mediaId to pattern: mediaId=" |
| + mediaId); |
| } |
| |
| long queueItemId = Long.parseLong(m.group(1)); |
| if (getActivePlayer() != null) { |
| getActivePlayer().playItemFromQueue(queueItemId); |
| } |
| } |
| |
| private void playFolderItem(String mediaId) { |
| d("playFolderItem: mediaId=" + mediaId); |
| |
| if (!mediaId.matches(BROWSE_ID_PATTERN)) { |
| // This should never happen since we control the media ID's reported |
| Log.wtf(TAG, "playFolderItem: mediaId didn't match pattern: mediaId=" + mediaId); |
| } |
| |
| int playerIndex = Integer.parseInt(mediaId.substring(0, 2)); |
| String itemId = mediaId.substring(2); |
| |
| if (!mBrowsablePlayers.containsKey(playerIndex)) { |
| e("playFolderItem: Do not have the a browsable player with ID " + playerIndex); |
| return; |
| } |
| |
| mBrowsablePlayers.get(playerIndex).playItem(itemId); |
| } |
| |
| void getFolderItemsMediaPlayerList(GetFolderItemsCallback cb) { |
| d("getFolderItemsMediaPlayerList: Sending Media Player list for root directory"); |
| |
| ArrayList<ListItem> playerList = new ArrayList<ListItem>(); |
| for (BrowsedPlayerWrapper player : mBrowsablePlayers.values()) { |
| |
| String displayName = Util.getDisplayName(mContext, player.getPackageName()); |
| int id = mMediaPlayerIds.get(player.getPackageName()); |
| |
| d("getFolderItemsMediaPlayerList: Adding player " + displayName); |
| Folder playerFolder = new Folder(String.format("%02d", id), false, displayName); |
| playerList.add(new ListItem(playerFolder)); |
| } |
| cb.run("", playerList); |
| return; |
| } |
| |
| void getFolderItems(int playerId, String mediaId, GetFolderItemsCallback cb) { |
| // The playerId is unused since we always assume the remote device is using the |
| // Bluetooth Player. |
| d("getFolderItems(): playerId=" + playerId + ", mediaId=" + mediaId); |
| |
| // The device is requesting the content of the root folder. This folder contains a list of |
| // Browsable Media Players displayed as folders with their contents contained within. |
| if (mediaId.equals("")) { |
| getFolderItemsMediaPlayerList(cb); |
| return; |
| } |
| |
| if (!mediaId.matches(BROWSE_ID_PATTERN)) { |
| // This should never happen since we control the media ID's reported |
| Log.wtf(TAG, "getFolderItems: mediaId didn't match pattern: mediaId=" + mediaId); |
| } |
| |
| int playerIndex = Integer.parseInt(mediaId.substring(0, 2)); |
| String itemId = mediaId.substring(2); |
| |
| // TODO (apanicke): Add timeouts for looking up folder items since media browsers don't |
| // have to respond. |
| if (mBrowsablePlayers.containsKey(playerIndex)) { |
| BrowsedPlayerWrapper wrapper = mBrowsablePlayers.get(playerIndex); |
| if (itemId.equals("")) { |
| Log.i(TAG, "Empty media id, getting the root for " |
| + wrapper.getPackageName()); |
| itemId = wrapper.getRootId(); |
| } |
| |
| wrapper.getFolderItems(itemId, (status, id, results) -> { |
| if (status != BrowsedPlayerWrapper.STATUS_SUCCESS) { |
| cb.run(mediaId, new ArrayList<ListItem>()); |
| return; |
| } |
| |
| String playerPrefix = String.format("%02d", playerIndex); |
| for (ListItem item : results) { |
| if (item.isFolder) { |
| item.folder.mediaId = playerPrefix.concat(item.folder.mediaId); |
| } else { |
| item.song.mediaId = playerPrefix.concat(item.song.mediaId); |
| } |
| } |
| cb.run(mediaId, results); |
| }); |
| return; |
| } else { |
| cb.run(mediaId, new ArrayList<ListItem>()); |
| } |
| } |
| |
| // Adds the controller to the MediaPlayerList or updates the controller if we already had |
| // a controller for a package. Returns the new ID of the controller where its added or its |
| // previous value if it already existed. Returns -1 if the controller passed in is invalid |
| int addMediaPlayer(android.media.session.MediaController controller) { |
| if (controller == null) return -1; |
| |
| // Each new player has an ID of 1 plus the highest ID. The ID 0 is reserved to signify that |
| // there is no active player. If we already have a browsable player for the package, reuse |
| // that key. |
| String packageName = controller.getPackageName(); |
| if (!mMediaPlayerIds.containsKey(packageName)) { |
| mMediaPlayerIds.put(packageName, getFreeMediaPlayerId()); |
| } |
| |
| int playerId = mMediaPlayerIds.get(packageName); |
| |
| // If we already have a controller for the package, then update it with this new controller |
| // as the old controller has probably gone stale. |
| if (mMediaPlayers.containsKey(playerId)) { |
| d("Already have a controller for the player: " + packageName + ", updating instead"); |
| MediaPlayerWrapper player = mMediaPlayers.get(playerId); |
| player.updateMediaController(MediaControllerFactory.wrap(controller)); |
| |
| // If the media controller we updated was the active player check if the media updated |
| if (playerId == mActivePlayerId) { |
| sendMediaUpdate(getActivePlayer().getCurrentMediaData()); |
| } |
| |
| return playerId; |
| } |
| |
| MediaPlayerWrapper newPlayer = MediaPlayerWrapper.wrap( |
| MediaControllerFactory.wrap(controller), |
| mLooper); |
| |
| Log.i(TAG, "Adding wrapped media player: " + packageName + " at key: " |
| + mMediaPlayerIds.get(controller.getPackageName())); |
| |
| mMediaPlayers.put(playerId, newPlayer); |
| return playerId; |
| } |
| |
| void removeMediaPlayer(int playerId) { |
| if (!mMediaPlayers.containsKey(playerId)) { |
| e("Trying to remove nonexistent media player: " + playerId); |
| return; |
| } |
| |
| // If we removed the active player, set no player as active until the Media Framework |
| // tells us otherwise |
| if (playerId == mActivePlayerId && playerId != NO_ACTIVE_PLAYER) { |
| getActivePlayer().unregisterCallback(); |
| mActivePlayerId = NO_ACTIVE_PLAYER; |
| } |
| |
| final MediaPlayerWrapper wrapper = mMediaPlayers.get(playerId); |
| d("Removing media player " + wrapper.getPackageName()); |
| mMediaPlayerIds.remove(wrapper.getPackageName()); |
| mMediaPlayers.remove(playerId); |
| wrapper.cleanup(); |
| } |
| |
| void setActivePlayer(int playerId) { |
| if (!mMediaPlayers.containsKey(playerId)) { |
| e("Player doesn't exist in list(): " + playerId); |
| return; |
| } |
| |
| if (playerId == mActivePlayerId) { |
| Log.w(TAG, getActivePlayer().getPackageName() + " is already the active player"); |
| return; |
| } |
| |
| if (mActivePlayerId != NO_ACTIVE_PLAYER) getActivePlayer().unregisterCallback(); |
| |
| mActivePlayerId = playerId; |
| getActivePlayer().registerCallback(mMediaPlayerCallback); |
| Log.i(TAG, "setActivePlayer(): setting player to " + getActivePlayer().getPackageName()); |
| |
| // Ensure that metadata is synced on the new player |
| if (!getActivePlayer().isMetadataSynced()) { |
| Log.w(TAG, "setActivePlayer(): Metadata not synced on new player"); |
| return; |
| } |
| |
| if (Utils.isPtsTestMode()) { |
| sendFolderUpdate(true, true, false); |
| } |
| |
| sendMediaUpdate(getActivePlayer().getCurrentMediaData()); |
| } |
| |
| // TODO (apanicke): Add logging for media key events in dumpsys |
| void sendMediaKeyEvent(int key, boolean pushed) { |
| d("sendMediaKeyEvent: key=" + key + " pushed=" + pushed); |
| int action = pushed ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP; |
| KeyEvent event = new KeyEvent(action, AvrcpPassthrough.toKeyCode(key)); |
| mMediaSessionManager.dispatchMediaKeyEvent(event); |
| } |
| |
| private void sendFolderUpdate(boolean availablePlayers, boolean addressedPlayers, |
| boolean uids) { |
| d("sendFolderUpdate"); |
| if (mCallback == null) { |
| return; |
| } |
| |
| mCallback.run(availablePlayers, addressedPlayers, uids); |
| } |
| |
| private void sendMediaUpdate(MediaData data) { |
| d("sendMediaUpdate"); |
| if (mCallback == null) { |
| return; |
| } |
| |
| // Always have items in the queue |
| if (data.queue.size() == 0) { |
| Log.i(TAG, "sendMediaUpdate: Creating a one item queue for a player with no queue"); |
| data.queue.add(data.metadata); |
| } |
| |
| mCallback.run(data); |
| } |
| |
| private final MediaSessionManager.OnActiveSessionsChangedListener |
| mActiveSessionsChangedListener = |
| new MediaSessionManager.OnActiveSessionsChangedListener() { |
| @Override |
| public void onActiveSessionsChanged( |
| List<android.media.session.MediaController> newControllers) { |
| synchronized (MediaPlayerList.this) { |
| Log.v(TAG, "onActiveSessionsChanged: number of controllers: " |
| + newControllers.size()); |
| if (newControllers.size() == 0) return; |
| |
| // Apps are allowed to have multiple MediaControllers. If an app does have |
| // multiple controllers then newControllers contains them in highest |
| // priority order. Since we only want to keep the highest priority one, |
| // we keep track of which controllers we updated and skip over ones |
| // we've already looked at. |
| HashSet<String> addedPackages = new HashSet<String>(); |
| |
| for (int i = 0; i < newControllers.size(); i++) { |
| Log.d(TAG, "onActiveSessionsChanged: controller: " |
| + newControllers.get(i).getPackageName()); |
| if (addedPackages.contains(newControllers.get(i).getPackageName())) { |
| continue; |
| } |
| |
| addedPackages.add(newControllers.get(i).getPackageName()); |
| addMediaPlayer(newControllers.get(i)); |
| } |
| } |
| } |
| }; |
| |
| // TODO (apanicke): Write a test that tests uninstalling the active session |
| private final BroadcastReceiver mPackageChangedBroadcastReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| String action = intent.getAction(); |
| Log.v(TAG, "mPackageChangedBroadcastReceiver: action: " + action); |
| |
| if (action.equals(Intent.ACTION_PACKAGE_REMOVED) |
| || action.equals(Intent.ACTION_PACKAGE_DATA_CLEARED)) { |
| if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) return; |
| |
| String packageName = intent.getData().getSchemeSpecificPart(); |
| if (packageName != null && mMediaPlayerIds.containsKey(packageName)) { |
| removeMediaPlayer(mMediaPlayerIds.get(packageName)); |
| } |
| } else if (action.equals(Intent.ACTION_PACKAGE_ADDED) |
| || action.equals(Intent.ACTION_PACKAGE_CHANGED)) { |
| String packageName = intent.getData().getSchemeSpecificPart(); |
| if (packageName != null) { |
| if (DEBUG) Log.d(TAG, "Name of package changed: " + packageName); |
| // TODO (apanicke): Handle either updating or adding the new package. |
| // Check if its browsable and send the UIDS changed to update the |
| // root folder |
| } |
| } |
| } |
| }; |
| |
| private final MediaPlayerWrapper.Callback mMediaPlayerCallback = |
| new MediaPlayerWrapper.Callback() { |
| @Override |
| public void mediaUpdatedCallback(MediaData data) { |
| if (data.metadata == null) { |
| Log.d(TAG, "mediaUpdatedCallback(): metadata is null"); |
| return; |
| } |
| |
| if (data.state == null) { |
| Log.w(TAG, "mediaUpdatedCallback(): Tried to update with null state"); |
| return; |
| } |
| |
| sendMediaUpdate(data); |
| } |
| }; |
| |
| private final MediaSessionManager.Callback mButtonDispatchCallback = |
| new MediaSessionManager.Callback() { |
| @Override |
| public void onMediaKeyEventDispatched(KeyEvent event, MediaSession.Token token) { |
| // TODO (apanicke): Add logging for these |
| } |
| |
| @Override |
| public void onMediaKeyEventDispatched(KeyEvent event, ComponentName receiver) { |
| // TODO (apanicke): Add logging for these |
| } |
| |
| @Override |
| public void onAddressedPlayerChanged(MediaSession.Token token) { |
| android.media.session.MediaController controller = |
| new android.media.session.MediaController(mContext, token); |
| |
| if (!mMediaPlayerIds.containsKey(controller.getPackageName())) { |
| // Since we have a controller, we can try to to recover by adding the |
| // player and then setting it as active. |
| Log.w(TAG, "onAddressedPlayerChanged(Token): Addressed Player " |
| + "changed to a player we didn't have a session for"); |
| addMediaPlayer(controller); |
| } |
| |
| Log.i(TAG, "onAddressedPlayerChanged: token=" + controller.getPackageName()); |
| setActivePlayer(mMediaPlayerIds.get(controller.getPackageName())); |
| } |
| |
| @Override |
| public void onAddressedPlayerChanged(ComponentName receiver) { |
| if (receiver == null) { |
| return; |
| } |
| |
| if (!mMediaPlayerIds.containsKey(receiver.getPackageName())) { |
| e("onAddressedPlayerChanged(Component): Addressed Player " |
| + "changed to a player we don't have a session for"); |
| return; |
| } |
| |
| Log.i(TAG, "onAddressedPlayerChanged: component=" + receiver.getPackageName()); |
| setActivePlayer(mMediaPlayerIds.get(receiver.getPackageName())); |
| } |
| }; |
| |
| |
| void dump(StringBuilder sb) { |
| sb.append("List of MediaControllers: size=" + mMediaPlayers.size() + "\n"); |
| for (int id : mMediaPlayers.keySet()) { |
| if (id == mActivePlayerId) { |
| sb.append("<Active> "); |
| } |
| MediaPlayerWrapper player = mMediaPlayers.get(id); |
| sb.append(" Media Player " + id + ": " + player.getPackageName() + "\n"); |
| sb.append(player.toString().replaceAll("(?m)^", " ")); |
| sb.append("\n"); |
| } |
| |
| sb.append("List of Browsers: size=" + mBrowsablePlayers.size() + "\n"); |
| for (BrowsedPlayerWrapper player : mBrowsablePlayers.values()) { |
| sb.append(player.toString().replaceAll("(?m)^", " ")); |
| sb.append("\n"); |
| } |
| // TODO (apanicke): Add media key events |
| // TODO (apanicke): Add last sent data |
| // TODO (apanicke): Add addressed player history |
| } |
| |
| private static void e(String message) { |
| if (sTesting) { |
| Log.wtfStack(TAG, message); |
| } else { |
| Log.e(TAG, message); |
| } |
| } |
| |
| private static void d(String message) { |
| if (DEBUG) { |
| Log.d(TAG, message); |
| } |
| } |
| } |