Merge "Unmute the streaming as the last step of changing active device"
diff --git a/src/com/android/bluetooth/avrcp/AvrcpTargetService.java b/src/com/android/bluetooth/avrcp/AvrcpTargetService.java
index 8cb6e6a..e2ae494 100644
--- a/src/com/android/bluetooth/avrcp/AvrcpTargetService.java
+++ b/src/com/android/bluetooth/avrcp/AvrcpTargetService.java
@@ -35,6 +35,7 @@
import com.android.bluetooth.a2dp.A2dpService;
import com.android.bluetooth.btservice.MetricsLogger;
import com.android.bluetooth.btservice.ProfileService;
+import com.android.bluetooth.btservice.ServiceFactory;
import java.util.List;
import java.util.Objects;
@@ -56,6 +57,7 @@
private AvrcpBroadcastReceiver mReceiver;
private AvrcpNativeInterface mNativeInterface;
private AvrcpVolumeManager mVolumeManager;
+ private ServiceFactory mFactory = new ServiceFactory();
// Only used to see if the metadata has changed from its previous value
private MediaData mCurrentData;
@@ -242,6 +244,15 @@
public void storeVolumeForDevice(BluetoothDevice device) {
if (device == null) return;
+ List<BluetoothDevice> HAActiveDevices = null;
+ if (mFactory.getHearingAidService() != null) {
+ HAActiveDevices = mFactory.getHearingAidService().getActiveDevices();
+ }
+ if (HAActiveDevices != null
+ && (HAActiveDevices.get(0) != null || HAActiveDevices.get(1) != null)) {
+ Log.d(TAG, "Do not store volume when Hearing Aid devices is active");
+ return;
+ }
mVolumeManager.storeVolumeForDevice(device);
}
diff --git a/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java b/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java
index 0bf34de..fa18045 100644
--- a/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java
+++ b/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java
@@ -105,6 +105,7 @@
boolean mBrowsingConnected = false;
BrowseTree mBrowseTree = null;
private AvrcpPlayer mAddressedPlayer = new AvrcpPlayer();
+ private int mAddressedPlayerId = -1;
private SparseArray<AvrcpPlayer> mAvailablePlayerList = new SparseArray<AvrcpPlayer>();
private int mVolumeChangedNotificationsToIgnore = 0;
@@ -347,6 +348,20 @@
}
return true;
+ case MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED:
+ mAddressedPlayerId = msg.arg1;
+ logD("AddressedPlayer = " + mAddressedPlayerId);
+ AvrcpPlayer updatedPlayer = mAvailablePlayerList.get(mAddressedPlayerId);
+ if (updatedPlayer != null) {
+ mAddressedPlayer = updatedPlayer;
+ logD("AddressedPlayer = " + mAddressedPlayer.getName());
+ } else {
+ mBrowseTree.mRootNode.setCached(false);
+ mBrowseTree.mRootNode.setExpectedChildren(255);
+ BluetoothMediaBrowserService.notifyChanged(mBrowseTree.mRootNode);
+ }
+ return true;
+
case DISCONNECT:
transitionTo(mDisconnecting);
return true;
@@ -404,11 +419,8 @@
return (cmd == AvrcpControllerService.PASS_THRU_CMD_ID_REWIND)
|| (cmd == AvrcpControllerService.PASS_THRU_CMD_ID_FF);
}
-
-
}
-
// Handle the get folder listing action
// a) Fetch the listing of folders
// b) Once completed return the object listing
@@ -525,8 +537,8 @@
case MESSAGE_GET_FOLDER_ITEMS:
if (!mBrowseNode.equals(msg.obj)) {
- if (mBrowseNode.getScope()
- == ((BrowseTree.BrowseNode) msg.obj).getScope()) {
+ if (shouldAbort(mBrowseNode.getScope(),
+ ((BrowseTree.BrowseNode) msg.obj).getScope())) {
mAbort = true;
}
deferMessage(msg);
@@ -545,6 +557,8 @@
case MESSAGE_PROCESS_PLAY_POS_CHANGED:
case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION:
+ case MESSAGE_PLAY_ITEM:
+ case MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED:
// All of these messages should be handled by parent state immediately.
return false;
@@ -556,6 +570,23 @@
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()
@@ -683,18 +714,21 @@
@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);
+ onPrepare();
BrowseTree.BrowseNode node = mBrowseTree.getTrackFromNowPlayingList((int) id);
if (node != null) {
sendMessage(MESSAGE_PLAY_ITEM, node);
@@ -730,10 +764,10 @@
@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
+ logD("onPlayFromMediaId");
// Play the item if possible.
onPrepare();
BrowseTree.BrowseNode node = mBrowseTree.findBrowseNodeByID(mediaId);
- Log.w(TAG, "Play Node not found");
sendMessage(MESSAGE_PLAY_ITEM, node);
}
};
diff --git a/src/com/android/bluetooth/avrcpcontroller/BrowseTree.java b/src/com/android/bluetooth/avrcpcontroller/BrowseTree.java
index 542beee..accea2a 100644
--- a/src/com/android/bluetooth/avrcpcontroller/BrowseTree.java
+++ b/src/com/android/bluetooth/avrcpcontroller/BrowseTree.java
@@ -412,7 +412,8 @@
if (target == null) {
return null;
} else if (target.equals(mCurrentBrowseNode)
- || target.equals(mNowPlayingNode)) {
+ || target.equals(mNowPlayingNode)
+ || target.equals(mRootNode)) {
return target;
} else if (target.isPlayer()) {
if (mDepth > 0) {
diff --git a/src/com/android/bluetooth/btservice/SilenceDeviceManager.java b/src/com/android/bluetooth/btservice/SilenceDeviceManager.java
index ec01130..e8ec235 100644
--- a/src/com/android/bluetooth/btservice/SilenceDeviceManager.java
+++ b/src/com/android/bluetooth/btservice/SilenceDeviceManager.java
@@ -64,8 +64,6 @@
private final ServiceFactory mFactory;
private Handler mHandler = null;
private Looper mLooper = null;
- private A2dpService mA2dpService = null;
- private HeadsetService mHeadsetService = null;
private final Map<BluetoothDevice, Boolean> mSilenceDevices = new HashMap<>();
private final List<BluetoothDevice> mA2dpConnectedDevices = new ArrayList<>();
@@ -225,8 +223,6 @@
filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
filter.addAction(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED);
mAdapterService.registerReceiver(mReceiver, filter);
- mA2dpService = mFactory.getA2dpService();
- mHeadsetService = mFactory.getHeadsetService();
}
void cleanup() {
@@ -234,8 +230,6 @@
Log.v(TAG, "cleanup()");
}
mSilenceDevices.clear();
- mA2dpService = null;
- mHeadsetService = null;
mAdapterService.unregisterReceiver(mReceiver);
}
@@ -268,16 +262,14 @@
}
mSilenceDevices.replace(device, state);
- if (mA2dpService == null) {
- Log.d(TAG, "A2dpService is null!");
- return;
+ A2dpService a2dpService = mFactory.getA2dpService();
+ if (a2dpService != null) {
+ a2dpService.setSilenceMode(device, state);
}
- if (mHeadsetService == null) {
- Log.d(TAG, "HeadsetService is null!");
- return;
+ HeadsetService headsetService = mFactory.getHeadsetService();
+ if (headsetService != null) {
+ headsetService.setSilenceMode(device, state);
}
- mA2dpService.setSilenceMode(device, state);
- mHeadsetService.setSilenceMode(device, state);
Log.i(TAG, "Silence mode change " + device.getAddress() + ": " + oldState + " -> "
+ state);
broadcastSilenceStateChange(device, state);
diff --git a/src/com/android/bluetooth/hearingaid/HearingAidService.java b/src/com/android/bluetooth/hearingaid/HearingAidService.java
index 54d5805..f29d9b9 100644
--- a/src/com/android/bluetooth/hearingaid/HearingAidService.java
+++ b/src/com/android/bluetooth/hearingaid/HearingAidService.java
@@ -505,6 +505,15 @@
Long deviceHiSyncId = mDeviceHiSyncIdMap.getOrDefault(device,
BluetoothHearingAid.HI_SYNC_ID_INVALID);
if (deviceHiSyncId != mActiveDeviceHiSyncId) {
+ // Give an early notification to A2DP that active device is being switched
+ // to Hearing Aids before the Audio Service.
+ final A2dpService a2dpService = mFactory.getA2dpService();
+ if (a2dpService != null) {
+ if (DBG) {
+ Log.d(TAG, "earlyNotifyHearingAidActive for " + device);
+ }
+ a2dpService.earlyNotifyHearingAidActive();
+ }
mActiveDeviceHiSyncId = deviceHiSyncId;
reportActiveDevice(device);
}
@@ -519,7 +528,7 @@
* device; the second element is the right active device. If either or both side
* is not active, it will be null on that position
*/
- List<BluetoothDevice> getActiveDevices() {
+ public List<BluetoothDevice> getActiveDevices() {
if (DBG) {
Log.d(TAG, "getActiveDevices");
}
@@ -625,18 +634,6 @@
StatsLog.write(StatsLog.BLUETOOTH_ACTIVE_DEVICE_CHANGED, BluetoothProfile.HEARING_AID,
mAdapterService.obfuscateAddress(device));
- if (device != null) {
- // Give an early notification to A2DP that active device is being switched
- // to Hearing Aids before the Audio Service.
- final A2dpService a2dpService = mFactory.getA2dpService();
- if (a2dpService != null) {
- if (DBG) {
- Log.d(TAG, "earlyNotifyHearingAidActive for " + device);
- }
- a2dpService.earlyNotifyHearingAidActive();
- }
- }
-
Intent intent = new Intent(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
diff --git a/src/com/android/bluetooth/mapclient/MceStateMachine.java b/src/com/android/bluetooth/mapclient/MceStateMachine.java
index 63ecd89..11b634c 100644
--- a/src/com/android/bluetooth/mapclient/MceStateMachine.java
+++ b/src/com/android/bluetooth/mapclient/MceStateMachine.java
@@ -51,6 +51,7 @@
import android.content.Intent;
import android.net.Uri;
import android.os.Message;
+import android.provider.Telephony;
import android.telecom.PhoneAccount;
import android.telephony.SmsManager;
import android.util.Log;
@@ -68,8 +69,10 @@
import java.util.ArrayList;
import java.util.Calendar;
+import java.util.Date;
import java.util.HashMap;
import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
/* The MceStateMachine is responsible for setting up and maintaining a connection to a single
* specific Messaging Server Equipment endpoint. Upon connect command an SDP record is retrieved,
@@ -122,6 +125,48 @@
new HashMap<>(MAX_MESSAGES);
private Bmessage.Type mDefaultMessageType = Bmessage.Type.SMS_CDMA;
+ /**
+ * An object to hold the necessary meta-data for each message so we can broadcast it alongside
+ * the message content.
+ *
+ * This is necessary because the metadata is inferred or received separately from the actual
+ * message content.
+ *
+ * Note: In the future it may be best to use the entries from the MessageListing in full instead
+ * of this small subset.
+ */
+ private class MessageMetadata {
+ private final String mHandle;
+ private final Long mTimestamp;
+ private boolean mRead;
+
+ MessageMetadata(String handle, Long timestamp, boolean read) {
+ mHandle = handle;
+ mTimestamp = timestamp;
+ mRead = read;
+ }
+
+ public String getHandle() {
+ return mHandle;
+ }
+
+ public Long getTimestamp() {
+ return mTimestamp;
+ }
+
+ public synchronized boolean getRead() {
+ return mRead;
+ }
+
+ public synchronized void setRead(boolean read) {
+ mRead = read;
+ }
+ }
+
+ // Map each message to its metadata via the handle
+ private ConcurrentHashMap<String, MessageMetadata> mMessages =
+ new ConcurrentHashMap<String, MessageMetadata>();
+
MceStateMachine(MapClientService service, BluetoothDevice device) {
this(service, device, null);
}
@@ -503,6 +548,15 @@
mPreviousState = BluetoothProfile.STATE_CONNECTED;
}
+ /**
+ * Given a message notification event, will ensure message caching and updating and update
+ * interested applications.
+ *
+ * Message notifications arrive for both remote message reception and Message-Listing object
+ * updates that are triggered by the server side.
+ *
+ * @param msg - A Message object containing a EventReport object describing the remote event
+ */
private void processNotification(Message msg) {
if (DBG) {
Log.d(TAG, "Handler: msg: " + msg.what);
@@ -518,7 +572,14 @@
switch (ev.getType()) {
case NEW_MESSAGE:
- //mService.get().sendNewMessageNotification(ev);
+ // Infer the timestamp for this message as 'now' and read status false
+ // instead of getting the message listing data for it
+ if (!mMessages.contains(ev.getHandle())) {
+ Calendar calendar = Calendar.getInstance();
+ MessageMetadata metadata = new MessageMetadata(ev.getHandle(),
+ calendar.getTime().getTime(), false);
+ mMessages.put(ev.getHandle(), metadata);
+ }
mMasClient.makeRequest(new RequestGetMessage(ev.getHandle(),
MasClient.CharsetType.UTF_8, false));
break;
@@ -534,6 +595,8 @@
// Sets the specified message status to "read" (from "unread" status, mostly)
private void markMessageRead(RequestGetMessage request) {
if (DBG) Log.d(TAG, "markMessageRead");
+ MessageMetadata metadata = mMessages.get(request.getHandle());
+ metadata.setRead(true);
mMasClient.makeRequest(new RequestSetMessageStatus(
request.getHandle(), RequestSetMessageStatus.StatusIndicator.READ));
}
@@ -545,21 +608,41 @@
request.getHandle(), RequestSetMessageStatus.StatusIndicator.DELETED));
}
+ /**
+ * Given the result of a Message Listing request, will cache the contents of each Message in
+ * the Message Listing Object and kick off requests to retrieve message contents from the
+ * remote device.
+ *
+ * @param request - A request object that has been resolved and returned with a message list
+ */
private void processMessageListing(RequestGetMessagesListing request) {
if (DBG) {
Log.d(TAG, "processMessageListing");
}
- ArrayList<com.android.bluetooth.mapclient.Message> messageHandles = request.getList();
- if (messageHandles != null) {
- for (com.android.bluetooth.mapclient.Message handle : messageHandles) {
+ ArrayList<com.android.bluetooth.mapclient.Message> messageListing = request.getList();
+ if (messageListing != null) {
+ for (com.android.bluetooth.mapclient.Message msg : messageListing) {
if (DBG) {
Log.d(TAG, "getting message ");
}
- getMessage(handle.getHandle());
+ // A message listing coming from the server should always have up to date data
+ mMessages.put(msg.getHandle(), new MessageMetadata(msg.getHandle(),
+ msg.getDateTime().getTime(), msg.isRead()));
+ getMessage(msg.getHandle());
}
}
}
+ /**
+ * Given the response of a GetMessage request, will broadcast the bMessage contents on to
+ * all registered applications.
+ *
+ * Inbound messages arrive as bMessage objects following a GetMessage request. GetMessage
+ * uses a message handle that can arrive from both a GetMessageListing request or a Message
+ * Notification event.
+ *
+ * @param request - A request object that has been resolved and returned with message data
+ */
private void processInboundMessage(RequestGetMessage request) {
Bmessage message = request.getMessage();
if (DBG) {
@@ -588,10 +671,18 @@
Log.d(TAG, "Recipients" + message.getRecipients().toString());
}
+ // Grab the message metadata and update the cached read status from the bMessage
+ MessageMetadata metadata = mMessages.get(request.getHandle());
+ metadata.setRead(request.getMessage().getStatus() == Bmessage.Status.READ);
+
Intent intent = new Intent();
intent.setAction(BluetoothMapClient.ACTION_MESSAGE_RECEIVED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_HANDLE, request.getHandle());
+ intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_TIMESTAMP,
+ metadata.getTimestamp());
+ intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_READ_STATUS,
+ metadata.getRead());
intent.putExtra(android.content.Intent.EXTRA_TEXT, message.getBodyContent());
VCardEntry originator = message.getOriginator();
if (originator != null) {
@@ -610,7 +701,12 @@
intent.putExtra(BluetoothMapClient.EXTRA_SENDER_CONTACT_NAME,
originator.getDisplayName());
}
- mService.sendBroadcast(intent);
+ // Only send to the current default SMS app if one exists
+ String defaultMessagingPackage = Telephony.Sms.getDefaultSmsPackage(mService);
+ if (defaultMessagingPackage != null) {
+ intent.setPackage(defaultMessagingPackage);
+ }
+ mService.sendBroadcast(intent, android.Manifest.permission.RECEIVE_SMS);
break;
case MMS:
diff --git a/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java b/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java
index 979becd..306f31d 100755
--- a/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java
+++ b/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java
@@ -986,6 +986,8 @@
nmnum = nmnum > 0 ? nmnum : 0;
misnum[0] = (byte) nmnum;
+ ap.addAPPHeader(ApplicationParameter.TRIPLET_TAGID.NEWMISSEDCALLS_TAGID,
+ ApplicationParameter.TRIPLET_LENGTH.NEWMISSEDCALLS_LENGTH, misnum);
if (D) {
Log.d(TAG, "handleAppParaForResponse(): mNeedNewMissedCallsNum=true, num= "
+ nmnum);
diff --git a/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachineTest.java b/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachineTest.java
index 14adf89..b1d1743 100644
--- a/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachineTest.java
+++ b/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachineTest.java
@@ -404,6 +404,49 @@
}
/**
+ * Test addressed media player changed
+ * Verify when the addressed media player changes browsing data updates
+ * Verify that the contents of a player are fetched upon request
+ */
+ @Test
+ public void testPlayerChanged() {
+ setUpConnectedState(true, true);
+ final String rootName = "__ROOT__";
+ final String playerName = "Player 1";
+
+ //Get the root of the device
+ BrowseTree.BrowseNode results = mAvrcpStateMachine.findNode(rootName);
+ Assert.assertEquals(rootName + mTestDevice.toString(), results.getID());
+
+ //Request fetch the list of players
+ BrowseTree.BrowseNode playerNodes = mAvrcpStateMachine.findNode(results.getID());
+ mAvrcpStateMachine.requestContents(results);
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).getPlayerListNative(eq(mTestAddress),
+ eq(0), eq(19));
+
+ //Provide back a player object
+ byte[] playerFeatures =
+ new byte[]{0, 0, 0, 0, 0, (byte) 0xb7, 0x01, 0x0c, 0x0a, 0, 0, 0, 0, 0, 0, 0};
+ AvrcpPlayer playerOne = new AvrcpPlayer(1, playerName, playerFeatures, 1, 1);
+ List<AvrcpPlayer> testPlayers = new ArrayList<>();
+ testPlayers.add(playerOne);
+ mAvrcpStateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_PLAYER_ITEMS,
+ testPlayers);
+
+ //Change players and verify that BT attempts to update the results
+ mAvrcpStateMachine.sendMessage(
+ AvrcpControllerStateMachine.MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED, 4);
+ results = mAvrcpStateMachine.findNode(rootName);
+
+ mAvrcpStateMachine.requestContents(results);
+
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)).getPlayerListNative(eq(mTestAddress),
+ eq(0), eq(19));
+ }
+
+ /**
* Test that the Now Playing playlist is updated when it changes.
*/
@Test