Add Settings support for Bluetooth Multi-A2DP and Multi-HFP

When there are multiple connected A2DP/HFP devices, if a connected
device's name is clicked on, that device will be chosen as
Active - i.e., it will be the device chosen for audio out / phone call.

Also:
 * Listen to the BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED
   and BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED intents
   and update the the status of the current active device.
 * When connecting a new device, and Multi-A2DP is enabled, don't
   disconnect the currently connected device.
 * Update the implementation of isA2dpPlaying() so it correctly checks
   all connected devices, not only the first one.

Test: Manual: multiple connected A2DP devices, and selecting each as
      the Active Device.
Bug: 64767509

Change-Id: I69f3c85ebf5a7f07f6deed484c6dd65705460ae4
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java
index 764c5922..9b69304 100755
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java
@@ -128,14 +128,18 @@
 
     public boolean connect(BluetoothDevice device) {
         if (mService == null) return false;
-        List<BluetoothDevice> sinks = getConnectedDevices();
-        if (sinks != null) {
-            for (BluetoothDevice sink : sinks) {
-                if (sink.equals(device)) {
-                    Log.w(TAG, "Connecting to device " + device + " : disconnect skipped");
-                    continue;
+        int max_connected_devices = mLocalAdapter.getMaxConnectedAudioDevices();
+        if (max_connected_devices == 1) {
+            // Original behavior: disconnect currently connected device
+            List<BluetoothDevice> sinks = getConnectedDevices();
+            if (sinks != null) {
+                for (BluetoothDevice sink : sinks) {
+                    if (sink.equals(device)) {
+                        Log.w(TAG, "Connecting to device " + device + " : disconnect skipped");
+                        continue;
+                    }
+                    mService.disconnect(sink);
                 }
-                mService.disconnect(sink);
             }
         }
         return mService.connect(device);
@@ -157,6 +161,16 @@
         return mService.getConnectionState(device);
     }
 
+    public boolean setActiveDevice(BluetoothDevice device) {
+        if (mService == null) return false;
+        return mService.setActiveDevice(device);
+    }
+
+    public BluetoothDevice getActiveDevice() {
+        if (mService == null) return null;
+        return mService.getActiveDevice();
+    }
+
     public boolean isPreferred(BluetoothDevice device) {
         if (mService == null) return false;
         return mService.getPriority(device) > BluetoothProfile.PRIORITY_OFF;
@@ -180,8 +194,8 @@
     boolean isA2dpPlaying() {
         if (mService == null) return false;
         List<BluetoothDevice> sinks = mService.getConnectedDevices();
-        if (!sinks.isEmpty()) {
-            if (mService.isA2dpPlaying(sinks.get(0))) {
+        for (BluetoothDevice device : sinks) {
+            if (mService.isA2dpPlaying(device)) {
                 return true;
             }
         }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java
index 4c41b49..ac3599c 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java
@@ -28,4 +28,5 @@
     void onDeviceDeleted(CachedBluetoothDevice cachedDevice);
     void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState);
     void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state);
+    void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile);
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
index f57d02b..3cda9c9 100755
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
@@ -16,9 +16,12 @@
 
 package com.android.settingslib.bluetooth;
 
+import android.bluetooth.BluetoothA2dp;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothProfile;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -31,6 +34,7 @@
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -106,6 +110,12 @@
         // Dock event broadcasts
         addHandler(Intent.ACTION_DOCK_EVENT, new DockEventHandler());
 
+        // Active device broadcasts
+        addHandler(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED,
+                   new ActiveDeviceChangedHandler());
+        addHandler(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED,
+                   new ActiveDeviceChangedHandler());
+
         mContext.registerReceiver(mBroadcastReceiver, mAdapterIntentFilter, null, mReceiverHandler);
         mContext.registerReceiver(mProfileBroadcastReceiver, mProfileIntentFilter, null, mReceiverHandler);
     }
@@ -409,4 +419,35 @@
 
         return deviceAdded;
     }
+
+    private class ActiveDeviceChangedHandler implements Handler {
+        @Override
+        public void onReceive(Context context, Intent intent, BluetoothDevice device) {
+            String action = intent.getAction();
+            if (action == null) {
+                Log.w(TAG, "ActiveDeviceChangedHandler: action is null");
+                return;
+            }
+            CachedBluetoothDevice activeDevice = mDeviceManager.findDevice(device);
+            int bluetoothProfile = 0;
+            if (Objects.equals(action, BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED)) {
+                bluetoothProfile = BluetoothProfile.A2DP;
+            } else if (Objects.equals(action, BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED)) {
+                bluetoothProfile = BluetoothProfile.HEADSET;
+            } else {
+                Log.w(TAG, "ActiveDeviceChangedHandler: unknown action " + action);
+                return;
+            }
+            dispatchActiveDeviceChanged(activeDevice, bluetoothProfile);
+        }
+    }
+
+    private void dispatchActiveDeviceChanged(CachedBluetoothDevice activeDevice,
+                                             int bluetoothProfile) {
+        synchronized (mCallbacks) {
+            for (BluetoothCallback callback : mCallbacks) {
+                callback.onActiveDeviceChanged(activeDevice, bluetoothProfile);
+            }
+        }
+    }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 9caff10..fb0f75b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -105,6 +105,10 @@
     private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
     private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000;
 
+    // Active device state
+    private boolean mIsActiveDeviceA2dp = false;
+    private boolean mIsActiveDeviceHeadset = false;
+
     /**
      * Describes the current device and profile for logging.
      *
@@ -156,6 +160,7 @@
             mRemovedProfiles.add(profile);
             mLocalNapRoleConnected = false;
         }
+        fetchActiveDevices();
     }
 
     CachedBluetoothDevice(Context context,
@@ -359,6 +364,7 @@
         fetchName();
         fetchBtClass();
         updateProfiles();
+        fetchActiveDevices();
         migratePhonebookPermissionChoice();
         migrateMessagePermissionChoice();
         fetchMessageRejectionCount();
@@ -454,6 +460,33 @@
         return mDevice.getBondState();
     }
 
+    /**
+     * Set the device status as active or non-active per Bluetooth profile.
+     *
+     * @param isActive true if the device is active
+     * @param bluetoothProfile the Bluetooth profile
+     */
+    public void setActiveDevice(boolean isActive, int bluetoothProfile) {
+        boolean changed = false;
+        switch (bluetoothProfile) {
+        case BluetoothProfile.A2DP:
+            changed = (mIsActiveDeviceA2dp != isActive);
+            mIsActiveDeviceA2dp = isActive;
+            break;
+        case BluetoothProfile.HEADSET:
+            changed = (mIsActiveDeviceHeadset != isActive);
+            mIsActiveDeviceHeadset = isActive;
+            break;
+        default:
+            Log.w(TAG, "setActiveDevice: unknown profile " + bluetoothProfile +
+                    " isActive " + isActive);
+            break;
+        }
+        if (changed) {
+            dispatchAttributesChanged();
+        }
+    }
+
     void setRssi(short rssi) {
         if (mRssi != rssi) {
             mRssi = rssi;
@@ -529,6 +562,17 @@
         return true;
     }
 
+    private void fetchActiveDevices() {
+        A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
+        if (a2dpProfile != null) {
+            mIsActiveDeviceA2dp = mDevice.equals(a2dpProfile.getActiveDevice());
+        }
+        HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
+        if (headsetProfile != null) {
+            mIsActiveDeviceHeadset = mDevice.equals(headsetProfile.getActiveDevice());
+        }
+    }
+
     /**
      * Refreshes the UI for the BT class, including fetching the latest value
      * for the class.
@@ -896,37 +940,60 @@
                     com.android.settingslib.Utils.formatPercentage(batteryLevel);
         }
 
+        // TODO: A temporary workaround solution using string description the device is active.
+        // Issue tracked by b/72317067 .
+        // An alternative solution would be visual indication.
+        // Intentionally not adding the strings to strings.xml for now:
+        //  1) If this is just a short-term solution, no need to waste translation effort
+        //  2) The number of strings with all possible combinations becomes enormously large.
+        // If string description becomes part of the final solution, we MUST NOT
+        // concatenate the strings here: this does not translate well.
+        String activeString = null;
+        if (mIsActiveDeviceA2dp && mIsActiveDeviceHeadset) {
+            activeString = ", active";
+        } else {
+            if (mIsActiveDeviceA2dp) {
+                activeString = ", active(media)";
+            }
+            if (mIsActiveDeviceHeadset) {
+                activeString = ", active(phone)";
+            }
+        }
+        if (activeString == null) activeString = "";
+
         if (profileConnected) {
             if (a2dpNotConnected && hfpNotConnected) {
                 if (batteryLevelPercentageString != null) {
                     return mContext.getString(
                             R.string.bluetooth_connected_no_headset_no_a2dp_battery_level,
-                            batteryLevelPercentageString);
+                            batteryLevelPercentageString) + activeString;
                 } else {
-                    return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp);
+                    return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp) +
+                        activeString;
                 }
 
             } else if (a2dpNotConnected) {
                 if (batteryLevelPercentageString != null) {
                     return mContext.getString(R.string.bluetooth_connected_no_a2dp_battery_level,
-                            batteryLevelPercentageString);
+                            batteryLevelPercentageString) + activeString;
                 } else {
-                    return mContext.getString(R.string.bluetooth_connected_no_a2dp);
+                    return mContext.getString(R.string.bluetooth_connected_no_a2dp) + activeString;
                 }
 
             } else if (hfpNotConnected) {
                 if (batteryLevelPercentageString != null) {
                     return mContext.getString(R.string.bluetooth_connected_no_headset_battery_level,
-                            batteryLevelPercentageString);
+                            batteryLevelPercentageString) + activeString;
                 } else {
-                    return mContext.getString(R.string.bluetooth_connected_no_headset);
+                    return mContext.getString(R.string.bluetooth_connected_no_headset)
+                          + activeString;
                 }
             } else {
                 if (batteryLevelPercentageString != null) {
                     return mContext.getString(R.string.bluetooth_connected_battery_level,
-                            batteryLevelPercentageString);
+                            batteryLevelPercentageString) + activeString;
                 } else {
-                    return mContext.getString(R.string.bluetooth_connected);
+                    return mContext.getString(R.string.bluetooth_connected) + activeString;
                 }
             }
         }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HeadsetProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HeadsetProfile.java
index d45fe1a..ee12191 100755
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HeadsetProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HeadsetProfile.java
@@ -153,6 +153,16 @@
         return BluetoothProfile.STATE_DISCONNECTED;
     }
 
+    public boolean setActiveDevice(BluetoothDevice device) {
+        if (mService == null) return false;
+        return mService.setActiveDevice(device);
+    }
+
+    public BluetoothDevice getActiveDevice() {
+        if (mService == null) return null;
+        return mService.getActiveDevice();
+    }
+
     public boolean isPreferred(BluetoothDevice device) {
         if (mService == null) return false;
         return mService.getPriority(device) > BluetoothProfile.PRIORITY_OFF;
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothAdapter.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothAdapter.java
index 22674cb..cda4e45 100755
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothAdapter.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothAdapter.java
@@ -239,4 +239,8 @@
     public BluetoothDevice getRemoteDevice(String address) {
         return mAdapter.getRemoteDevice(address);
     }
+
+    public int getMaxConnectedAudioDevices() {
+        return mAdapter.getMaxConnectedAudioDevices();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java
index 4b775a5..b8411e2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java
@@ -608,6 +608,9 @@
         public void onScanningStateChanged(boolean started) { }
         @Override
         public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { }
+        @Override
+        public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice,
+                                          int bluetoothProfile) { }
     }
 
     private final class BluetoothErrorListener implements Utils.ErrorListener {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
index 3b15c2b..fcf084b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
@@ -276,6 +276,9 @@
         mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED);
     }
 
+    @Override
+    public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) {}
+
     private ActuallyCachedState getCachedState(CachedBluetoothDevice device) {
         ActuallyCachedState state = mCachedState.get(device);
         if (state == null) {