/* * Copyright (C) 2016 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.avrcpcontroller; import static android.Manifest.permission.BLUETOOTH_CONNECT; import android.bluetooth.BluetoothAvrcpController; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.content.Context; import android.content.Intent; import android.media.AudioManager; import android.net.Uri; import android.os.Bundle; import android.os.Message; import android.support.v4.media.MediaBrowserCompat.MediaItem; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.util.Log; import android.util.SparseArray; import com.android.bluetooth.BluetoothMetricsProto; import com.android.bluetooth.R; import com.android.bluetooth.Utils; import com.android.bluetooth.a2dpsink.A2dpSinkService; import com.android.bluetooth.btservice.MetricsLogger; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.statemachine.State; import com.android.bluetooth.statemachine.StateMachine; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.List; import java.util.Set; /** * Provides Bluetooth AVRCP Controller State Machine responsible for all remote control connections * and interactions with a remote controlable device. */ class AvrcpControllerStateMachine extends StateMachine { static final String TAG = "AvrcpControllerStateMachine"; static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); //0->99 Events from Outside public static final int CONNECT = 1; public static final int DISCONNECT = 2; public static final int ACTIVE_DEVICE_CHANGE = 3; //100->199 Internal Events protected static final int CLEANUP = 100; private static final int CONNECT_TIMEOUT = 101; static final int MESSAGE_INTERNAL_ABS_VOL_TIMEOUT = 102; //200->299 Events from Native static final int STACK_EVENT = 200; static final int MESSAGE_INTERNAL_CMD_TIMEOUT = 201; static final int MESSAGE_PROCESS_SET_ABS_VOL_CMD = 203; static final int MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION = 204; static final int MESSAGE_PROCESS_TRACK_CHANGED = 205; static final int MESSAGE_PROCESS_PLAY_POS_CHANGED = 206; static final int MESSAGE_PROCESS_PLAY_STATUS_CHANGED = 207; static final int MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION = 208; static final int MESSAGE_PROCESS_GET_FOLDER_ITEMS = 209; static final int MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE = 210; static final int MESSAGE_PROCESS_GET_PLAYER_ITEMS = 211; static final int MESSAGE_PROCESS_FOLDER_PATH = 212; static final int MESSAGE_PROCESS_SET_BROWSED_PLAYER = 213; static final int MESSAGE_PROCESS_SET_ADDRESSED_PLAYER = 214; static final int MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED = 215; static final int MESSAGE_PROCESS_NOW_PLAYING_CONTENTS_CHANGED = 216; static final int MESSAGE_PROCESS_SUPPORTED_APPLICATION_SETTINGS = 217; static final int MESSAGE_PROCESS_CURRENT_APPLICATION_SETTINGS = 218; static final int MESSAGE_PROCESS_AVAILABLE_PLAYER_CHANGED = 219; static final int MESSAGE_PROCESS_RECEIVED_COVER_ART_PSM = 220; //300->399 Events for Browsing static final int MESSAGE_GET_FOLDER_ITEMS = 300; static final int MESSAGE_PLAY_ITEM = 301; static final int MSG_AVRCP_PASSTHRU = 302; static final int MSG_AVRCP_SET_SHUFFLE = 303; static final int MSG_AVRCP_SET_REPEAT = 304; //400->499 Events for Cover Artwork static final int MESSAGE_PROCESS_IMAGE_DOWNLOADED = 400; /* * Base value for absolute volume from JNI */ private static final int ABS_VOL_BASE = 127; /* * Notification types for Avrcp protocol JNI. */ private static final byte NOTIFICATION_RSP_TYPE_INTERIM = 0x00; private static final byte NOTIFICATION_RSP_TYPE_CHANGED = 0x01; private static BluetoothDevice sActiveDevice; private final AudioManager mAudioManager; private final boolean mIsVolumeFixed; protected final BluetoothDevice mDevice; protected final byte[] mDeviceAddress; protected final AvrcpControllerService mService; protected int mCoverArtPsm; protected final AvrcpCoverArtManager mCoverArtManager; protected final Disconnected mDisconnected; protected final Connecting mConnecting; protected final Connected mConnected; protected final Disconnecting mDisconnecting; protected int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED; boolean mRemoteControlConnected = false; boolean mBrowsingConnected = false; final BrowseTree mBrowseTree; private AvrcpPlayer mAddressedPlayer; private int mAddressedPlayerId; private SparseArray mAvailablePlayerList; private int mVolumeChangedNotificationsToIgnore = 0; private int mVolumeNotificationLabel = -1; GetFolderList mGetFolderList = null; //Number of items to get in a single fetch static final int ITEM_PAGE_SIZE = 20; static final int CMD_TIMEOUT_MILLIS = 10000; static final int ABS_VOL_TIMEOUT_MILLIS = 1000; //1s AvrcpControllerStateMachine(BluetoothDevice device, AvrcpControllerService service) { super(TAG); mDevice = device; mDeviceAddress = Utils.getByteAddress(mDevice); mService = service; mCoverArtPsm = 0; mCoverArtManager = service.getCoverArtManager(); logD(device.toString()); mAvailablePlayerList = new SparseArray(); mAddressedPlayerId = AvrcpPlayer.DEFAULT_ID; AvrcpPlayer.Builder apb = new AvrcpPlayer.Builder(); apb.setDevice(mDevice); apb.setPlayerId(mAddressedPlayerId); apb.setSupportedFeature(AvrcpPlayer.FEATURE_PLAY); apb.setSupportedFeature(AvrcpPlayer.FEATURE_PAUSE); apb.setSupportedFeature(AvrcpPlayer.FEATURE_STOP); apb.setSupportedFeature(AvrcpPlayer.FEATURE_FORWARD); apb.setSupportedFeature(AvrcpPlayer.FEATURE_PREVIOUS); mAddressedPlayer = apb.build(); mAvailablePlayerList.put(mAddressedPlayerId, mAddressedPlayer); mBrowseTree = new BrowseTree(mDevice); mDisconnected = new Disconnected(); mConnecting = new Connecting(); mConnected = new Connected(); mDisconnecting = new Disconnecting(); addState(mDisconnected); addState(mConnecting); addState(mConnected); addState(mDisconnecting); mGetFolderList = new GetFolderList(); addState(mGetFolderList, mConnected); mAudioManager = (AudioManager) service.getSystemService(Context.AUDIO_SERVICE); mIsVolumeFixed = mAudioManager.isVolumeFixed(); setInitialState(mDisconnected); } BrowseTree.BrowseNode findNode(String parentMediaId) { logD("FindNode"); return mBrowseTree.findBrowseNodeByID(parentMediaId); } /** * Get the current connection state * * @return current State */ public int getState() { return mMostRecentState; } /** * Get the underlying device tracked by this state machine * * @return device in focus */ public BluetoothDevice getDevice() { return mDevice; } /** * send the connection event asynchronously */ public boolean connect(StackEvent event) { if (event.mBrowsingConnected) { onBrowsingConnected(); } mRemoteControlConnected = event.mRemoteControlConnected; sendMessage(CONNECT); return true; } /** * send the Disconnect command asynchronously */ public void disconnect() { sendMessage(DISCONNECT); } /** * Get the current playing track */ public AvrcpItem getCurrentTrack() { return mAddressedPlayer.getCurrentTrack(); } @VisibleForTesting int getAddressedPlayerId() { return mAddressedPlayerId; } @VisibleForTesting SparseArray getAvailablePlayers() { return mAvailablePlayerList; } /** * Dump the current State Machine to the string builder. * * @param sb output string */ public void dump(StringBuilder sb) { ProfileService.println(sb, "mDevice: " + mDevice.getAddress() + "(" + Utils.getName(mDevice) + ") " + this.toString()); ProfileService.println(sb, "isActive: " + isActive()); ProfileService.println(sb, "Control: " + mRemoteControlConnected); ProfileService.println(sb, "Browsing: " + mBrowsingConnected); ProfileService.println(sb, "Cover Art: " + (mCoverArtManager.getState(mDevice) == BluetoothProfile.STATE_CONNECTED)); ProfileService.println(sb, "Addressed Player ID: " + mAddressedPlayerId); ProfileService.println(sb, "Available Players (" + mAvailablePlayerList.size() + "): "); for (int i = 0; i < mAvailablePlayerList.size(); i++) { AvrcpPlayer player = mAvailablePlayerList.valueAt(i); boolean isAddressed = (player.getId() == mAddressedPlayerId); ProfileService.println(sb, "\t" + (isAddressed ? "(Addressed) " : "") + player); } List queue = null; if (mBrowseTree.mNowPlayingNode != null) { queue = mBrowseTree.mNowPlayingNode.getContents(); } ProfileService.println(sb, "Queue (" + (queue == null ? 0 : queue.size()) + "): " + queue); } @VisibleForTesting boolean isActive() { return mDevice.equals(mService.getActiveDevice()); } /** * Attempt to set the active status for this device */ public void setDeviceState(int state) { sendMessage(ACTIVE_DEVICE_CHANGE, state); } @Override protected void unhandledMessage(Message msg) { Log.w(TAG, "Unhandled message in state " + getCurrentState() + "msg.what=" + msg.what); } private static void logD(String message) { if (DBG) { Log.d(TAG, message); } } synchronized void onBrowsingConnected() { mBrowsingConnected = true; requestContents(mBrowseTree.mRootNode); } synchronized void onBrowsingDisconnected() { if (!mBrowsingConnected) return; mAddressedPlayer.setPlayStatus(PlaybackStateCompat.STATE_ERROR); AvrcpItem previousTrack = mAddressedPlayer.getCurrentTrack(); String previousTrackUuid = previousTrack != null ? previousTrack.getCoverArtUuid() : null; mAddressedPlayer.updateCurrentTrack(null); mBrowseTree.mNowPlayingNode.setCached(false); mBrowseTree.mRootNode.setCached(false); if (isActive()) { BluetoothMediaBrowserService.notifyChanged(mBrowseTree.mNowPlayingNode); BluetoothMediaBrowserService.notifyChanged(mBrowseTree.mRootNode); } removeUnusedArtwork(previousTrackUuid); removeUnusedArtworkFromBrowseTree(); mBrowsingConnected = false; } synchronized void connectCoverArt() { // Called from "connected" state, which assumes either control or browse is connected if (mCoverArtManager != null && mCoverArtPsm != 0 && mCoverArtManager.getState(mDevice) != BluetoothProfile.STATE_CONNECTED) { logD("Attempting to connect to AVRCP BIP, psm: " + mCoverArtPsm); mCoverArtManager.connect(mDevice, /* psm */ mCoverArtPsm); } } synchronized void refreshCoverArt() { if (mCoverArtManager != null && mCoverArtPsm != 0 && mCoverArtManager.getState(mDevice) == BluetoothProfile.STATE_CONNECTED) { logD("Attempting to refresh AVRCP BIP OBEX session, psm: " + mCoverArtPsm); mCoverArtManager.refreshSession(mDevice); } } synchronized void disconnectCoverArt() { // Safe to call even if we're not connected if (mCoverArtManager != null) { logD("Disconnect BIP cover artwork"); mCoverArtManager.disconnect(mDevice); } } /** * Remove an unused cover art image from storage if it's unused by the browse tree and the * current track. */ synchronized void removeUnusedArtwork(String previousTrackUuid) { logD("removeUnusedArtwork(" + previousTrackUuid + ")"); if (mCoverArtManager == null) return; AvrcpItem currentTrack = getCurrentTrack(); String currentTrackUuid = currentTrack != null ? currentTrack.getCoverArtUuid() : null; if (previousTrackUuid != null) { if (!previousTrackUuid.equals(currentTrackUuid) && mBrowseTree.getNodesUsingCoverArt(previousTrackUuid).isEmpty()) { mCoverArtManager.removeImage(mDevice, previousTrackUuid); } } } /** * Queries the browse tree for unused uuids and removes the associated images from storage * if the uuid is not used by the current track. */ synchronized void removeUnusedArtworkFromBrowseTree() { logD("removeUnusedArtworkFromBrowseTree()"); if (mCoverArtManager == null) return; AvrcpItem currentTrack = getCurrentTrack(); String currentTrackUuid = currentTrack != null ? currentTrack.getCoverArtUuid() : null; ArrayList unusedArtwork = mBrowseTree.getAndClearUnusedCoverArt(); for (String uuid : unusedArtwork) { if (!uuid.equals(currentTrackUuid)) { mCoverArtManager.removeImage(mDevice, uuid); } } } private void notifyChanged(BrowseTree.BrowseNode node) { // We should only notify now playing content updates if we're the active device. VFS // updates are fine at any time int scope = node.getScope(); if (scope != AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING || (scope == AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING && isActive())) { BluetoothMediaBrowserService.notifyChanged(node); } } private void notifyChanged(PlaybackStateCompat state) { if (isActive()) { BluetoothMediaBrowserService.notifyChanged(state); } } void requestContents(BrowseTree.BrowseNode node) { sendMessage(MESSAGE_GET_FOLDER_ITEMS, node); logD("Fetching " + node); } public void playItem(BrowseTree.BrowseNode node) { sendMessage(MESSAGE_PLAY_ITEM, node); } void nowPlayingContentChanged() { mBrowseTree.mNowPlayingNode.setCached(false); removeUnusedArtworkFromBrowseTree(); sendMessage(MESSAGE_GET_FOLDER_ITEMS, mBrowseTree.mNowPlayingNode); } protected class Disconnected extends State { @Override public void enter() { logD("Enter Disconnected"); if (mMostRecentState != BluetoothProfile.STATE_DISCONNECTED) { sendMessage(CLEANUP); } broadcastConnectionStateChanged(BluetoothProfile.STATE_DISCONNECTED); } @Override public boolean processMessage(Message message) { switch (message.what) { case MESSAGE_PROCESS_RECEIVED_COVER_ART_PSM: mCoverArtPsm = message.arg1; break; case CONNECT: logD("Connect"); transitionTo(mConnecting); break; case CLEANUP: mService.removeStateMachine(AvrcpControllerStateMachine.this); break; case ACTIVE_DEVICE_CHANGE: // Wait until we're connected to process this deferMessage(message); break; } return true; } } protected class Connecting extends State { @Override public void enter() { logD("Enter Connecting"); broadcastConnectionStateChanged(BluetoothProfile.STATE_CONNECTING); transitionTo(mConnected); } } class Connected extends State { private static final String STATE_TAG = "Avrcp.ConnectedAvrcpController"; private int mCurrentlyHeldKey = 0; @Override public void enter() { if (mMostRecentState == BluetoothProfile.STATE_CONNECTING) { broadcastConnectionStateChanged(BluetoothProfile.STATE_CONNECTED); mService.sBrowseTree.mRootNode.addChild(mBrowseTree.mRootNode); BluetoothMediaBrowserService.notifyChanged(mService.sBrowseTree.mRootNode); connectCoverArt(); // only works if we have a valid PSM } else { logD("ReEnteringConnected"); } super.enter(); } @Override public boolean processMessage(Message msg) { logD(STATE_TAG + " processMessage " + msg.what); switch (msg.what) { case ACTIVE_DEVICE_CHANGE: int state = msg.arg1; if (state == AvrcpControllerService.DEVICE_STATE_ACTIVE) { BluetoothMediaBrowserService.addressedPlayerChanged(mSessionCallbacks); BluetoothMediaBrowserService.trackChanged( mAddressedPlayer.getCurrentTrack()); BluetoothMediaBrowserService.notifyChanged( mAddressedPlayer.getPlaybackState()); BluetoothMediaBrowserService.notifyChanged(mBrowseTree.mNowPlayingNode); } else { sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE); } return true; case MESSAGE_PROCESS_SET_ABS_VOL_CMD: mVolumeChangedNotificationsToIgnore++; removeMessages(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT); sendMessageDelayed(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT, ABS_VOL_TIMEOUT_MILLIS); handleAbsVolumeRequest(msg.arg1, msg.arg2); return true; case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION: mVolumeNotificationLabel = msg.arg1; mService.sendRegisterAbsVolRspNative(mDeviceAddress, NOTIFICATION_RSP_TYPE_INTERIM, getAbsVolume(), mVolumeNotificationLabel); return true; case MESSAGE_GET_FOLDER_ITEMS: transitionTo(mGetFolderList); return true; case MESSAGE_PLAY_ITEM: //Set Addressed Player processPlayItem((BrowseTree.BrowseNode) msg.obj); return true; case MSG_AVRCP_PASSTHRU: passThru(msg.arg1); return true; case MSG_AVRCP_SET_REPEAT: setRepeat(msg.arg1); return true; case MSG_AVRCP_SET_SHUFFLE: setShuffle(msg.arg1); return true; case MESSAGE_PROCESS_TRACK_CHANGED: AvrcpItem track = (AvrcpItem) msg.obj; AvrcpItem previousTrack = mAddressedPlayer.getCurrentTrack(); downloadImageIfNeeded(track); mAddressedPlayer.updateCurrentTrack(track); if (isActive()) { BluetoothMediaBrowserService.trackChanged(track); } if (previousTrack != null) { removeUnusedArtwork(previousTrack.getCoverArtUuid()); removeUnusedArtworkFromBrowseTree(); } return true; case MESSAGE_PROCESS_PLAY_STATUS_CHANGED: mAddressedPlayer.setPlayStatus(msg.arg1); if (!isActive()) { sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE); return true; } PlaybackStateCompat playbackState = mAddressedPlayer.getPlaybackState(); BluetoothMediaBrowserService.notifyChanged(playbackState); int focusState = AudioManager.ERROR; A2dpSinkService a2dpSinkService = A2dpSinkService.getA2dpSinkService(); if (a2dpSinkService != null) { focusState = a2dpSinkService.getFocusState(); } if (focusState == AudioManager.ERROR) { sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE); return true; } if (playbackState.getState() == PlaybackStateCompat.STATE_PLAYING && focusState == AudioManager.AUDIOFOCUS_NONE) { if (shouldRequestFocus()) { mSessionCallbacks.onPrepare(); } else { sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE); } } return true; case MESSAGE_PROCESS_PLAY_POS_CHANGED: if (msg.arg2 != -1) { mAddressedPlayer.setPlayTime(msg.arg2); notifyChanged(mAddressedPlayer.getPlaybackState()); } return true; case MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED: int oldAddressedPlayerId = mAddressedPlayerId; mAddressedPlayerId = msg.arg1; logD("AddressedPlayer changed " + oldAddressedPlayerId + " -> " + mAddressedPlayerId); // The now playing list is tied to the addressed player by specification in // AVRCP 5.9.1. A new addressed player means our now playing content is now // invalid mBrowseTree.mNowPlayingNode.setCached(false); if (isActive()) { BluetoothMediaBrowserService.notifyChanged(mBrowseTree.mNowPlayingNode); } removeUnusedArtworkFromBrowseTree(); // For devices that support browsing, we *may* have an AvrcpPlayer with player // metadata already. We could also be in the middle fetching it. If the player // isn't there then we need to ensure that a default Addressed AvrcpPlayer is // created to represent it. It can be updated if/when we do fetch the player. if (!mAvailablePlayerList.contains(mAddressedPlayerId)) { logD("Available player set does not contain the new Addressed Player"); AvrcpPlayer.Builder apb = new AvrcpPlayer.Builder(); apb.setDevice(mDevice); apb.setPlayerId(mAddressedPlayerId); apb.setSupportedFeature(AvrcpPlayer.FEATURE_PLAY); apb.setSupportedFeature(AvrcpPlayer.FEATURE_PAUSE); apb.setSupportedFeature(AvrcpPlayer.FEATURE_STOP); apb.setSupportedFeature(AvrcpPlayer.FEATURE_FORWARD); apb.setSupportedFeature(AvrcpPlayer.FEATURE_PREVIOUS); mAvailablePlayerList.put(mAddressedPlayerId, apb.build()); } // Set our new addressed player object from our set of available players that's // guaranteed to have the addressed player now. mAddressedPlayer = mAvailablePlayerList.get(mAddressedPlayerId); // Fetch metadata including the now playing list if the new player supports the // now playing feature mService.getCurrentMetadataNative(Utils.getByteAddress(mDevice)); mService.getPlaybackStateNative(Utils.getByteAddress(mDevice)); if (mAddressedPlayer.supportsFeature(AvrcpPlayer.FEATURE_NOW_PLAYING)) { sendMessage(MESSAGE_GET_FOLDER_ITEMS, mBrowseTree.mNowPlayingNode); } logD("AddressedPlayer = " + mAddressedPlayer); return true; case MESSAGE_PROCESS_SUPPORTED_APPLICATION_SETTINGS: mAddressedPlayer.setSupportedPlayerApplicationSettings( (PlayerApplicationSettings) msg.obj); notifyChanged(mAddressedPlayer.getPlaybackState()); return true; case MESSAGE_PROCESS_CURRENT_APPLICATION_SETTINGS: mAddressedPlayer.setCurrentPlayerApplicationSettings( (PlayerApplicationSettings) msg.obj); notifyChanged(mAddressedPlayer.getPlaybackState()); return true; case MESSAGE_PROCESS_AVAILABLE_PLAYER_CHANGED: processAvailablePlayerChanged(); return true; case MESSAGE_PROCESS_RECEIVED_COVER_ART_PSM: mCoverArtPsm = msg.arg1; connectCoverArt(); return true; case MESSAGE_PROCESS_IMAGE_DOWNLOADED: AvrcpCoverArtManager.DownloadEvent event = (AvrcpCoverArtManager.DownloadEvent) msg.obj; String uuid = event.getUuid(); Uri uri = event.getUri(); logD("Received image for " + uuid + " at " + uri.toString()); // Let the addressed player know we got an image so it can see if the current // track now has cover artwork boolean addedArtwork = mAddressedPlayer.notifyImageDownload(uuid, uri); if (addedArtwork && isActive()) { BluetoothMediaBrowserService.trackChanged( mAddressedPlayer.getCurrentTrack()); } // Let the browse tree know of the newly downloaded image so it can attach it to // all the items that need it. Notify of changed nodes accordingly Set nodes = mBrowseTree.notifyImageDownload(uuid, uri); for (BrowseTree.BrowseNode node : nodes) { notifyChanged(node); } // Delete images that were downloaded and entirely unused if (!addedArtwork && nodes.isEmpty()) { removeUnusedArtwork(uuid); removeUnusedArtworkFromBrowseTree(); } return true; case DISCONNECT: transitionTo(mDisconnecting); return true; default: return super.processMessage(msg); } } private void processPlayItem(BrowseTree.BrowseNode node) { if (node == null) { Log.w(TAG, "Invalid item to play"); } else { mService.playItemNative( mDeviceAddress, node.getScope(), node.getBluetoothID(), 0); } } private synchronized void passThru(int cmd) { logD("msgPassThru " + cmd); // Some keys should be held until the next event. if (mCurrentlyHeldKey != 0) { mService.sendPassThroughCommandNative( mDeviceAddress, mCurrentlyHeldKey, AvrcpControllerService.KEY_STATE_RELEASED); if (mCurrentlyHeldKey == cmd) { // Return to prevent starting FF/FR operation again mCurrentlyHeldKey = 0; return; } else { // FF/FR is in progress and other operation is desired // so after stopping FF/FR, not returning so that command // can be sent for the desired operation. mCurrentlyHeldKey = 0; } } // Send the pass through. mService.sendPassThroughCommandNative(mDeviceAddress, cmd, AvrcpControllerService.KEY_STATE_PRESSED); if (isHoldableKey(cmd)) { // Release cmd next time a command is sent. mCurrentlyHeldKey = cmd; } else { mService.sendPassThroughCommandNative(mDeviceAddress, cmd, AvrcpControllerService.KEY_STATE_RELEASED); } } private boolean isHoldableKey(int cmd) { return (cmd == AvrcpControllerService.PASS_THRU_CMD_ID_REWIND) || (cmd == AvrcpControllerService.PASS_THRU_CMD_ID_FF); } private void setRepeat(int repeatMode) { mService.setPlayerApplicationSettingValuesNative(mDeviceAddress, (byte) 1, new byte[]{PlayerApplicationSettings.REPEAT_STATUS}, new byte[]{ PlayerApplicationSettings.mapAvrcpPlayerSettingstoBTattribVal( PlayerApplicationSettings.REPEAT_STATUS, repeatMode)}); } private void setShuffle(int shuffleMode) { mService.setPlayerApplicationSettingValuesNative(mDeviceAddress, (byte) 1, new byte[]{PlayerApplicationSettings.SHUFFLE_STATUS}, new byte[]{ PlayerApplicationSettings.mapAvrcpPlayerSettingstoBTattribVal( PlayerApplicationSettings.SHUFFLE_STATUS, shuffleMode)}); } private void processAvailablePlayerChanged() { logD("processAvailablePlayerChanged"); mBrowseTree.mRootNode.setCached(false); mBrowseTree.mRootNode.setExpectedChildren(255); BluetoothMediaBrowserService.notifyChanged(mBrowseTree.mRootNode); removeUnusedArtworkFromBrowseTree(); requestContents(mBrowseTree.mRootNode); } } // Handle the get folder listing action // a) Fetch the listing of folders // b) Once completed return the object listing class GetFolderList extends State { private static final String STATE_TAG = "Avrcp.GetFolderList"; boolean mAbort; BrowseTree.BrowseNode mBrowseNode; BrowseTree.BrowseNode mNextStep; @Override public void enter() { logD(STATE_TAG + " Entering GetFolderList"); // Setup the timeouts. sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS); super.enter(); mAbort = false; Message msg = getCurrentMessage(); if (msg.what == MESSAGE_GET_FOLDER_ITEMS) { { logD(STATE_TAG + " new Get Request"); mBrowseNode = (BrowseTree.BrowseNode) msg.obj; } } if (mBrowseNode == null) { transitionTo(mConnected); } else { navigateToFolderOrRetrieve(mBrowseNode); } } @Override public boolean processMessage(Message msg) { logD(STATE_TAG + " processMessage " + msg.what); switch (msg.what) { case MESSAGE_PROCESS_GET_FOLDER_ITEMS: ArrayList folderList = (ArrayList) msg.obj; int endIndicator = mBrowseNode.getExpectedChildren() - 1; logD("GetFolderItems: End " + endIndicator + " received " + folderList.size()); // Queue up image download if the item has an image and we don't have it yet // Only do this if the feature is enabled. for (AvrcpItem track : folderList) { if (shouldDownloadBrowsedImages()) { downloadImageIfNeeded(track); } else { track.setCoverArtUuid(null); } } // Always update the node so that the user does not wait forever // for the list to populate. int newSize = mBrowseNode.addChildren(folderList); logD("Added " + newSize + " items to the browse tree"); notifyChanged(mBrowseNode); if (mBrowseNode.getChildrenCount() >= endIndicator || folderList.size() == 0 || mAbort) { // If we have fetched all the elements or if the remotes sends us 0 elements // (which can lead us into a loop since mCurrInd does not proceed) we simply // abort. mBrowseNode.setCached(true); transitionTo(mConnected); } else { // Fetch the next set of items. fetchContents(mBrowseNode); // Reset the timeout message since we are doing a new fetch now. removeMessages(MESSAGE_INTERNAL_CMD_TIMEOUT); sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS); } break; case MESSAGE_PROCESS_SET_BROWSED_PLAYER: mBrowseTree.setCurrentBrowsedPlayer(mNextStep.getID(), msg.arg1, msg.arg2); removeMessages(MESSAGE_INTERNAL_CMD_TIMEOUT); sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS); navigateToFolderOrRetrieve(mBrowseNode); break; case MESSAGE_PROCESS_FOLDER_PATH: mBrowseTree.setCurrentBrowsedFolder(mNextStep.getID()); mBrowseTree.getCurrentBrowsedFolder().setExpectedChildren(msg.arg1); // AVRCP Specification says, if we're not database aware, we must disconnect and // reconnect our BIP client each time we successfully change path refreshCoverArt(); if (mAbort) { transitionTo(mConnected); } else { removeMessages(MESSAGE_INTERNAL_CMD_TIMEOUT); sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS); navigateToFolderOrRetrieve(mBrowseNode); } break; case MESSAGE_PROCESS_GET_PLAYER_ITEMS: logD("Received new available player items"); BrowseTree.BrowseNode rootNode = mBrowseTree.mRootNode; // The specification is not firm on what receiving available player changes // means relative to the existing player IDs, the addressed player and any // currently saved play status, track or now playing list metadata. We're going // to assume nothing and act verbosely, as some devices are known to reuse // Player IDs. if (!rootNode.isCached()) { List playerList = (List) msg.obj; // Since players hold metadata, including cover art handles that point to // stored images, be sure to save image UUIDs so we can see if we can // remove them from storage after setting our new player object ArrayList coverArtUuids = new ArrayList(); for (int i = 0; i < mAvailablePlayerList.size(); i++) { AvrcpPlayer player = mAvailablePlayerList.valueAt(i); AvrcpItem track = player.getCurrentTrack(); if (track != null && track.getCoverArtUuid() != null) { coverArtUuids.add(track.getCoverArtUuid()); } } mAvailablePlayerList.clear(); for (AvrcpPlayer player : playerList) { mAvailablePlayerList.put(player.getId(), player); } // If our new set of players contains our addressed player again then we // will replace it and re-download metadata. If not, we'll re-use the old // player to save the metadata queries. if (!mAvailablePlayerList.contains(mAddressedPlayerId)) { logD("Available player set doesn't contain the addressed player"); mAvailablePlayerList.put(mAddressedPlayerId, mAddressedPlayer); } else { logD("Update addressed player with new available player metadata"); mAddressedPlayer = mAvailablePlayerList.get(mAddressedPlayerId); mService.getCurrentMetadataNative(Utils.getByteAddress(mDevice)); mService.getPlaybackStateNative(Utils.getByteAddress(mDevice)); mBrowseTree.mNowPlayingNode.setCached(false); if (mAddressedPlayer.supportsFeature(AvrcpPlayer.FEATURE_NOW_PLAYING)) { sendMessage(MESSAGE_GET_FOLDER_ITEMS, mBrowseTree.mNowPlayingNode); } } logD("AddressedPlayer = " + mAddressedPlayer); // Check old cover art UUIDs for deletion for (String uuid : coverArtUuids) { removeUnusedArtwork(uuid); } // Make sure our browse tree matches our received Available Player set only rootNode.addChildren(playerList); mBrowseTree.setCurrentBrowsedFolder(BrowseTree.ROOT); rootNode.setExpectedChildren(playerList.size()); rootNode.setCached(true); notifyChanged(rootNode); } transitionTo(mConnected); break; case MESSAGE_INTERNAL_CMD_TIMEOUT: // We have timed out to execute the request, we should simply send // whatever listing we have gotten until now. Log.w(TAG, "TIMEOUT"); transitionTo(mConnected); break; case MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE: // If we have gotten an error for OUT OF RANGE we have // already sent all the items to the client hence simply // transition to Connected state here. mBrowseNode.setCached(true); transitionTo(mConnected); break; case MESSAGE_GET_FOLDER_ITEMS: if (!mBrowseNode.equals(msg.obj)) { if (shouldAbort(mBrowseNode.getScope(), ((BrowseTree.BrowseNode) msg.obj).getScope())) { mAbort = true; } deferMessage(msg); logD("GetFolderItems: Go Get Another Directory"); } else { logD("GetFolderItems: Get The Same Directory, ignore"); } break; default: // All of these messages should be handled by parent state immediately. return false; } return true; } /** * shouldAbort calculates the cases where fetching the current directory is no longer * necessary. * * @return true: a new folder in the same scope * a new player while fetching contents of a folder * false: other cases, specifically Now Playing while fetching a folder */ private boolean shouldAbort(int currentScope, int fetchScope) { if ((currentScope == fetchScope) || (currentScope == AvrcpControllerService.BROWSE_SCOPE_VFS && fetchScope == AvrcpControllerService.BROWSE_SCOPE_PLAYER_LIST)) { return true; } return false; } private void fetchContents(BrowseTree.BrowseNode target) { int start = target.getChildrenCount(); int end = Math.min(target.getExpectedChildren(), target.getChildrenCount() + ITEM_PAGE_SIZE) - 1; logD("fetchContents(title=" + target.getID() + ", scope=" + target.getScope() + ", start=" + start + ", end=" + end + ", expected=" + target.getExpectedChildren() + ")"); switch (target.getScope()) { case AvrcpControllerService.BROWSE_SCOPE_PLAYER_LIST: mService.getPlayerListNative(mDeviceAddress, start, end); break; case AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING: mService.getNowPlayingListNative( mDeviceAddress, start, end); break; case AvrcpControllerService.BROWSE_SCOPE_VFS: mService.getFolderListNative(mDeviceAddress, start, end); break; default: Log.e(TAG, STATE_TAG + " Scope " + target.getScope() + " cannot be handled here."); } } /* One of several things can happen when trying to get a folder list * * * 0: The folder handle is no longer valid * 1: The folder contents can be retrieved directly (NowPlaying, Root, Current) * 2: The folder is a browsable player * 3: The folder is a non browsable player * 4: The folder is not a child of the current folder * 5: The folder is a child of the current folder * */ private void navigateToFolderOrRetrieve(BrowseTree.BrowseNode target) { mNextStep = mBrowseTree.getNextStepToFolder(target); logD("NAVIGATING From " + mBrowseTree.getCurrentBrowsedFolder().toString()); logD("NAVIGATING Toward " + target.toString()); if (mNextStep == null) { return; } else if (target.equals(mBrowseTree.mNowPlayingNode) || target.equals(mBrowseTree.mRootNode) || mNextStep.equals(mBrowseTree.getCurrentBrowsedFolder())) { fetchContents(mNextStep); } else if (mNextStep.isPlayer()) { logD("NAVIGATING Player " + mNextStep.toString()); if (mNextStep.isBrowsable()) { mService.setBrowsedPlayerNative( mDeviceAddress, (int) mNextStep.getBluetoothID()); } else { logD("Player doesn't support browsing"); mNextStep.setCached(true); transitionTo(mConnected); } } else if (mNextStep.equals(mBrowseTree.mNavigateUpNode)) { logD("NAVIGATING UP " + mNextStep.toString()); mNextStep = mBrowseTree.getCurrentBrowsedFolder().getParent(); mBrowseTree.getCurrentBrowsedFolder().setCached(false); removeUnusedArtworkFromBrowseTree(); mService.changeFolderPathNative( mDeviceAddress, AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_UP, 0); } else { logD("NAVIGATING DOWN " + mNextStep.toString()); mService.changeFolderPathNative( mDeviceAddress, AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_DOWN, mNextStep.getBluetoothID()); } } @Override public void exit() { removeMessages(MESSAGE_INTERNAL_CMD_TIMEOUT); mBrowseNode = null; super.exit(); } } protected class Disconnecting extends State { @Override public void enter() { disconnectCoverArt(); onBrowsingDisconnected(); if (mService.sBrowseTree != null) { mService.sBrowseTree.mRootNode.removeChild(mBrowseTree.mRootNode); BluetoothMediaBrowserService.notifyChanged(mService.sBrowseTree.mRootNode); } broadcastConnectionStateChanged(BluetoothProfile.STATE_DISCONNECTING); transitionTo(mDisconnected); } } /** * Handle a request to align our local volume with the volume of a remote device. If * we're assuming the source volume is fixed then a response of ABS_VOL_MAX will always be * sent and no volume adjustment action will be taken on the sink side. * * @param absVol A volume level based on a domain of [0, ABS_VOL_MAX] * @param label Volume notification label */ private void handleAbsVolumeRequest(int absVol, int label) { logD("handleAbsVolumeRequest: absVol = " + absVol + ", label = " + label); if (mIsVolumeFixed) { logD("Source volume is assumed to be fixed, responding with max volume"); absVol = ABS_VOL_BASE; } else { mVolumeChangedNotificationsToIgnore++; removeMessages(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT); sendMessageDelayed(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT, ABS_VOL_TIMEOUT_MILLIS); setAbsVolume(absVol); } mService.sendAbsVolRspNative(mDeviceAddress, absVol, label); } /** * Align our volume with a requested absolute volume level * * @param absVol A volume level based on a domain of [0, ABS_VOL_MAX] */ private void setAbsVolume(int absVol) { int maxLocalVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); int curLocalVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); int reqLocalVolume = (maxLocalVolume * absVol) / ABS_VOL_BASE; logD("setAbsVolme: absVol = " + absVol + ", reqLocal = " + reqLocalVolume + ", curLocal = " + curLocalVolume + ", maxLocal = " + maxLocalVolume); /* * In some cases change in percentage is not sufficient enough to warrant * change in index values which are in range of 0-15. For such cases * no action is required */ if (reqLocalVolume != curLocalVolume) { mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, reqLocalVolume, AudioManager.FLAG_SHOW_UI); } } private int getAbsVolume() { if (mIsVolumeFixed) { return ABS_VOL_BASE; } int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); int currIndex = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); int newIndex = (currIndex * ABS_VOL_BASE) / maxVolume; return newIndex; } private boolean shouldDownloadBrowsedImages() { return mService.getResources() .getBoolean(R.bool.avrcp_controller_cover_art_browsed_images); } private void downloadImageIfNeeded(AvrcpItem track) { if (mCoverArtManager == null) return; String uuid = track.getCoverArtUuid(); Uri imageUri = null; if (uuid != null) { imageUri = mCoverArtManager.getImageUri(mDevice, uuid); if (imageUri != null) { track.setCoverArtLocation(imageUri); } else { mCoverArtManager.downloadImage(mDevice, uuid); } } } MediaSessionCompat.Callback mSessionCallbacks = new MediaSessionCompat.Callback() { @Override public void onPlay() { logD("onPlay"); onPrepare(); sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PLAY); } @Override public void onPause() { logD("onPause"); sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE); } @Override public void onSkipToNext() { logD("onSkipToNext"); onPrepare(); sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_FORWARD); } @Override public void onSkipToPrevious() { logD("onSkipToPrevious"); onPrepare(); sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_BACKWARD); } @Override public void onSkipToQueueItem(long id) { logD("onSkipToQueueItem id=" + id); onPrepare(); BrowseTree.BrowseNode node = mBrowseTree.getTrackFromNowPlayingList((int) id); if (node != null) { sendMessage(MESSAGE_PLAY_ITEM, node); } } @Override public void onStop() { logD("onStop"); sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_STOP); } @Override public void onPrepare() { logD("onPrepare"); A2dpSinkService a2dpSinkService = A2dpSinkService.getA2dpSinkService(); if (a2dpSinkService != null) { a2dpSinkService.requestAudioFocus(mDevice, true); } } @Override public void onRewind() { logD("onRewind"); sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_REWIND); } @Override public void onFastForward() { logD("onFastForward"); sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_FF); } @Override public void onPlayFromMediaId(String mediaId, Bundle extras) { logD("onPlayFromMediaId"); // Play the item if possible. onPrepare(); BrowseTree.BrowseNode node = mBrowseTree.findBrowseNodeByID(mediaId); if (node != null) { // node was found on this bluetooth device sendMessage(MESSAGE_PLAY_ITEM, node); } else { // node was not found on this device, pause here, and play on another device sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE); mService.playItem(mediaId); } } @Override public void onSetRepeatMode(int repeatMode) { logD("onSetRepeatMode"); sendMessage(MSG_AVRCP_SET_REPEAT, repeatMode); } @Override public void onSetShuffleMode(int shuffleMode) { logD("onSetShuffleMode"); sendMessage(MSG_AVRCP_SET_SHUFFLE, shuffleMode); } }; protected void broadcastConnectionStateChanged(int currentState) { if (mMostRecentState == currentState) { return; } if (currentState == BluetoothProfile.STATE_CONNECTED) { MetricsLogger.logProfileConnectionEvent( BluetoothMetricsProto.ProfileId.AVRCP_CONTROLLER); } logD("Connection state " + mDevice + ": " + mMostRecentState + "->" + currentState); Intent intent = new Intent(BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED); intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, mMostRecentState); intent.putExtra(BluetoothProfile.EXTRA_STATE, currentState); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); mMostRecentState = currentState; mService.sendBroadcast(intent, BLUETOOTH_CONNECT, Utils.getTempAllowlistBroadcastOptions()); } private boolean shouldRequestFocus() { return mService.getResources() .getBoolean(R.bool.a2dp_sink_automatically_request_audio_focus); } }