Multi-HFP device support for audio selector.

Test: BluetoothDeviceProviderTest
PiperOrigin-RevId: 193401073
Change-Id: I3483392d1d7a473a753477074b5c377af257186e
diff --git a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java
index 2d3ef19..11e952c 100644
--- a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java
+++ b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java
@@ -39,6 +39,7 @@
 import com.android.dialer.speeddial.loader.UiItemLoaderComponent;
 import com.android.dialer.storage.StorageComponent;
 import com.android.dialer.strictmode.StrictModeComponent;
+import com.android.incallui.audiomode.BluetoothDeviceProviderComponent;
 import com.android.incallui.calllocation.CallLocationComponent;
 import com.android.incallui.maps.MapsComponent;
 import com.android.incallui.speakeasy.SpeakEasyComponent;
@@ -49,7 +50,8 @@
  * from this component.
  */
 public interface BaseDialerRootComponent
-    extends BubbleComponent.HasComponent,
+    extends BluetoothDeviceProviderComponent.HasComponent,
+        BubbleComponent.HasComponent,
         CallLocationComponent.HasComponent,
         CallLogComponent.HasComponent,
         CallLogConfigComponent.HasComponent,
diff --git a/java/com/android/incallui/AndroidManifest.xml b/java/com/android/incallui/AndroidManifest.xml
index 9a762fe..832a5e8 100644
--- a/java/com/android/incallui/AndroidManifest.xml
+++ b/java/com/android/incallui/AndroidManifest.xml
@@ -40,6 +40,9 @@
   <!-- Testing location -->
   <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
 
+  <!-- Set Bluetooth device -->
+  <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
+
   <!-- Set android:taskAffinity="com.android.incallui" for all activities to ensure proper
   navigation. Otherwise system could bring up DialtactsActivity instead, e.g. when user unmerge a
   call.
diff --git a/java/com/android/incallui/InCallServiceImpl.java b/java/com/android/incallui/InCallServiceImpl.java
index 6b463ce..29a65b9 100644
--- a/java/com/android/incallui/InCallServiceImpl.java
+++ b/java/com/android/incallui/InCallServiceImpl.java
@@ -26,6 +26,7 @@
 import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
 import com.android.dialer.feedback.FeedbackComponent;
 import com.android.incallui.audiomode.AudioModeProvider;
+import com.android.incallui.audiomode.BluetoothDeviceProviderComponent;
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.ExternalCallList;
 import com.android.incallui.call.TelecomAdapter;
@@ -97,6 +98,7 @@
     final Context context = getApplicationContext();
     final ContactInfoCache contactInfoCache = ContactInfoCache.getInstance(context);
     AudioModeProvider.getInstance().initializeAudioState(this);
+    BluetoothDeviceProviderComponent.get(context).bluetoothDeviceProvider().setUp();
     InCallPresenter.getInstance()
         .setUp(
             context,
@@ -141,6 +143,7 @@
     // Tear down the InCall system
     InCallPresenter.getInstance().tearDown();
     TelecomAdapter.getInstance().clearInCallService();
+    BluetoothDeviceProviderComponent.get(this).bluetoothDeviceProvider().tearDown();
     if (returnToCallController != null) {
       returnToCallController.tearDown();
       returnToCallController = null;
diff --git a/java/com/android/incallui/audiomode/BluetoothDeviceProvider.java b/java/com/android/incallui/audiomode/BluetoothDeviceProvider.java
new file mode 100644
index 0000000..1aa1c20
--- /dev/null
+++ b/java/com/android/incallui/audiomode/BluetoothDeviceProvider.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.audiomode;
+
+import android.annotation.SuppressLint;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothProfile;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.util.ArraySet;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.inject.ApplicationContext;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Set;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/** Proxy class for getting and setting connected/active Bluetooth devices. */
+@Singleton
+public final class BluetoothDeviceProvider extends BroadcastReceiver {
+
+  // TODO(yueg): use BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED when possible
+  private static final String ACTION_ACTIVE_DEVICE_CHANGED =
+      "android.bluetooth.headset.profile.action.ACTIVE_DEVICE_CHANGED";
+
+  private final Context appContext;
+  private final BluetoothProfileServiceListener bluetoothProfileServiceListener =
+      new BluetoothProfileServiceListener();
+
+  private final Set<BluetoothDevice> connectedBluetoothDeviceSet = new ArraySet<>();
+
+  private BluetoothDevice activeBluetoothDevice;
+  private BluetoothHeadset bluetoothHeadset;
+  private boolean isSetUp;
+
+  @Inject
+  public BluetoothDeviceProvider(@ApplicationContext Context appContext) {
+    this.appContext = appContext;
+  }
+
+  public void setUp() {
+    if (BluetoothAdapter.getDefaultAdapter() == null) {
+      // Bluetooth is not supported on this hardware platform
+      return;
+    }
+    // Get Bluetooth service including the initial connected device list (should only contain one
+    // device)
+    BluetoothAdapter.getDefaultAdapter()
+        .getProfileProxy(appContext, bluetoothProfileServiceListener, BluetoothProfile.HEADSET);
+    // Get notified of Bluetooth device update
+    IntentFilter filter = new IntentFilter();
+    filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
+    filter.addAction(ACTION_ACTIVE_DEVICE_CHANGED);
+    appContext.registerReceiver(this, filter);
+
+    isSetUp = true;
+  }
+
+  public void tearDown() {
+    if (!isSetUp) {
+      return;
+    }
+    appContext.unregisterReceiver(this);
+    if (bluetoothHeadset != null) {
+      BluetoothAdapter.getDefaultAdapter()
+          .closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
+    }
+  }
+
+  public Set<BluetoothDevice> getConnectedBluetoothDeviceSet() {
+    return connectedBluetoothDeviceSet;
+  }
+
+  public BluetoothDevice getActiveBluetoothDevice() {
+    return activeBluetoothDevice;
+  }
+
+  @SuppressLint("PrivateApi")
+  public void setActiveBluetoothDevice(BluetoothDevice bluetoothDevice) {
+    if (!connectedBluetoothDeviceSet.contains(bluetoothDevice)) {
+      LogUtil.e("BluetoothProfileServiceListener.setActiveBluetoothDevice", "device is not in set");
+      return;
+    }
+    // TODO(yueg): use BluetoothHeadset.setActiveDevice() when possible
+    try {
+      Method getActiveDeviceMethod =
+          bluetoothHeadset.getClass().getDeclaredMethod("setActiveDevice", BluetoothDevice.class);
+      getActiveDeviceMethod.setAccessible(true);
+      getActiveDeviceMethod.invoke(bluetoothHeadset, bluetoothDevice);
+    } catch (Exception e) {
+      LogUtil.e(
+          "BluetoothProfileServiceListener.setActiveBluetoothDevice",
+          "failed to call setActiveDevice",
+          e);
+    }
+  }
+
+  @Override
+  public void onReceive(Context context, Intent intent) {
+    if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
+      handleActionConnectionStateChanged(intent);
+    } else if (ACTION_ACTIVE_DEVICE_CHANGED.equals(intent.getAction())) {
+      handleActionActiveDeviceChanged(intent);
+    }
+  }
+
+  private void handleActionConnectionStateChanged(Intent intent) {
+    if (!intent.hasExtra(BluetoothDevice.EXTRA_DEVICE)) {
+      LogUtil.i(
+          "BluetoothDeviceProvider.handleActionConnectionStateChanged",
+          "extra BluetoothDevice.EXTRA_DEVICE not found");
+      return;
+    }
+    BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+    if (bluetoothDevice == null) {
+      return;
+    }
+
+    int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
+    if (state == BluetoothProfile.STATE_DISCONNECTED) {
+      connectedBluetoothDeviceSet.remove(bluetoothDevice);
+      LogUtil.i("BluetoothDeviceProvider.handleActionConnectionStateChanged", "device removed");
+    } else if (state == BluetoothProfile.STATE_CONNECTED) {
+      connectedBluetoothDeviceSet.add(bluetoothDevice);
+      LogUtil.i("BluetoothDeviceProvider.handleActionConnectionStateChanged", "device added");
+    }
+  }
+
+  private void handleActionActiveDeviceChanged(Intent intent) {
+    if (!intent.hasExtra(BluetoothDevice.EXTRA_DEVICE)) {
+      LogUtil.i(
+          "BluetoothDeviceProvider.handleActionActiveDeviceChanged",
+          "extra BluetoothDevice.EXTRA_DEVICE not found");
+      return;
+    }
+    activeBluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+    LogUtil.i(
+        "BluetoothDeviceProvider.handleActionActiveDeviceChanged",
+        (activeBluetoothDevice == null ? "null" : ""));
+  }
+
+  private final class BluetoothProfileServiceListener implements BluetoothProfile.ServiceListener {
+    @Override
+    @SuppressLint("PrivateApi")
+    public void onServiceConnected(int profile, BluetoothProfile bluetoothProfile) {
+      if (profile != BluetoothProfile.HEADSET) {
+        return;
+      }
+      // Get initial connected device list
+      bluetoothHeadset = (BluetoothHeadset) bluetoothProfile;
+      List<BluetoothDevice> devices = bluetoothProfile.getConnectedDevices();
+      for (BluetoothDevice device : devices) {
+        connectedBluetoothDeviceSet.add(device);
+        LogUtil.i(
+            "BluetoothProfileServiceListener.onServiceConnected", "get initial connected device");
+      }
+
+      // Get initial active device
+      // TODO(yueg): use BluetoothHeadset.getActiveDevice() when possible
+      try {
+        Method getActiveDeviceMethod =
+            bluetoothHeadset.getClass().getDeclaredMethod("getActiveDevice");
+        getActiveDeviceMethod.setAccessible(true);
+        activeBluetoothDevice = (BluetoothDevice) getActiveDeviceMethod.invoke(bluetoothHeadset);
+        LogUtil.i(
+            "BluetoothProfileServiceListener.onServiceConnected",
+            "get initial active device" + ((activeBluetoothDevice == null) ? " null" : ""));
+      } catch (Exception e) {
+        LogUtil.e(
+            "BluetoothProfileServiceListener.onServiceConnected",
+            "failed to call getAcitveDevice",
+            e);
+      }
+    }
+
+    @Override
+    public void onServiceDisconnected(int profile) {
+      LogUtil.enterBlock("BluetoothProfileServiceListener.onServiceDisconnected");
+      if (profile == BluetoothProfile.HEADSET) {
+        bluetoothHeadset = null;
+      }
+    }
+  }
+}
diff --git a/java/com/android/incallui/audiomode/BluetoothDeviceProviderComponent.java b/java/com/android/incallui/audiomode/BluetoothDeviceProviderComponent.java
new file mode 100644
index 0000000..9cd9268
--- /dev/null
+++ b/java/com/android/incallui/audiomode/BluetoothDeviceProviderComponent.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.audiomode;
+
+import android.content.Context;
+import com.android.dialer.inject.HasRootComponent;
+import dagger.Subcomponent;
+
+/** Dagger component for the Bluetooth device provider. */
+@Subcomponent
+public abstract class BluetoothDeviceProviderComponent {
+
+  public abstract BluetoothDeviceProvider bluetoothDeviceProvider();
+
+  public static BluetoothDeviceProviderComponent get(Context context) {
+    return ((BluetoothDeviceProviderComponent.HasComponent)
+            ((HasRootComponent) context.getApplicationContext()).component())
+        .bluetoothDeviceProviderComponent();
+  }
+
+  /** Used to refer to the root application component. */
+  public interface HasComponent {
+    BluetoothDeviceProviderComponent bluetoothDeviceProviderComponent();
+  }
+}
diff --git a/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java b/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java
index d481f43..79cae09 100644
--- a/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java
+++ b/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java
@@ -17,6 +17,7 @@
 package com.android.incallui.audioroute;
 
 import android.app.Dialog;
+import android.bluetooth.BluetoothDevice;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.res.ColorStateList;
@@ -29,9 +30,12 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowManager;
+import android.widget.LinearLayout;
 import android.widget.TextView;
 import com.android.dialer.common.FragmentUtils;
 import com.android.dialer.common.LogUtil;
+import com.android.incallui.audiomode.BluetoothDeviceProviderComponent;
+import java.util.Set;
 
 /** Shows picker for audio routes */
 public class AudioRouteSelectorDialogFragment extends BottomSheetDialogFragment {
@@ -75,10 +79,22 @@
     View view = layoutInflater.inflate(R.layout.audioroute_selector, viewGroup, false);
     CallAudioState audioState = getArguments().getParcelable(ARG_AUDIO_STATE);
 
-    initItem(
-        (TextView) view.findViewById(R.id.audioroute_bluetooth),
-        CallAudioState.ROUTE_BLUETOOTH,
-        audioState);
+    Set<BluetoothDevice> bluetoothDeviceSet =
+        BluetoothDeviceProviderComponent.get(getContext())
+            .bluetoothDeviceProvider()
+            .getConnectedBluetoothDeviceSet();
+    for (BluetoothDevice device : bluetoothDeviceSet) {
+      boolean selected =
+          (audioState.getRoute() == CallAudioState.ROUTE_BLUETOOTH)
+              && (bluetoothDeviceSet.size() == 1
+                  || device.equals(
+                      BluetoothDeviceProviderComponent.get(getContext())
+                          .bluetoothDeviceProvider()
+                          .getActiveBluetoothDevice()));
+      TextView textView = createBluetoothItem(device, selected);
+      ((LinearLayout) view).addView(textView, 0);
+    }
+
     initItem(
         (TextView) view.findViewById(R.id.audioroute_speaker),
         CallAudioState.ROUTE_SPEAKER,
@@ -121,4 +137,30 @@
               .onAudioRouteSelected(itemRoute);
         });
   }
+
+  private TextView createBluetoothItem(BluetoothDevice bluetoothDevice, boolean selected) {
+    int selectedColor = getResources().getColor(R.color.dialer_theme_color);
+    TextView textView =
+        (TextView) getLayoutInflater().inflate(R.layout.audioroute_item, null, false);
+    textView.setText(bluetoothDevice.getName());
+    if (selected) {
+      textView.setTextColor(selectedColor);
+      textView.setCompoundDrawableTintList(ColorStateList.valueOf(selectedColor));
+      textView.setCompoundDrawableTintMode(Mode.SRC_ATOP);
+    }
+    textView.setOnClickListener(
+        (v) -> {
+          dismiss();
+          // Set Bluetooth audio route
+          FragmentUtils.getParentUnsafe(
+                  AudioRouteSelectorDialogFragment.this, AudioRouteSelectorPresenter.class)
+              .onAudioRouteSelected(CallAudioState.ROUTE_BLUETOOTH);
+          // Set active Bluetooth device
+          BluetoothDeviceProviderComponent.get(getContext())
+              .bluetoothDeviceProvider()
+              .setActiveBluetoothDevice(bluetoothDevice);
+        });
+
+    return textView;
+  }
 }
diff --git a/java/com/android/incallui/audioroute/res/layout/audioroute_item.xml b/java/com/android/incallui/audioroute/res/layout/audioroute_item.xml
new file mode 100644
index 0000000..66c83f6
--- /dev/null
+++ b/java/com/android/incallui/audioroute/res/layout/audioroute_item.xml
@@ -0,0 +1,21 @@
+<!--
+~ Copyright (C) 2018 The Android Open Source Project
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~      http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License
+-->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/AudioRouteItem"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:drawableStart="@drawable/quantum_ic_bluetooth_audio_vd_theme_24"
+    android:drawableTint="@color/material_grey_600"/>
\ No newline at end of file
diff --git a/java/com/android/incallui/audioroute/res/layout/audioroute_selector.xml b/java/com/android/incallui/audioroute/res/layout/audioroute_selector.xml
index ef2220e..145101d 100644
--- a/java/com/android/incallui/audioroute/res/layout/audioroute_selector.xml
+++ b/java/com/android/incallui/audioroute/res/layout/audioroute_selector.xml
@@ -1,4 +1,18 @@
-<?xml version="1.0" encoding="utf-8"?>
+<!--
+~ Copyright (C) 2018 The Android Open Source Project
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~      http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License
+-->
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
@@ -6,13 +20,6 @@
   android:orientation="vertical"
   tools:layout_gravity="bottom">
   <TextView
-    android:id="@+id/audioroute_bluetooth"
-    style="@style/AudioRouteItem"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:drawableStart="@drawable/quantum_ic_bluetooth_audio_grey600_24"
-    android:text="@string/audioroute_bluetooth"/>
-  <TextView
     android:id="@+id/audioroute_speaker"
     style="@style/AudioRouteItem"
     android:layout_width="match_parent"