Merge changes from topics "hearing-aid-white-list", "hearing-aid" into pi-dev

* changes:
  DO NOT MERGE Hearing Aid: Use separate time for L/R in connect() and use whitelist
  Hearing Aid: Remove device from HiSyncIdMap when unbonded
  Add Feature Flag for Hearing Aid Profile
diff --git a/jni/com_android_bluetooth_hearing_aid.cpp b/jni/com_android_bluetooth_hearing_aid.cpp
index c965b1d..1602aac 100644
--- a/jni/com_android_bluetooth_hearing_aid.cpp
+++ b/jni/com_android_bluetooth_hearing_aid.cpp
@@ -194,6 +194,40 @@
   return JNI_TRUE;
 }
 
+static jboolean addToWhiteListNative(JNIEnv* env, jobject object,
+                                     jbyteArray address) {
+  std::shared_lock<std::shared_timed_mutex> lock(interface_mutex);
+  if (!sHearingAidInterface) return JNI_FALSE;
+
+  jbyte* addr = env->GetByteArrayElements(address, nullptr);
+  if (!addr) {
+    jniThrowIOException(env, EINVAL);
+    return JNI_FALSE;
+  }
+
+  RawAddress* tmpraw = (RawAddress*)addr;
+  sHearingAidInterface->AddToWhiteList(*tmpraw);
+  env->ReleaseByteArrayElements(address, addr, 0);
+  return JNI_TRUE;
+}
+
+static jboolean removeFromWhiteListNative(JNIEnv* env, jobject object,
+                                          jbyteArray address) {
+  std::shared_lock<std::shared_timed_mutex> lock(interface_mutex);
+  if (!sHearingAidInterface) return JNI_FALSE;
+
+  jbyte* addr = env->GetByteArrayElements(address, nullptr);
+  if (!addr) {
+    jniThrowIOException(env, EINVAL);
+    return JNI_FALSE;
+  }
+
+  RawAddress* tmpraw = (RawAddress*)addr;
+  sHearingAidInterface->RemoveFromWhiteList(*tmpraw);
+  env->ReleaseByteArrayElements(address, addr, 0);
+  return JNI_TRUE;
+}
+
 static void setVolumeNative(JNIEnv* env, jclass clazz, jint volume) {
   if (!sHearingAidInterface) {
     LOG(ERROR) << __func__
@@ -209,6 +243,8 @@
     {"cleanupNative", "()V", (void*)cleanupNative},
     {"connectHearingAidNative", "([B)Z", (void*)connectHearingAidNative},
     {"disconnectHearingAidNative", "([B)Z", (void*)disconnectHearingAidNative},
+    {"addToWhiteListNative", "([B)Z", (void*)addToWhiteListNative},
+    {"removeFromWhiteListNative", "([B)Z", (void*)removeFromWhiteListNative},
     {"setVolumeNative", "(I)V", (void*)setVolumeNative},
 };
 
diff --git a/src/com/android/bluetooth/btservice/Config.java b/src/com/android/bluetooth/btservice/Config.java
index ae65863..db79486 100644
--- a/src/com/android/bluetooth/btservice/Config.java
+++ b/src/com/android/bluetooth/btservice/Config.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.provider.Settings;
+import android.util.FeatureFlagUtils;
 import android.util.Log;
 
 import com.android.bluetooth.R;
@@ -117,6 +118,13 @@
         ArrayList<Class> profiles = new ArrayList<>(PROFILE_SERVICES_AND_FLAGS.length);
         for (ProfileConfig config : PROFILE_SERVICES_AND_FLAGS) {
             boolean supported = resources.getBoolean(config.mSupported);
+
+            if (supported && (config.mClass == HearingAidService.class) && !FeatureFlagUtils
+                                .isEnabled(ctx, FeatureFlagUtils.HEARING_AID_SETTINGS)) {
+                Log.v(TAG, "Feature Flag disables support for HearingAidService");
+                supported = false;
+            }
+
             if (supported && !isProfileDisabled(ctx, config.mMask)) {
                 Log.v(TAG, "Adding " + config.mClass.getSimpleName());
                 profiles.add(config.mClass);
diff --git a/src/com/android/bluetooth/hearingaid/HearingAidNativeInterface.java b/src/com/android/bluetooth/hearingaid/HearingAidNativeInterface.java
index 2603659..e735a88 100644
--- a/src/com/android/bluetooth/hearingaid/HearingAidNativeInterface.java
+++ b/src/com/android/bluetooth/hearingaid/HearingAidNativeInterface.java
@@ -105,6 +105,28 @@
     }
 
     /**
+     * Add a hearing aid device to white list.
+     *
+     * @param device the remote device
+     * @return true on success, otherwise false.
+     */
+    @VisibleForTesting (otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+    public boolean addToWhiteList(BluetoothDevice device) {
+        return addToWhiteListNative(getByteAddress(device));
+    }
+
+    /**
+     * Remove a hearing aid device from white list.
+     *
+     * @param device the remote device
+     * @return true on success, otherwise false.
+     */
+    @VisibleForTesting (otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+    public boolean removeFromWhiteList(BluetoothDevice device) {
+        return removeFromWhiteListNative(getByteAddress(device));
+    }
+
+    /**
      * Sets the HearingAid volume
      * @param volume
      */
@@ -168,5 +190,7 @@
     private native void cleanupNative();
     private native boolean connectHearingAidNative(byte[] address);
     private native boolean disconnectHearingAidNative(byte[] address);
+    private native boolean addToWhiteListNative(byte[] address);
+    private native boolean removeFromWhiteListNative(byte[] address);
     private native void setVolumeNative(int volume);
 }
diff --git a/src/com/android/bluetooth/hearingaid/HearingAidService.java b/src/com/android/bluetooth/hearingaid/HearingAidService.java
index bbc61ba..ce2ab3b 100644
--- a/src/com/android/bluetooth/hearingaid/HearingAidService.java
+++ b/src/com/android/bluetooth/hearingaid/HearingAidService.java
@@ -56,6 +56,10 @@
     // Upper limit of all HearingAid devices: Bonded or Connected
     private static final int MAX_HEARING_AID_STATE_MACHINES = 10;
     private static HearingAidService sHearingAidService;
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    static int sConnectTimeoutForEachSideMs = 8000;
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    static int sCheckWhitelistTimeoutMs = 16000;
 
     private AdapterService mAdapterService;
     private HandlerThread mStateMachinesThread;
@@ -243,14 +247,6 @@
             }
         }
 
-        synchronized (mStateMachines) {
-            HearingAidStateMachine smConnect = getOrCreateStateMachine(device);
-            if (smConnect == null) {
-                Log.e(TAG, "Cannot connect to " + device + " : no state machine");
-            }
-            smConnect.sendMessage(HearingAidStateMachine.CONNECT);
-        }
-
         for (BluetoothDevice storedDevice : mDeviceHiSyncIdMap.keySet()) {
             if (device.equals(storedDevice)) {
                 continue;
@@ -263,14 +259,27 @@
                         Log.e(TAG, "Ignored connect request for " + device + " : no state machine");
                         continue;
                     }
-                    sm.sendMessage(HearingAidStateMachine.CONNECT);
+                    sm.sendMessage(HearingAidStateMachine.CONNECT,
+                            sConnectTimeoutForEachSideMs);
+                    sm.sendMessageDelayed(HearingAidStateMachine.CHECK_WHITELIST_CONNECTION,
+                            sCheckWhitelistTimeoutMs);
                 }
-                if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID
-                        && !device.equals(storedDevice)) {
-                    break;
-                }
+                break;
             }
         }
+
+        synchronized (mStateMachines) {
+            HearingAidStateMachine smConnect = getOrCreateStateMachine(device);
+            if (smConnect == null) {
+                Log.e(TAG, "Cannot connect to " + device + " : no state machine");
+            } else {
+                smConnect.sendMessage(HearingAidStateMachine.CONNECT,
+                        sConnectTimeoutForEachSideMs * 2);
+                smConnect.sendMessageDelayed(HearingAidStateMachine.CHECK_WHITELIST_CONNECTION,
+                        sCheckWhitelistTimeoutMs);
+            }
+        }
+
         return true;
     }
 
@@ -405,6 +414,16 @@
         }
     }
 
+    /**
+     * Get the HiSyncIdMap for testing
+     *
+     * @return mDeviceHiSyncIdMap
+     */
+    @VisibleForTesting
+    Map<BluetoothDevice, Long> getHiSyncIdMap() {
+        return mDeviceHiSyncIdMap;
+    }
+
     int getConnectionState(BluetoothDevice device) {
         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
         synchronized (mStateMachines) {
@@ -658,6 +677,7 @@
         if (bondState != BluetoothDevice.BOND_NONE) {
             return;
         }
+        mDeviceHiSyncIdMap.remove(device);
         synchronized (mStateMachines) {
             HearingAidStateMachine sm = mStateMachines.get(device);
             if (sm == null) {
diff --git a/src/com/android/bluetooth/hearingaid/HearingAidStateMachine.java b/src/com/android/bluetooth/hearingaid/HearingAidStateMachine.java
index d02e102..3e0d617 100644
--- a/src/com/android/bluetooth/hearingaid/HearingAidStateMachine.java
+++ b/src/com/android/bluetooth/hearingaid/HearingAidStateMachine.java
@@ -69,13 +69,15 @@
 
     static final int CONNECT = 1;
     static final int DISCONNECT = 2;
+    static final int CHECK_WHITELIST_CONNECTION = 3;
     @VisibleForTesting
     static final int STACK_EVENT = 101;
     private static final int CONNECT_TIMEOUT = 201;
 
-    // NOTE: the value is not "final" - it is modified in the unit tests
-    @VisibleForTesting
-    static int sConnectTimeoutMs = 30000;        // 30s
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    static int sConnectTimeoutMs = 16000;
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    static int sDisconnectTimeoutMs = 16000;
 
     private Disconnected mDisconnected;
     private Connecting mConnecting;
@@ -172,6 +174,12 @@
                 case DISCONNECT:
                     Log.w(TAG, "Disconnected: DISCONNECT ignored: " + mDevice);
                     break;
+                case CHECK_WHITELIST_CONNECTION:
+                    if (mService.getConnectedDevices().isEmpty()) {
+                        log("No device connected, remove this device from white list");
+                        mNativeInterface.removeFromWhiteList(mDevice);
+                    }
+                    break;
                 case STACK_EVENT:
                     HearingAidStackEvent event = (HearingAidStackEvent) message.obj;
                     if (DBG) {
@@ -238,7 +246,9 @@
         public void enter() {
             Log.i(TAG, "Enter Connecting(" + mDevice + "): "
                     + messageWhatToString(getCurrentMessage().what));
-            sendMessageDelayed(CONNECT_TIMEOUT, sConnectTimeoutMs);
+            int timeout = getCurrentMessage().arg1 != 0
+                    ? getCurrentMessage().arg1 : sConnectTimeoutMs;
+            sendMessageDelayed(CONNECT_TIMEOUT, timeout);
             mConnectionState = BluetoothProfile.STATE_CONNECTING;
             broadcastConnectionState(mConnectionState, mLastConnectionState);
         }
@@ -261,14 +271,13 @@
                     deferMessage(message);
                     break;
                 case CONNECT_TIMEOUT:
-                    Log.w(TAG, "Connecting connection timeout: " + mDevice);
+                    Log.w(TAG, "Connecting connection timeout: " + mDevice + ". Try whitelist");
                     mNativeInterface.disconnectHearingAid(mDevice);
-                    HearingAidStackEvent disconnectEvent =
-                            new HearingAidStackEvent(
-                                    HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
-                    disconnectEvent.device = mDevice;
-                    disconnectEvent.valueInt1 = HearingAidStackEvent.CONNECTION_STATE_DISCONNECTED;
-                    sendMessage(STACK_EVENT, disconnectEvent);
+                    mNativeInterface.addToWhiteList(mDevice);
+                    transitionTo(mDisconnected);
+                    break;
+                case CHECK_WHITELIST_CONNECTION:
+                    deferMessage(message);
                     break;
                 case DISCONNECT:
                     log("Connecting: connection canceled to " + mDevice);
@@ -325,7 +334,7 @@
         public void enter() {
             Log.i(TAG, "Enter Disconnecting(" + mDevice + "): "
                     + messageWhatToString(getCurrentMessage().what));
-            sendMessageDelayed(CONNECT_TIMEOUT, sConnectTimeoutMs);
+            sendMessageDelayed(CONNECT_TIMEOUT, sDisconnectTimeoutMs);
             mConnectionState = BluetoothProfile.STATE_DISCONNECTING;
             broadcastConnectionState(mConnectionState, mLastConnectionState);
         }
diff --git a/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidServiceTest.java b/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidServiceTest.java
index b0a4755..bdc3c00 100644
--- a/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidServiceTest.java
@@ -118,6 +118,8 @@
                 .getBondState(any(BluetoothDevice.class));
         doReturn(new ParcelUuid[]{BluetoothUuid.HearingAid}).when(mAdapterService)
                 .getRemoteUuids(any(BluetoothDevice.class));
+        HearingAidService.sConnectTimeoutForEachSideMs = 1000;
+        HearingAidService.sCheckWhitelistTimeoutMs = 2000;
     }
 
     @After
@@ -343,7 +345,8 @@
                 mService.getConnectionState(mLeftDevice));
 
         // Verify the connection state broadcast, and that we are in Disconnected state
-        verifyConnectionStateIntent(HearingAidStateMachine.sConnectTimeoutMs * 2, mLeftDevice,
+        verifyConnectionStateIntent(HearingAidService.sConnectTimeoutForEachSideMs * 3,
+                mLeftDevice,
                 BluetoothProfile.STATE_DISCONNECTED,
                 BluetoothProfile.STATE_CONNECTING);
         Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED,
@@ -916,6 +919,31 @@
                 mService.getConnectionState(mLeftDevice));
     }
 
+    /**
+     * Test that the service can update HiSyncId from native message
+     */
+    @Test
+    public void getHiSyncIdFromNative_addToMap() {
+        getHiSyncIdFromNative();
+        Assert.assertTrue("hiSyncIdMap should contain mLeftDevice",
+                mService.getHiSyncIdMap().containsKey(mLeftDevice));
+        Assert.assertTrue("hiSyncIdMap should contain mRightDevice",
+                mService.getHiSyncIdMap().containsKey(mRightDevice));
+        Assert.assertTrue("hiSyncIdMap should contain mSingleDevice",
+                mService.getHiSyncIdMap().containsKey(mSingleDevice));
+    }
+
+    /**
+     * Test that the service removes the device from HiSyncIdMap when it's unbonded
+     */
+    @Test
+    public void deviceUnbonded_removeHiSyncId() {
+        getHiSyncIdFromNative();
+        mService.bondStateChanged(mLeftDevice, BluetoothDevice.BOND_NONE);
+        Assert.assertFalse("hiSyncIdMap shouldn't contain mLeftDevice",
+                mService.getHiSyncIdMap().containsKey(mLeftDevice));
+    }
+
     private void connectDevice(BluetoothDevice device) {
         HearingAidStackEvent connCompletedEvent;
 
diff --git a/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidStateMachineTest.java b/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidStateMachineTest.java
index e6d3ba2..474861e 100644
--- a/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidStateMachineTest.java
+++ b/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidStateMachineTest.java
@@ -77,7 +77,10 @@
         mHearingAidStateMachine = new HearingAidStateMachine(mTestDevice, mHearingAidService,
                 mHearingAidNativeInterface, mHandlerThread.getLooper());
         // Override the timeout value to speed up the test
-        mHearingAidStateMachine.sConnectTimeoutMs = 1000;     // 1s
+        mHearingAidStateMachine.sConnectTimeoutMs = 1000;
+        mHearingAidStateMachine.sDisconnectTimeoutMs = 1000;
+        HearingAidService.sConnectTimeoutForEachSideMs = 1000;
+        HearingAidService.sCheckWhitelistTimeoutMs = 2000;
         mHearingAidStateMachine.start();
     }
 
@@ -209,6 +212,7 @@
         // Check that we are in Disconnected state
         Assert.assertThat(mHearingAidStateMachine.getCurrentState(),
                 IsInstanceOf.instanceOf(HearingAidStateMachine.Disconnected.class));
+        verify(mHearingAidNativeInterface).addToWhiteList(eq(mTestDevice));
     }
 
     /**