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