Update input device selection APIs for Android S
This CL avoids using startBluetoothSco()/stopBluetoothSco() and
setSpeakerphoneOn()/isSpeakerPhoneOn() on Android S. These calls are
instead replaced by getAvailableCommunicationDevices() and
setCommunicationDevices().
This CL also checks for the BLUETOOTH_CONNECT permission, instead of
the pre-S BLUETOOTH permission.
Note: We only check for permissions without prompting for the moment.
A future CL will prompts users for "nearby devices" permission. In the
mean time, users can manually give "nearby devices" permissions to
Chrome to circumvent the attached issue.
Note: There are still issues with switching to a BT headset after a
page load, or after the device disconnects. Refreshing the page, or
refreshing + reconnecting the device normally fixes the issue.
Bug: 1317548
Change-Id: I94920aa81b7c79524099dd6d580e1e6f7bd334b6
Binary-Size: Replacing deprecated methods on Android S+
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3803424
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Commit-Queue: Thomas Guilbert <tguilbert@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1031251}
diff --git a/media/base/android/BUILD.gn b/media/base/android/BUILD.gn
index e2d9961b..7d45fe6f 100644
--- a/media/base/android/BUILD.gn
+++ b/media/base/android/BUILD.gn
@@ -179,6 +179,7 @@
sources = [
"java/src/org/chromium/media/AudioDeviceListener.java",
"java/src/org/chromium/media/AudioDeviceSelector.java",
+ "java/src/org/chromium/media/AudioDeviceSelectorPostS.java",
"java/src/org/chromium/media/AudioDeviceSelectorPreS.java",
"java/src/org/chromium/media/AudioManagerAndroid.java",
"java/src/org/chromium/media/AudioTrackOutputStream.java",
diff --git a/media/base/android/java/src/org/chromium/media/AudioDeviceSelector.java b/media/base/android/java/src/org/chromium/media/AudioDeviceSelector.java
index 2c4f57b..84616da 100644
--- a/media/base/android/java/src/org/chromium/media/AudioDeviceSelector.java
+++ b/media/base/android/java/src/org/chromium/media/AudioDeviceSelector.java
@@ -64,6 +64,12 @@
*/
protected abstract void setAudioDevice(int device);
+ public abstract boolean[] getAvailableDevices_Locked();
+
+ public void setDeviceExistence_Locked(int deviceId, boolean exists) {
+ // Overridden by AudioDeviceSelectorPreS.
+ }
+
public AudioManagerAndroid.AudioDeviceName[] getAudioInputDeviceNames() {
return mDeviceStates.getAudioInputDeviceNames();
}
@@ -78,6 +84,8 @@
int nextDevice = mDeviceStates.setRequestedDeviceIdAndGetNextId(deviceId);
+ if (DEBUG) logd("selectDevice: id=" + DeviceHelpers.getDeviceName(nextDevice));
+
// `deviceId` is invalid, or its corresponding device is not available.
if (nextDevice == Devices.ID_INVALID) return false;
@@ -93,6 +101,8 @@
protected void maybeUpdateSelectedDevice() {
int nextDevice = mDeviceStates.getNextDeviceIfRequested();
+ if (DEBUG) logd("maybeUpdateSelectedDevice: id=" + DeviceHelpers.getDeviceName(nextDevice));
+
// No device was explicitly requested.
if (nextDevice == Devices.ID_INVALID) return;
@@ -100,7 +110,7 @@
}
// Collection of static helpers.
- private static class DeviceHelpers {
+ protected static class DeviceHelpers {
// Maps audio device types to string values. This map must be in sync
// with the Devices.ID_* below.
// TODO(henrika): add support for proper detection of device names and
@@ -117,6 +127,14 @@
private static final int ID_VALID_LOWER_BOUND = Devices.ID_SPEAKERPHONE;
private static final int ID_VALID_UPPER_BOUND = Devices.ID_USB_AUDIO;
+ public static String getDeviceName(int deviceId) {
+ if (deviceId == Devices.ID_INVALID) return "invalid-ID";
+
+ if (deviceId == Devices.ID_DEFAULT) return "default-device";
+
+ return DEVICE_NAMES[deviceId];
+ }
+
/**
* Use a special selection scheme if the default device is selected.
* The "most unique" device will be selected; Wired headset first, then USB
@@ -170,14 +188,12 @@
public static final int ID_EARPIECE = 2;
public static final int ID_BLUETOOTH_HEADSET = 3;
public static final int ID_USB_AUDIO = 4;
- private static final int DEVICE_COUNT = 5;
+ public static final int DEVICE_COUNT = 5;
private Object mLock = new Object();
private int mRequestedAudioDevice = ID_INVALID;
- private boolean[] mDeviceExistence = new boolean[DEVICE_COUNT];
-
/**
* Sets the whether a device exists.
*
@@ -187,8 +203,10 @@
public void setDeviceExistence(int deviceId, boolean exists) {
if (!DeviceHelpers.isDeviceValid(deviceId)) return;
+ if (DEBUG) logd("Setting [" + DeviceHelpers.getDeviceName(deviceId) + "]=" + exists);
+
synchronized (mLock) {
- mDeviceExistence[deviceId] = exists;
+ setDeviceExistence_Locked(deviceId, exists);
}
}
@@ -243,26 +261,6 @@
}
/**
- * Computes the list of available devices based off of exiting devices.
- * We consider the availability of wired headsets, USB audio and earpices to be
- * mutually exclusive.
- */
- private boolean[] getAvailableDevices_Locked() {
- boolean[] availableDevices = mDeviceExistence.clone();
-
- // Wired headset, USB audio and earpiece are mutually exclusive, and
- // prioritized in that order.
- if (availableDevices[ID_WIRED_HEADSET]) {
- availableDevices[ID_USB_AUDIO] = false;
- availableDevices[ID_EARPIECE] = false;
- } else if (availableDevices[ID_USB_AUDIO]) {
- availableDevices[ID_EARPIECE] = false;
- }
-
- return availableDevices;
- }
-
- /**
* Returns the list of currently available devices, to be used by the native side.
*/
public AudioManagerAndroid.AudioDeviceName[] getAudioInputDeviceNames() {
@@ -271,7 +269,9 @@
devices = getAvailableDevices_Locked();
}
List<String> list = new ArrayList<String>();
+
int activeDeviceCount = DeviceHelpers.getActiveDeviceCount(devices);
+
AudioManagerAndroid.AudioDeviceName[] array =
new AudioManagerAndroid.AudioDeviceName[activeDeviceCount];
@@ -289,6 +289,7 @@
}
public void clearRequestedDevice() {
+ if (DEBUG) logd("Clearing requested device");
synchronized (mLock) {
mRequestedAudioDevice = ID_INVALID;
}
diff --git a/media/base/android/java/src/org/chromium/media/AudioDeviceSelectorPostS.java b/media/base/android/java/src/org/chromium/media/AudioDeviceSelectorPostS.java
new file mode 100644
index 0000000..0831947b
--- /dev/null
+++ b/media/base/android/java/src/org/chromium/media/AudioDeviceSelectorPostS.java
@@ -0,0 +1,168 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.media;
+
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+import org.chromium.base.Log;
+import org.chromium.base.compat.ApiHelperForS;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RequiresApi(Build.VERSION_CODES.S)
+class AudioDeviceSelectorPostS extends AudioDeviceSelector {
+ private static final String TAG = "media";
+
+ private boolean mHasBluetoothConnectPermission;
+
+ public AudioDeviceSelectorPostS(AudioManager audioManager) {
+ super(audioManager);
+ }
+
+ private static List<Integer> getTargetTypesFromId(int deviceId) {
+ List<Integer> types = new ArrayList<Integer>();
+
+ switch (deviceId) {
+ case Devices.ID_SPEAKERPHONE:
+ types.add(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
+ break;
+ case Devices.ID_WIRED_HEADSET:
+ types.add(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+ types.add(AudioDeviceInfo.TYPE_WIRED_HEADPHONES);
+ break;
+ case Devices.ID_EARPIECE:
+ types.add(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE);
+ break;
+ case Devices.ID_BLUETOOTH_HEADSET:
+ types.add(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ types.add(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
+ break;
+ case Devices.ID_USB_AUDIO:
+ types.add(AudioDeviceInfo.TYPE_USB_HEADSET);
+ types.add(AudioDeviceInfo.TYPE_USB_DEVICE);
+ break;
+ }
+
+ return types;
+ }
+
+ @Override
+ public void init() {
+ mHasBluetoothConnectPermission = ApiHelperForS.hasBluetoothConnectPermission();
+
+ if (!mHasBluetoothConnectPermission) {
+ Log.w(TAG, "BLUETOOTH_CONNECT permission is missing.");
+ }
+
+ mDeviceListener.init(mHasBluetoothConnectPermission);
+ }
+
+ @Override
+ public void close() {
+ mDeviceListener.close();
+ }
+
+ @Override
+ public void setCommunicationAudioModeOn(boolean on) {
+ if (on) {
+ // TODO(crbug.com/1317548): Prompt for BLUETOOTH_CONNECT permission at this point if we
+ // don't have it.
+ } else {
+ mDeviceStates.clearRequestedDevice();
+ mAudioManager.clearCommunicationDevice();
+ }
+ }
+
+ @Override
+ public boolean isSpeakerphoneOn() {
+ AudioDeviceInfo currentDevice = mAudioManager.getCommunicationDevice();
+ return currentDevice != null
+ && currentDevice.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
+ }
+
+ @Override
+ public void setSpeakerphoneOn(boolean on) {
+ boolean isCurrentlyOn = isSpeakerphoneOn();
+
+ if (isCurrentlyOn == on) return;
+
+ if (on) {
+ setAudioDevice(Devices.ID_SPEAKERPHONE);
+ } else {
+ // Turn speakerphone OFF.
+ mAudioManager.clearCommunicationDevice();
+ maybeUpdateSelectedDevice();
+ }
+ }
+
+ @Override
+ public boolean[] getAvailableDevices_Locked() {
+ List<AudioDeviceInfo> communicationDevices =
+ mAudioManager.getAvailableCommunicationDevices();
+
+ boolean[] availableDevices = new boolean[Devices.DEVICE_COUNT];
+
+ for (AudioDeviceInfo device : communicationDevices) {
+ switch (device.getType()) {
+ case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
+ availableDevices[Devices.ID_SPEAKERPHONE] = true;
+ break;
+
+ case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
+ case AudioDeviceInfo.TYPE_WIRED_HEADSET:
+ availableDevices[Devices.ID_WIRED_HEADSET] = true;
+ break;
+
+ case AudioDeviceInfo.TYPE_USB_DEVICE:
+ case AudioDeviceInfo.TYPE_USB_HEADSET:
+ availableDevices[Devices.ID_USB_AUDIO] = true;
+ break;
+
+ case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
+ case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
+ availableDevices[Devices.ID_BLUETOOTH_HEADSET] = true;
+ break;
+
+ case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE:
+ availableDevices[Devices.ID_EARPIECE] = true;
+ break;
+ }
+ }
+
+ return availableDevices;
+ }
+
+ public AudioDeviceInfo getMatchingCommunicationDevice(List<Integer> targetTypes) {
+ List<AudioDeviceInfo> availableDevices = mAudioManager.getAvailableCommunicationDevices();
+
+ for (AudioDeviceInfo device : availableDevices) {
+ if (targetTypes.contains(device.getType())) return device;
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void setAudioDevice(int deviceId) {
+ if (!DeviceHelpers.isDeviceValid(deviceId)) return;
+
+ AudioDeviceInfo targetDevice =
+ getMatchingCommunicationDevice(getTargetTypesFromId(deviceId));
+
+ if (targetDevice != null) {
+ boolean result = mAudioManager.setCommunicationDevice(targetDevice);
+ if (!result) {
+ loge("Error setting communication device");
+ }
+ } else {
+ loge("Couldn't find available device for: " + DeviceHelpers.getDeviceName(deviceId));
+ }
+ }
+}
\ No newline at end of file
diff --git a/media/base/android/java/src/org/chromium/media/AudioDeviceSelectorPreS.java b/media/base/android/java/src/org/chromium/media/AudioDeviceSelectorPreS.java
index 527da8b..87228b0 100644
--- a/media/base/android/java/src/org/chromium/media/AudioDeviceSelectorPreS.java
+++ b/media/base/android/java/src/org/chromium/media/AudioDeviceSelectorPreS.java
@@ -10,10 +10,8 @@
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.media.AudioManager;
-import android.os.Build;
import org.chromium.base.ContextUtils;
-import org.chromium.base.compat.ApiHelperForS;
class AudioDeviceSelectorPreS extends AudioDeviceSelector {
private static final String TAG = "media";
@@ -33,6 +31,8 @@
private boolean mHasBluetoothPermission;
+ private boolean[] mDeviceExistence = new boolean[Devices.DEVICE_COUNT];
+
public AudioDeviceSelectorPreS(AudioManager audioManager) {
super(audioManager);
}
@@ -45,12 +45,6 @@
public void init() {
mHasBluetoothPermission = hasPermission(android.Manifest.permission.BLUETOOTH);
- // TODO(crbug.com/1317548): Remove this check once there is an AudioDeviceSelector
- // for S and above.
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- mHasBluetoothPermission &= ApiHelperForS.hasBluetoothConnectPermission();
- }
-
mDeviceListener.init(mHasBluetoothPermission);
if (mHasBluetoothPermission) registerForBluetoothScoIntentBroadcast();
@@ -84,6 +78,27 @@
mAudioManager.setSpeakerphoneOn(on);
}
+ @Override
+ public boolean[] getAvailableDevices_Locked() {
+ boolean[] availableDevices = mDeviceExistence.clone();
+
+ // Wired headset, USB audio and earpiece are mutually exclusive, and
+ // prioritized in that order.
+ if (availableDevices[Devices.ID_WIRED_HEADSET]) {
+ availableDevices[Devices.ID_USB_AUDIO] = false;
+ availableDevices[Devices.ID_EARPIECE] = false;
+ } else if (availableDevices[Devices.ID_USB_AUDIO]) {
+ availableDevices[Devices.ID_EARPIECE] = false;
+ }
+
+ return availableDevices;
+ }
+
+ @Override
+ public void setDeviceExistence_Locked(int deviceId, boolean exists) {
+ mDeviceExistence[deviceId] = exists;
+ }
+
/** Checks if the process has as specified permission or not. */
private boolean hasPermission(String permission) {
return ContextUtils.getApplicationContext().checkSelfPermission(permission)
@@ -125,7 +140,7 @@
// do nothing
break;
default:
- loge("Invalid state");
+ break;
}
}
};
diff --git a/media/base/android/java/src/org/chromium/media/AudioManagerAndroid.java b/media/base/android/java/src/org/chromium/media/AudioManagerAndroid.java
index 2056357b..bf772e2 100644
--- a/media/base/android/java/src/org/chromium/media/AudioManagerAndroid.java
+++ b/media/base/android/java/src/org/chromium/media/AudioManagerAndroid.java
@@ -98,9 +98,11 @@
Context.AUDIO_SERVICE);
mContentResolver = ContextUtils.getApplicationContext().getContentResolver();
- // TODO(crbug.com/1317548): For now, we also use AudioDeviceSelectorPreS on Android S and
- // above. Fix this by adding an S and above implementation.
- mAudioDeviceSelector = new AudioDeviceSelectorPreS(mAudioManager);
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+ mAudioDeviceSelector = new AudioDeviceSelectorPreS(mAudioManager);
+ } else {
+ mAudioDeviceSelector = new AudioDeviceSelectorPostS(mAudioManager);
+ }
}
/**