Avrcp: Limit available players changed

According to the spec, we should only send an Available Players Changed
update when a player is added or removed. (AVRCP 1.6.1 Sec 6.9.4 p74)

Only send an update when a player is added or removed.  If a player
would change feature bits, display name, major or minor type, present it
as a new player instead and remove the old player.

Sync available players notifications with other notifications.

Bug: 34471252
Test: switch players, note that Available Players Changed is not sent
all the time
Change-Id: Icd3730afa6e182810920f28fa7db17b98e53ceea
(cherry picked from commit f2f6a4ea02e9456f71ea0490112845110ca5d975)
(cherry picked from commit 85ff6903243d244d4342f390bb40aa99097a9a7c)
diff --git a/src/com/android/bluetooth/avrcp/Avrcp.java b/src/com/android/bluetooth/avrcp/Avrcp.java
index 6103abc..577aecb 100644
--- a/src/com/android/bluetooth/avrcp/Avrcp.java
+++ b/src/com/android/bluetooth/avrcp/Avrcp.java
@@ -161,9 +161,8 @@
     private static final int MSG_SET_ABSOLUTE_VOLUME = 16;
     private static final int MSG_ABS_VOL_TIMEOUT = 17;
     private static final int MSG_SET_A2DP_AUDIO_STATE = 18;
-    private static final int MSG_AVAILABLE_PLAYERS_CHANGED_RSP = 19;
-    private static final int MSG_NOW_PLAYING_CHANGED_RSP = 20;
-    private static final int MSG_UPDATE_MEDIA = 21;
+    private static final int MSG_NOW_PLAYING_CHANGED_RSP = 19;
+    private static final int MSG_UPDATE_MEDIA = 20;
 
     private static final int CMD_TIMEOUT_DELAY = 2000;
     private static final int MEDIA_DWELL_TIME = 1000;
@@ -179,6 +178,7 @@
 
     /* List of Media player instances, useful for retrieving MediaPlayerList or MediaPlayerInfo */
     private SortedMap<Integer, MediaPlayerInfo> mMediaPlayerInfoList;
+    private boolean mAvailablePlayerViewChanged;
 
     /* List of media players which supports browse */
     private List<BrowsePlayerInfo> mBrowsePlayerInfoList;
@@ -304,6 +304,7 @@
         mMediaControllerCb = new MediaControllerListener();
         mAvrcpMediaRsp = new AvrcpMediaRsp();
         mMediaPlayerInfoList = new TreeMap<Integer, MediaPlayerInfo>();
+        mAvailablePlayerViewChanged = false;
         mBrowsePlayerInfoList = Collections.synchronizedList(new ArrayList<BrowsePlayerInfo>());
         mPassthroughDispatched = 0;
         mPassthroughLogs = new EvictingQueue<MediaKeyLog>(PASSTHROUGH_LOG_MAX_SIZE);
@@ -474,13 +475,6 @@
                 processRegisterNotification((byte[]) msg.obj, msg.arg1, msg.arg2);
                 break;
 
-            case MSG_AVAILABLE_PLAYERS_CHANGED_RSP:
-                if (DEBUG) Log.v(TAG, "MSG_AVAILABLE_PLAYERS_CHANGED_RSP");
-                removeMessages(MSG_AVAILABLE_PLAYERS_CHANGED_RSP);
-                registerNotificationRspAvalPlayerChangedNative(
-                        AvrcpConstants.NOTIFICATION_TYPE_CHANGED);
-                break;
-
             case MSG_NOW_PLAYING_CHANGED_RSP:
                 if (DEBUG) Log.v(TAG, "MSG_NOW_PLAYING_CHANGED_RSP");
                 removeMessages(MSG_NOW_PLAYING_CHANGED_RSP);
@@ -986,19 +980,27 @@
     }
 
     private void updateCurrentMediaState(boolean registering) {
-        if (!registering && mAddrPlayerChangedNT == AvrcpConstants.NOTIFICATION_TYPE_INTERIM
-                && mReportedPlayerID != mCurrAddrPlayerID) {
-            registerNotificationRspAddrPlayerChangedNative(
-                    AvrcpConstants.NOTIFICATION_TYPE_CHANGED, mCurrAddrPlayerID, sUIDCounter);
-            mAddrPlayerChangedNT = AvrcpConstants.NOTIFICATION_TYPE_CHANGED;
-            // Changing player sends reject to all these, so disable them.
-            mReportedPlayerID = mCurrAddrPlayerID;
-            mPlayStatusChangedNT = AvrcpConstants.NOTIFICATION_TYPE_CHANGED;
-            mTrackChangedNT = AvrcpConstants.NOTIFICATION_TYPE_CHANGED;
-            mPlayPosChangedNT = AvrcpConstants.NOTIFICATION_TYPE_CHANGED;
-            // If the player changed, they need to re-request anything here again
-            // so we can skip the rest of the update.
-            return;
+        // Only do player updates when we aren't registering for track changes.
+        if (!registering) {
+            if (mAvailablePlayerViewChanged) {
+                registerNotificationRspAvalPlayerChangedNative(
+                        AvrcpConstants.NOTIFICATION_TYPE_CHANGED);
+                mAvailablePlayerViewChanged = false;
+            }
+            if (mAddrPlayerChangedNT == AvrcpConstants.NOTIFICATION_TYPE_INTERIM
+                    && mReportedPlayerID != mCurrAddrPlayerID) {
+                registerNotificationRspAddrPlayerChangedNative(
+                        AvrcpConstants.NOTIFICATION_TYPE_CHANGED, mCurrAddrPlayerID, sUIDCounter);
+                mAddrPlayerChangedNT = AvrcpConstants.NOTIFICATION_TYPE_CHANGED;
+                // Changing player sends reject to anything else we would notify...
+                mReportedPlayerID = mCurrAddrPlayerID;
+                mPlayStatusChangedNT = AvrcpConstants.NOTIFICATION_TYPE_CHANGED;
+                mTrackChangedNT = AvrcpConstants.NOTIFICATION_TYPE_CHANGED;
+                mPlayPosChangedNT = AvrcpConstants.NOTIFICATION_TYPE_CHANGED;
+                // If the player changed, they need to re-request anything here again
+                // so we can skip the rest of the update.
+                return;
+            }
         }
 
         MediaAttributes currentAttributes;
@@ -1580,23 +1582,17 @@
                 @Override
                 public void onActiveSessionsChanged(
                         List<android.media.session.MediaController> newControllers) {
-                    boolean playersChanged = false;
-
                     // Update the current players
                     for (android.media.session.MediaController controller : newControllers) {
                         addMediaPlayerController(controller);
-                        playersChanged = true;
                     }
 
-                    if (playersChanged) {
-                        mHandler.sendEmptyMessage(MSG_AVAILABLE_PLAYERS_CHANGED_RSP);
-                        if (newControllers.size() > 0 && getAddressedPlayerInfo() == null) {
-                            if (DEBUG)
-                                Log.v(TAG,
-                                        "No addressed player but active sessions, taking first.");
-                            setAddressedMediaSessionPackage(newControllers.get(0).getPackageName());
-                        }
+                    if (newControllers.size() > 0 && getAddressedPlayerInfo() == null) {
+                        if (DEBUG)
+                            Log.v(TAG, "No addressed player but active sessions, taking first.");
+                        setAddressedMediaSessionPackage(newControllers.get(0).getPackageName());
                     }
+                    scheduleMediaUpdate();
                 }
             };
 
@@ -1612,7 +1608,7 @@
         // If the player doesn't exist, we need to add it.
         if (getMediaPlayerInfo(packageName) == null) {
             addMediaPlayerPackage(packageName);
-            mHandler.sendEmptyMessage(MSG_AVAILABLE_PLAYERS_CHANGED_RSP);
+            scheduleMediaUpdate();
         }
         synchronized (mMediaPlayerInfoList) {
             for (Map.Entry<Integer, MediaPlayerInfo> entry : mMediaPlayerInfoList.entrySet()) {
@@ -1758,9 +1754,8 @@
             for (android.media.session.MediaController controller : controllers) {
                 addMediaPlayerController(controller);
             }
-            if (controllers.size() > 0) {
-                mHandler.sendEmptyMessage(MSG_AVAILABLE_PLAYERS_CHANGED_RSP);
-            }
+
+            scheduleMediaUpdate();
 
             if (mMediaPlayerInfoList.size() > 0) {
                 // Set the first one as the Addressed Player
@@ -1808,10 +1803,20 @@
     private boolean addMediaPlayerInfo(MediaPlayerInfo info) {
         int updateId = -1;
         boolean updated = false;
+        boolean currentRemoved = false;
         synchronized (mMediaPlayerInfoList) {
             for (Map.Entry<Integer, MediaPlayerInfo> entry : mMediaPlayerInfoList.entrySet()) {
-                if (info.getPackageName().equals(entry.getValue().getPackageName())) {
-                    updateId = entry.getKey();
+                MediaPlayerInfo current = entry.getValue();
+                int id = entry.getKey();
+                if (info.getPackageName().equals(current.getPackageName())) {
+                    if (!current.equalView(info)) {
+                        // If we would present a different player, make it a new player
+                        // so that controllers know whether a player is browsable or not.
+                        mMediaPlayerInfoList.remove(id);
+                        currentRemoved = (mCurrAddrPlayerID == id);
+                        break;
+                    }
+                    updateId = id;
                     updated = true;
                     break;
                 }
@@ -1820,12 +1825,13 @@
                 // New player
                 mLastUsedPlayerID++;
                 updateId = mLastUsedPlayerID;
+                mAvailablePlayerViewChanged = true;
             }
             mMediaPlayerInfoList.put(updateId, info);
             if (DEBUG)
                 Log.d(TAG, (updated ? "update #" : "add #") + updateId + ":" + info.toString());
-            if (updateId == mCurrAddrPlayerID) {
-                updateCurrentController(mCurrAddrPlayerID, mCurrBrowsePlayerID);
+            if (currentRemoved || updateId == mCurrAddrPlayerID) {
+                updateCurrentController(updateId, mCurrBrowsePlayerID);
             }
         }
         return updated;
@@ -1844,6 +1850,7 @@
             if (removeKey != -1) {
                 if (DEBUG)
                     Log.d(TAG, "remove #" + removeKey + ":" + mMediaPlayerInfoList.get(removeKey));
+                mAvailablePlayerViewChanged = true;
                 return mMediaPlayerInfoList.remove(removeKey);
             }
 
diff --git a/src/com/android/bluetooth/avrcp/AvrcpHelperClasses.java b/src/com/android/bluetooth/avrcp/AvrcpHelperClasses.java
index 5a5b5f7..b0a4c5d 100644
--- a/src/com/android/bluetooth/avrcp/AvrcpHelperClasses.java
+++ b/src/com/android/bluetooth/avrcp/AvrcpHelperClasses.java
@@ -16,6 +16,8 @@
 
 package com.android.bluetooth.avrcp;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.media.session.MediaSession;
 
 import com.android.bluetooth.Utils;
@@ -200,17 +202,19 @@
     private int subType;
     private byte playStatus;
     private short[] featureBitMask;
-    private String packageName;
-    private String displayableName;
-    private MediaController mediaController;
+    private @NonNull String packageName;
+    private @NonNull String displayableName;
+    private @Nullable MediaController mediaController;
 
-    MediaPlayerInfo(MediaController controller, byte majorType, int subType, byte playStatus,
-            short[] featureBitMask, String packageName, String displayableName) {
+    MediaPlayerInfo(@Nullable MediaController controller, byte majorType, int subType,
+            byte playStatus, short[] featureBitMask, @NonNull String packageName,
+            @Nullable String displayableName) {
         this.setMajorType(majorType);
         this.setSubType(subType);
         this.playStatus = playStatus;
         // store a copy the FeatureBitMask array
         this.featureBitMask = Arrays.copyOf(featureBitMask, featureBitMask.length);
+        Arrays.sort(this.featureBitMask);
         this.setPackageName(packageName);
         this.setDisplayableName(displayableName);
         this.setMediaController(controller);
@@ -236,7 +240,7 @@
         this.mediaController = mediaController;
     }
 
-    void setPackageName(String name) {
+    void setPackageName(@NonNull String name) {
         // Controller determines package name when it is set.
         if (mediaController != null) return;
         this.packageName = name;
@@ -271,7 +275,8 @@
         return displayableName;
     }
 
-    void setDisplayableName(String displayableName) {
+    void setDisplayableName(@Nullable String displayableName) {
+        if (displayableName == null) displayableName = "";
         this.displayableName = displayableName;
     }
 
@@ -282,6 +287,7 @@
     void setFeatureBitMask(short[] featureBitMask) {
         synchronized (this) {
             this.featureBitMask = Arrays.copyOf(featureBitMask, featureBitMask.length);
+            Arrays.sort(this.featureBitMask);
         }
     }
 
@@ -295,6 +301,14 @@
         return false;
     }
 
+    /** Tests if the view of this player presented to the controller is different enough to
+     *  justify sending an Available Players Changed update */
+    public boolean equalView(MediaPlayerInfo other) {
+        return (this.majorType == other.getMajorType()) && (this.subType == other.getSubType())
+                && Arrays.equals(this.featureBitMask, other.getFeatureBitMask())
+                && this.displayableName.equals(other.getDisplayableName());
+    }
+
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder();