summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEric Laurent <elaurent@google.com>2023-12-03 20:56:25 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2023-12-03 20:56:25 +0000
commitfefdc2b7de20c0be8de12afe07e75e1776753d4d (patch)
treec7b3ff114abbc512b859b29e36a9e6b107b93250
parent08b86ffd373550238677b5abb61545c95509f91e (diff)
parentddf629a3fd05d133ef41009a6c916d53cd31b97a (diff)
downloadbase-fefdc2b7de20c0be8de12afe07e75e1776753d4d.tar.gz
Merge changes from topic "anon_bt_address_sc_qpr1_dev" into sc-qpr1-dev
* changes: AudioService: anonymize Bluetooth MAC addresses Refactor the SADeviceState to AdiDeviceState
-rw-r--r--core/java/android/provider/Settings.java7
-rw-r--r--media/java/android/media/AudioDeviceAttributes.java24
-rw-r--r--media/java/android/media/AudioSystem.java63
-rw-r--r--packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java1
-rw-r--r--services/core/java/com/android/server/audio/AdiDeviceState.java215
-rw-r--r--services/core/java/com/android/server/audio/AudioDeviceBroker.java87
-rw-r--r--services/core/java/com/android/server/audio/AudioDeviceInventory.java126
-rw-r--r--services/core/java/com/android/server/audio/AudioService.java153
-rw-r--r--services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java32
9 files changed, 689 insertions, 19 deletions
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 58b9feb6a905..18549699bf07 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -9000,6 +9000,13 @@ public final class Settings {
public static final String UNSAFE_VOLUME_MUSIC_ACTIVE_MS = "unsafe_volume_music_active_ms";
/**
+ * Internal collection of audio device inventory items
+ * The device item stored are {@link com.android.server.audio.AdiDeviceState}
+ * @hide
+ */
+ public static final String AUDIO_DEVICE_INVENTORY = "audio_device_inventory";
+
+ /**
* Indicates whether notification display on the lock screen is enabled.
* <p>
* Type: int (0 for false, 1 for true)
diff --git a/media/java/android/media/AudioDeviceAttributes.java b/media/java/android/media/AudioDeviceAttributes.java
index 7caac899a603..5341e7e5adc0 100644
--- a/media/java/android/media/AudioDeviceAttributes.java
+++ b/media/java/android/media/AudioDeviceAttributes.java
@@ -64,8 +64,7 @@ public final class AudioDeviceAttributes implements Parcelable {
/**
* The unique address of the device. Some devices don't have addresses, only an empty string.
*/
- private final @NonNull String mAddress;
-
+ private @NonNull String mAddress;
/**
* Is input or output device
*/
@@ -135,6 +134,18 @@ public final class AudioDeviceAttributes implements Parcelable {
/**
* @hide
+ * Copy Constructor.
+ * @param ada the copied AudioDeviceAttributes
+ */
+ public AudioDeviceAttributes(AudioDeviceAttributes ada) {
+ mRole = ada.getRole();
+ mType = ada.getType();
+ mAddress = ada.getAddress();
+ mNativeType = ada.getInternalType();
+ }
+
+ /**
+ * @hide
* Returns the role of a device
* @return the role
*/
@@ -165,6 +176,15 @@ public final class AudioDeviceAttributes implements Parcelable {
/**
* @hide
+ * Sets the device address. Only used by audio service.
+ */
+ public void setAddress(@NonNull String address) {
+ Objects.requireNonNull(address);
+ mAddress = address;
+ }
+
+ /**
+ * @hide
* Returns the internal device type of a device
* @return the internal device type
*/
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index 69d1889d5762..9736f8d8bd63 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -1119,6 +1119,9 @@ public class AudioSystem
public static final Set<Integer> DEVICE_IN_ALL_SCO_SET;
/** @hide */
public static final Set<Integer> DEVICE_IN_ALL_USB_SET;
+ /** @hide */
+ public static final Set<Integer> DEVICE_IN_ALL_BLE_SET;
+
static {
DEVICE_IN_ALL_SET = new HashSet<>();
DEVICE_IN_ALL_SET.add(DEVICE_IN_COMMUNICATION);
@@ -1158,6 +1161,66 @@ public class AudioSystem
DEVICE_IN_ALL_USB_SET.add(DEVICE_IN_USB_ACCESSORY);
DEVICE_IN_ALL_USB_SET.add(DEVICE_IN_USB_DEVICE);
DEVICE_IN_ALL_USB_SET.add(DEVICE_IN_USB_HEADSET);
+
+ DEVICE_IN_ALL_BLE_SET = new HashSet<>();
+ DEVICE_IN_ALL_BLE_SET.add(DEVICE_IN_BLE_HEADSET);
+ }
+
+ /** @hide */
+ public static boolean isBluetoothDevice(int deviceType) {
+ return isBluetoothA2dpOutDevice(deviceType)
+ || isBluetoothScoDevice(deviceType)
+ || isBluetoothLeDevice(deviceType);
+ }
+
+ /** @hide */
+ public static boolean isBluetoothOutDevice(int deviceType) {
+ return isBluetoothA2dpOutDevice(deviceType)
+ || isBluetoothScoOutDevice(deviceType)
+ || isBluetoothLeOutDevice(deviceType);
+ }
+
+ /** @hide */
+ public static boolean isBluetoothInDevice(int deviceType) {
+ return isBluetoothScoInDevice(deviceType)
+ || isBluetoothLeInDevice(deviceType);
+ }
+
+ /** @hide */
+ public static boolean isBluetoothA2dpOutDevice(int deviceType) {
+ return DEVICE_OUT_ALL_A2DP_SET.contains(deviceType);
+ }
+
+ /** @hide */
+ public static boolean isBluetoothScoOutDevice(int deviceType) {
+ return DEVICE_OUT_ALL_SCO_SET.contains(deviceType);
+ }
+
+ /** @hide */
+ public static boolean isBluetoothScoInDevice(int deviceType) {
+ return DEVICE_IN_ALL_SCO_SET.contains(deviceType);
+ }
+
+ /** @hide */
+ public static boolean isBluetoothScoDevice(int deviceType) {
+ return isBluetoothScoOutDevice(deviceType)
+ || isBluetoothScoInDevice(deviceType);
+ }
+
+ /** @hide */
+ public static boolean isBluetoothLeOutDevice(int deviceType) {
+ return DEVICE_OUT_ALL_BLE_SET.contains(deviceType);
+ }
+
+ /** @hide */
+ public static boolean isBluetoothLeInDevice(int deviceType) {
+ return DEVICE_IN_ALL_BLE_SET.contains(deviceType);
+ }
+
+ /** @hide */
+ public static boolean isBluetoothLeDevice(int deviceType) {
+ return isBluetoothLeOutDevice(deviceType)
+ || isBluetoothLeInDevice(deviceType);
}
/** @hide */
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 7db73c697ea7..bdc0edd90033 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -615,6 +615,7 @@ public class SettingsBackupTest {
Settings.Secure.ASSIST_SCREENSHOT_ENABLED,
Settings.Secure.ASSIST_STRUCTURE_ENABLED,
Settings.Secure.ATTENTIVE_TIMEOUT,
+ Settings.Secure.AUDIO_DEVICE_INVENTORY, // setting not controllable by user
Settings.Secure.AUTOFILL_FEATURE_FIELD_CLASSIFICATION,
Settings.Secure.AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT,
Settings.Secure.AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE,
diff --git a/services/core/java/com/android/server/audio/AdiDeviceState.java b/services/core/java/com/android/server/audio/AdiDeviceState.java
new file mode 100644
index 000000000000..eab1eca90f6c
--- /dev/null
+++ b/services/core/java/com/android/server/audio/AdiDeviceState.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2023 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.server.audio;
+
+import static android.media.AudioSystem.DEVICE_NONE;
+import static android.media.AudioSystem.isBluetoothDevice;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import java.util.Objects;
+
+/**
+ * Class representing all devices that were previously or are currently connected. Data is
+ * persisted in {@link android.provider.Settings.Secure}
+ */
+/*package*/ final class AdiDeviceState {
+ private static final String TAG = "AS.AdiDeviceState";
+
+ private static final String SETTING_FIELD_SEPARATOR = ",";
+
+ @AudioDeviceInfo.AudioDeviceType
+ private final int mDeviceType;
+
+ private final int mInternalDeviceType;
+ @NonNull
+ private final String mDeviceAddress;
+ /** Unique device id from internal device type and address. */
+ private final Pair<Integer, String> mDeviceId;
+ private boolean mSAEnabled;
+ private boolean mHasHeadTracker = false;
+ private boolean mHeadTrackerEnabled;
+
+ /**
+ * Constructor
+ *
+ * @param deviceType external audio device type
+ * @param internalDeviceType if not set pass {@link DEVICE_NONE}, in this case the
+ * default conversion of the external type will be used
+ * @param address must be non-null for wireless devices
+ * @throws NullPointerException if a null address is passed for a wireless device
+ */
+ AdiDeviceState(@AudioDeviceInfo.AudioDeviceType int deviceType,
+ int internalDeviceType,
+ @Nullable String address) {
+ mDeviceType = deviceType;
+ if (internalDeviceType != DEVICE_NONE) {
+ mInternalDeviceType = internalDeviceType;
+ } else {
+ mInternalDeviceType = AudioDeviceInfo.convertDeviceTypeToInternalDevice(deviceType);
+
+ }
+ mDeviceAddress = isBluetoothDevice(mInternalDeviceType) ? Objects.requireNonNull(
+ address) : "";
+ mDeviceId = new Pair<>(mInternalDeviceType, mDeviceAddress);
+ }
+
+ public Pair<Integer, String> getDeviceId() {
+ return mDeviceId;
+ }
+
+
+
+ @AudioDeviceInfo.AudioDeviceType
+ public int getDeviceType() {
+ return mDeviceType;
+ }
+
+ public int getInternalDeviceType() {
+ return mInternalDeviceType;
+ }
+
+ @NonNull
+ public String getDeviceAddress() {
+ return mDeviceAddress;
+ }
+
+ public void setSAEnabled(boolean sAEnabled) {
+ mSAEnabled = sAEnabled;
+ }
+
+ public boolean isSAEnabled() {
+ return mSAEnabled;
+ }
+
+ public void setHeadTrackerEnabled(boolean headTrackerEnabled) {
+ mHeadTrackerEnabled = headTrackerEnabled;
+ }
+
+ public boolean isHeadTrackerEnabled() {
+ return mHeadTrackerEnabled;
+ }
+
+ public void setHasHeadTracker(boolean hasHeadTracker) {
+ mHasHeadTracker = hasHeadTracker;
+ }
+
+
+ public boolean hasHeadTracker() {
+ return mHasHeadTracker;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ // type check and cast
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final AdiDeviceState sads = (AdiDeviceState) obj;
+ return mDeviceType == sads.mDeviceType
+ && mInternalDeviceType == sads.mInternalDeviceType
+ && mDeviceAddress.equals(sads.mDeviceAddress) // NonNull
+ && mSAEnabled == sads.mSAEnabled
+ && mHasHeadTracker == sads.mHasHeadTracker
+ && mHeadTrackerEnabled == sads.mHeadTrackerEnabled;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mDeviceType, mInternalDeviceType, mDeviceAddress, mSAEnabled,
+ mHasHeadTracker, mHeadTrackerEnabled);
+ }
+
+ @Override
+ public String toString() {
+ return "type: " + mDeviceType
+ + " internal type: 0x" + Integer.toHexString(mInternalDeviceType)
+ + " addr: " + mDeviceAddress + " enabled: " + mSAEnabled
+ + " HT: " + mHasHeadTracker + " HTenabled: " + mHeadTrackerEnabled;
+ }
+
+ public String toPersistableString() {
+ return (new StringBuilder().append(mDeviceType)
+ .append(SETTING_FIELD_SEPARATOR).append(mDeviceAddress)
+ .append(SETTING_FIELD_SEPARATOR).append(mSAEnabled ? "1" : "0")
+ .append(SETTING_FIELD_SEPARATOR).append(mHasHeadTracker ? "1" : "0")
+ .append(SETTING_FIELD_SEPARATOR).append(mHeadTrackerEnabled ? "1" : "0")
+ .append(SETTING_FIELD_SEPARATOR).append(mInternalDeviceType)
+ .toString());
+ }
+
+ /**
+ * Gets the max size (including separators) when persisting the elements with
+ * {@link AdiDeviceState#toPersistableString()}.
+ */
+ public static int getPeristedMaxSize() {
+ return 36; /* (mDeviceType)2 + (mDeviceAddresss)17 + (mInternalDeviceType)9 + (mSAEnabled)1
+ + (mHasHeadTracker)1 + (mHasHeadTrackerEnabled)1
+ + (SETTINGS_FIELD_SEPARATOR)5 */
+ }
+
+ @Nullable
+ public static AdiDeviceState fromPersistedString(@Nullable String persistedString) {
+ if (persistedString == null) {
+ return null;
+ }
+ if (persistedString.isEmpty()) {
+ return null;
+ }
+ String[] fields = TextUtils.split(persistedString, SETTING_FIELD_SEPARATOR);
+ // we may have 5 fields for the legacy AdiDeviceState and 6 containing the internal
+ // device type
+ if (fields.length != 5 && fields.length != 6) {
+ // expecting all fields, fewer may mean corruption, ignore those settings
+ return null;
+ }
+ try {
+ final int deviceType = Integer.parseInt(fields[0]);
+ int internalDeviceType = -1;
+ if (fields.length == 6) {
+ internalDeviceType = Integer.parseInt(fields[5]);
+ }
+ final AdiDeviceState deviceState = new AdiDeviceState(deviceType,
+ internalDeviceType, fields[1]);
+ deviceState.setHasHeadTracker(Integer.parseInt(fields[2]) == 1);
+ deviceState.setHasHeadTracker(Integer.parseInt(fields[3]) == 1);
+ deviceState.setHeadTrackerEnabled(Integer.parseInt(fields[4]) == 1);
+ return deviceState;
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "unable to parse setting for AdiDeviceState: " + persistedString, e);
+ return null;
+ }
+ }
+
+ public AudioDeviceAttributes getAudioDeviceAttributes() {
+ return new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT,
+ mDeviceType, mDeviceAddress);
+ }
+
+}
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 5ecdfe49cdf7..071c61d4fc12 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -47,6 +47,7 @@ import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
+import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import android.util.PrintWriterPrinter;
@@ -63,8 +64,11 @@ import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
-/** @hide */
-/*package*/ final class AudioDeviceBroker {
+/**
+ * @hide
+ * (non final for mocking/spying)
+ */
+public class AudioDeviceBroker {
private static final String TAG = "AS.AudioDeviceBroker";
@@ -742,8 +746,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
/*package*/ void registerStrategyPreferredDevicesDispatcher(
- @NonNull IStrategyPreferredDevicesDispatcher dispatcher) {
- mDeviceInventory.registerStrategyPreferredDevicesDispatcher(dispatcher);
+ @NonNull IStrategyPreferredDevicesDispatcher dispatcher, boolean isPrivileged) {
+ mDeviceInventory.registerStrategyPreferredDevicesDispatcher(dispatcher, isPrivileged);
}
/*package*/ void unregisterStrategyPreferredDevicesDispatcher(
@@ -761,8 +765,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
/*package*/ void registerCapturePresetDevicesRoleDispatcher(
- @NonNull ICapturePresetDevicesRoleDispatcher dispatcher) {
- mDeviceInventory.registerCapturePresetDevicesRoleDispatcher(dispatcher);
+ @NonNull ICapturePresetDevicesRoleDispatcher dispatcher, boolean isPrivileged) {
+ mDeviceInventory.registerCapturePresetDevicesRoleDispatcher(dispatcher, isPrivileged);
}
/*package*/ void unregisterCapturePresetDevicesRoleDispatcher(
@@ -770,6 +774,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
mDeviceInventory.unregisterCapturePresetDevicesRoleDispatcher(dispatcher);
}
+ /* package */ List<AudioDeviceAttributes> anonymizeAudioDeviceAttributesListUnchecked(
+ List<AudioDeviceAttributes> devices) {
+ return mAudioService.anonymizeAudioDeviceAttributesListUnchecked(devices);
+ }
+
/*package*/ void registerCommunicationDeviceDispatcher(
@NonNull ICommunicationDeviceDispatcher dispatcher) {
mCommDevDispatchers.register(dispatcher);
@@ -1415,6 +1424,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
final int capturePreset = msg.arg1;
mDeviceInventory.onSaveClearPreferredDevicesForCapturePreset(capturePreset);
} break;
+ case MSG_PERSIST_AUDIO_DEVICE_SETTINGS:
+ onPersistAudioDeviceSettings();
+ break;
default:
Log.wtf(TAG, "Invalid message " + msg.what);
}
@@ -1496,6 +1508,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
private static final int MSG_IL_SET_PREF_DEVICES_FOR_STRATEGY = 40;
private static final int MSG_I_REMOVE_PREF_DEVICES_FOR_STRATEGY = 41;
+ private static final int MSG_PERSIST_AUDIO_DEVICE_SETTINGS = 54;
+
private static boolean isMessageHandledUnderWakelock(int msgId) {
switch(msgId) {
case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE:
@@ -1841,4 +1855,65 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
return null;
}
+
+ /**
+ * post a message to persist the audio device settings.
+ * Message is delayed by 1s on purpose in case of successive changes in quick succession (at
+ * init time for instance)
+ * Note this method is made public to work around a Mockito bug where it needs to be public
+ * in order to be mocked by a test a the same package
+ * (see https://code.google.com/archive/p/mockito/issues/127)
+ */
+ public void persistAudioDeviceSettings() {
+ sendMsg(MSG_PERSIST_AUDIO_DEVICE_SETTINGS, SENDMSG_REPLACE, /*delay*/ 1000);
+ }
+
+ void onPersistAudioDeviceSettings() {
+ final String deviceSettings = mDeviceInventory.getDeviceSettings();
+ Log.v(TAG, "saving audio device settings: " + deviceSettings);
+ boolean res = Settings.Secure.putStringForUser(
+ mAudioService.getContentResolver(), Settings.Secure.AUDIO_DEVICE_INVENTORY,
+ deviceSettings, UserHandle.USER_CURRENT);
+
+ if (!res) {
+ Log.e(TAG, "error saving audio device settings: " + deviceSettings);
+ }
+ }
+
+ void onReadAudioDeviceSettings() {
+ final ContentResolver contentResolver = mAudioService.getContentResolver();
+ String settings = Settings.Secure.getStringForUser(contentResolver,
+ Settings.Secure.AUDIO_DEVICE_INVENTORY, UserHandle.USER_CURRENT);
+ if (settings != null && !settings.equals("")) {
+ setDeviceSettings(settings);
+ }
+ }
+
+ void setDeviceSettings(String settings) {
+ mDeviceInventory.setDeviceSettings(settings);
+ }
+
+ /** Test only method. */
+ String getDeviceSettings() {
+ return mDeviceInventory.getDeviceSettings();
+ }
+
+ List<AdiDeviceState> getImmutableDeviceInventory() {
+ return mDeviceInventory.getImmutableDeviceInventory();
+ }
+
+ void addDeviceStateToInventory(AdiDeviceState deviceState) {
+ mDeviceInventory.addDeviceStateToInventory(deviceState);
+ }
+
+ AdiDeviceState findDeviceStateForAudioDeviceAttributes(AudioDeviceAttributes ada,
+ int canonicalType) {
+ return mDeviceInventory.findDeviceStateForAudioDeviceAttributes(ada, canonicalType);
+ }
+
+ //------------------------------------------------
+ // for testing purposes only
+ void clearDeviceInventory() {
+ mDeviceInventory.clearDeviceInventory();
+ }
}
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 5944a63bd5e6..1c20b5d91b32 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -15,6 +15,8 @@
*/
package com.android.server.audio;
+import static android.media.AudioSystem.isBluetoothDevice;
+
import android.annotation.NonNull;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
@@ -40,6 +42,7 @@ import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
+import android.util.Pair;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
@@ -47,7 +50,9 @@ import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
@@ -61,12 +66,72 @@ public class AudioDeviceInventory {
private static final String TAG = "AS.AudioDeviceInventory";
+ private static final String SETTING_DEVICE_SEPARATOR_CHAR = "|";
+ private static final String SETTING_DEVICE_SEPARATOR = "\\|";
+
// lock to synchronize all access to mConnectedDevices and mApmConnectedDevices
private final Object mDevicesLock = new Object();
//Audio Analytics ids.
private static final String mMetricsId = "audio.device.";
+ private final Object mDeviceInventoryLock = new Object();
+ @GuardedBy("mDeviceInventoryLock")
+ private final HashMap<Pair<Integer, String>, AdiDeviceState> mDeviceInventory = new HashMap<>();
+
+ List<AdiDeviceState> getImmutableDeviceInventory() {
+ synchronized (mDeviceInventoryLock) {
+ return new ArrayList<AdiDeviceState>(mDeviceInventory.values());
+ }
+ }
+
+ void addDeviceStateToInventory(AdiDeviceState deviceState) {
+ synchronized (mDeviceInventoryLock) {
+ mDeviceInventory.put(deviceState.getDeviceId(), deviceState);
+ }
+ }
+
+ /**
+ * Adds a new entry in mDeviceInventory if the AudioDeviceAttributes passed is an sink
+ * Bluetooth device and no corresponding entry already exists.
+ * @param ada the device to add if needed
+ */
+ void addAudioDeviceInInventoryIfNeeded(AudioDeviceAttributes ada) {
+ if (!AudioSystem.isBluetoothOutDevice(ada.getInternalType())) {
+ return;
+ }
+ synchronized (mDeviceInventoryLock) {
+ if (findDeviceStateForAudioDeviceAttributes(ada, ada.getType()) != null) {
+ return;
+ }
+ AdiDeviceState ads = new AdiDeviceState(
+ ada.getType(), ada.getInternalType(), ada.getAddress());
+ mDeviceInventory.put(ads.getDeviceId(), ads);
+ }
+ mDeviceBroker.persistAudioDeviceSettings();
+ }
+
+ AdiDeviceState findDeviceStateForAudioDeviceAttributes(AudioDeviceAttributes ada,
+ int canonicalDeviceType) {
+ final boolean isWireless = isBluetoothDevice(ada.getInternalType());
+ synchronized (mDeviceInventoryLock) {
+ for (AdiDeviceState deviceState : mDeviceInventory.values()) {
+ if (deviceState.getDeviceType() == canonicalDeviceType
+ && (!isWireless || ada.getAddress().equals(
+ deviceState.getDeviceAddress()))) {
+ return deviceState;
+ }
+ }
+ }
+ return null;
+ }
+
+ void clearDeviceInventory() {
+ synchronized (mDeviceInventoryLock) {
+ mDeviceInventory.clear();
+ }
+ }
+
// List of connected devices
// Key for map created from DeviceInfo.makeDeviceListKey()
@GuardedBy("mDevicesLock")
@@ -253,6 +318,12 @@ public class AudioDeviceInventory {
mPreferredDevicesForCapturePreset.forEach((capturePreset, devices) -> {
pw.println(" " + prefix + "capturePreset:" + capturePreset
+ " devices:" + devices); });
+ pw.println("\ndevices:\n");
+ synchronized (mDeviceInventoryLock) {
+ for (AdiDeviceState device : mDeviceInventory.values()) {
+ pw.println("\t" + device + "\n");
+ }
+ }
}
//------------------------------------------------------------
@@ -678,8 +749,8 @@ public class AudioDeviceInventory {
}
/*package*/ void registerStrategyPreferredDevicesDispatcher(
- @NonNull IStrategyPreferredDevicesDispatcher dispatcher) {
- mPrefDevDispatchers.register(dispatcher);
+ @NonNull IStrategyPreferredDevicesDispatcher dispatcher, boolean isPrivileged) {
+ mPrefDevDispatchers.register(dispatcher, isPrivileged);
}
/*package*/ void unregisterStrategyPreferredDevicesDispatcher(
@@ -713,8 +784,8 @@ public class AudioDeviceInventory {
}
/*package*/ void registerCapturePresetDevicesRoleDispatcher(
- @NonNull ICapturePresetDevicesRoleDispatcher dispatcher) {
- mDevRoleCapturePresetDispatchers.register(dispatcher);
+ @NonNull ICapturePresetDevicesRoleDispatcher dispatcher, boolean isPrivileged) {
+ mDevRoleCapturePresetDispatchers.register(dispatcher, isPrivileged);
}
/*package*/ void unregisterCapturePresetDevicesRoleDispatcher(
@@ -769,6 +840,7 @@ public class AudioDeviceInventory {
mConnectedDevices.put(deviceKey, new DeviceInfo(
device, deviceName, address, AudioSystem.AUDIO_FORMAT_DEFAULT));
mDeviceBroker.postAccessoryPlugMediaUnmute(device);
+ addAudioDeviceInInventoryIfNeeded(new AudioDeviceAttributes(device, address));
mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.CONNECTED).record();
return true;
} else if (!connect && isConnected) {
@@ -987,8 +1059,11 @@ public class AudioDeviceInventory {
mDeviceBroker.postAccessoryPlugMediaUnmute(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
setCurrentAudioRouteNameIfPossible(name, true /*fromA2dp*/);
+ addAudioDeviceInInventoryIfNeeded(new AudioDeviceAttributes(
+ AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address));
}
+
@GuardedBy("mDevicesLock")
private void makeA2dpDeviceUnavailableNow(String address, int a2dpCodec) {
MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "a2dp." + address)
@@ -1091,6 +1166,8 @@ public class AudioDeviceInventory {
mDeviceBroker.postApplyVolumeOnDevice(streamType,
AudioSystem.DEVICE_OUT_HEARING_AID, "makeHearingAidDeviceAvailable");
setCurrentAudioRouteNameIfPossible(name, false /*fromA2dp*/);
+ addAudioDeviceInInventoryIfNeeded(new AudioDeviceAttributes(
+ AudioSystem.DEVICE_OUT_HEARING_AID, address));
new MediaMetrics.Item(mMetricsId + "makeHearingAidDeviceAvailable")
.set(MediaMetrics.Property.ADDRESS, address != null ? address : "")
.set(MediaMetrics.Property.DEVICE,
@@ -1372,6 +1449,9 @@ public class AudioDeviceInventory {
final int nbDispatchers = mPrefDevDispatchers.beginBroadcast();
for (int i = 0; i < nbDispatchers; i++) {
try {
+ if (!((Boolean) mPrefDevDispatchers.getBroadcastCookie(i))) {
+ devices = mDeviceBroker.anonymizeAudioDeviceAttributesListUnchecked(devices);
+ }
mPrefDevDispatchers.getBroadcastItem(i).dispatchPrefDevicesChanged(
strategy, devices);
} catch (RemoteException e) {
@@ -1385,6 +1465,9 @@ public class AudioDeviceInventory {
final int nbDispatchers = mDevRoleCapturePresetDispatchers.beginBroadcast();
for (int i = 0; i < nbDispatchers; ++i) {
try {
+ if (!((Boolean) mDevRoleCapturePresetDispatchers.getBroadcastCookie(i))) {
+ devices = mDeviceBroker.anonymizeAudioDeviceAttributesListUnchecked(devices);
+ }
mDevRoleCapturePresetDispatchers.getBroadcastItem(i).dispatchDevicesRoleChanged(
capturePreset, role, devices);
} catch (RemoteException e) {
@@ -1393,6 +1476,41 @@ public class AudioDeviceInventory {
mDevRoleCapturePresetDispatchers.finishBroadcast();
}
+ /*package*/ String getDeviceSettings() {
+ int deviceCatalogSize = 0;
+ synchronized (mDeviceInventoryLock) {
+ deviceCatalogSize = mDeviceInventory.size();
+
+ final StringBuilder settingsBuilder = new StringBuilder(
+ deviceCatalogSize * AdiDeviceState.getPeristedMaxSize());
+
+ Iterator<AdiDeviceState> iterator = mDeviceInventory.values().iterator();
+ if (iterator.hasNext()) {
+ settingsBuilder.append(iterator.next().toPersistableString());
+ }
+ while (iterator.hasNext()) {
+ settingsBuilder.append(SETTING_DEVICE_SEPARATOR_CHAR);
+ settingsBuilder.append(iterator.next().toPersistableString());
+ }
+ return settingsBuilder.toString();
+ }
+ }
+
+ /*package*/ void setDeviceSettings(String settings) {
+ clearDeviceInventory();
+ String[] devSettings = TextUtils.split(settings,
+ SETTING_DEVICE_SEPARATOR);
+ // small list, not worth overhead of Arrays.stream(devSettings)
+ for (String setting : devSettings) {
+ AdiDeviceState devState = AdiDeviceState.fromPersistedString(setting);
+ // Note if the device is not compatible with spatialization mode or the device
+ // type is not canonical, it will be ignored in {@link SpatializerHelper}.
+ if (devState != null) {
+ addDeviceStateToInventory(devState);
+ }
+ }
+ }
+
//----------------------------------------------------------
// For tests only
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 8d596b62cbf7..f4cc853bc4e6 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -2368,8 +2368,11 @@ public class AudioService extends IAudioService.Stub
return AudioSystem.ERROR;
}
enforceModifyAudioRoutingPermission();
+
+ devices = retrieveBluetoothAddresses(devices);
+
final String logString = String.format(
- "setPreferredDeviceForStrategy u/pid:%d/%d strat:%d dev:%s",
+ "setPreferredDevicesForStrategy u/pid:%d/%d strat:%d dev:%s",
Binder.getCallingUid(), Binder.getCallingPid(), strategy,
devices.stream().map(e -> e.toString()).collect(Collectors.joining(",")));
sDeviceLogger.log(new AudioEventLogger.StringEvent(logString).printLog(TAG));
@@ -2417,7 +2420,7 @@ public class AudioService extends IAudioService.Stub
status, strategy));
return new ArrayList<AudioDeviceAttributes>();
} else {
- return devices;
+ return anonymizeAudioDeviceAttributesList(devices);
}
}
@@ -2430,7 +2433,8 @@ public class AudioService extends IAudioService.Stub
return;
}
enforceModifyAudioRoutingPermission();
- mDeviceBroker.registerStrategyPreferredDevicesDispatcher(dispatcher);
+ mDeviceBroker.registerStrategyPreferredDevicesDispatcher(
+ dispatcher, isBluetoothPrividged());
}
/** @see AudioManager#removeOnPreferredDevicesForStrategyChangedListener(
@@ -2446,7 +2450,7 @@ public class AudioService extends IAudioService.Stub
}
/**
- * @see AudioManager#setPreferredDeviceForCapturePreset(int, AudioDeviceAttributes)
+ * @see AudioManager#setPreferredDevicesForCapturePreset(int, AudioDeviceAttributes)
*/
public int setPreferredDevicesForCapturePreset(
int capturePreset, List<AudioDeviceAttributes> devices) {
@@ -2465,6 +2469,8 @@ public class AudioService extends IAudioService.Stub
return AudioSystem.ERROR;
}
+ devices = retrieveBluetoothAddresses(devices);
+
final int status = mDeviceBroker.setPreferredDevicesForCapturePresetSync(
capturePreset, devices);
if (status != AudioSystem.SUCCESS) {
@@ -2503,7 +2509,7 @@ public class AudioService extends IAudioService.Stub
status, capturePreset));
return new ArrayList<AudioDeviceAttributes>();
} else {
- return devices;
+ return anonymizeAudioDeviceAttributesList(devices);
}
}
@@ -2517,7 +2523,8 @@ public class AudioService extends IAudioService.Stub
return;
}
enforceModifyAudioRoutingPermission();
- mDeviceBroker.registerCapturePresetDevicesRoleDispatcher(dispatcher);
+ mDeviceBroker.registerCapturePresetDevicesRoleDispatcher(
+ dispatcher, isBluetoothPrividged());
}
/**
@@ -2537,7 +2544,8 @@ public class AudioService extends IAudioService.Stub
public @NonNull ArrayList<AudioDeviceAttributes> getDevicesForAttributes(
@NonNull AudioAttributes attributes) {
enforceQueryStateOrModifyRoutingPermission();
- return getDevicesForAttributesInt(attributes);
+ return new ArrayList<AudioDeviceAttributes>(anonymizeAudioDeviceAttributesList(
+ getDevicesForAttributesInt(attributes)));
}
/**
@@ -6118,6 +6126,9 @@ public class AudioService extends IAudioService.Stub
// verify arguments
Objects.requireNonNull(device);
AudioManager.enforceValidVolumeBehavior(deviceVolumeBehavior);
+
+ device = retrieveBluetoothAddress(device);
+
if (pkgName == null) {
pkgName = "";
}
@@ -6170,6 +6181,8 @@ public class AudioService extends IAudioService.Stub
// verify permissions
enforceQueryStateOrModifyRoutingPermission();
+ device = retrieveBluetoothAddress(device);
+
// translate Java device type to native device type (for the devices masks for full / fixed)
final int audioSystemDeviceOut = AudioDeviceInfo.convertDeviceTypeToInternalDevice(
device.getType());
@@ -6218,6 +6231,9 @@ public class AudioService extends IAudioService.Stub
@ConnectionState int state, String address, String name,
String caller) {
enforceModifyAudioRoutingPermission();
+
+ address = retrieveBluetoothAddress(type, address);
+
if (state != CONNECTION_STATE_CONNECTED
&& state != CONNECTION_STATE_DISCONNECTED) {
throw new IllegalArgumentException("Invalid state " + state);
@@ -8237,6 +8253,120 @@ public class AudioService extends IAudioService.Stub
}
//==========================================================================================
+
+ private boolean isBluetoothPrividged() {
+ return PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.BLUETOOTH_CONNECT)
+ || Binder.getCallingUid() == Process.SYSTEM_UID;
+ }
+
+ List<AudioDeviceAttributes> retrieveBluetoothAddresses(List<AudioDeviceAttributes> devices) {
+ if (isBluetoothPrividged()) {
+ return devices;
+ }
+
+ List<AudioDeviceAttributes> checkedDevices = new ArrayList<AudioDeviceAttributes>();
+ for (AudioDeviceAttributes ada : devices) {
+ if (ada == null) {
+ continue;
+ }
+ checkedDevices.add(retrieveBluetoothAddressUncheked(ada));
+ }
+ return checkedDevices;
+ }
+
+ AudioDeviceAttributes retrieveBluetoothAddress(@NonNull AudioDeviceAttributes ada) {
+ if (isBluetoothPrividged()) {
+ return ada;
+ }
+ return retrieveBluetoothAddressUncheked(ada);
+ }
+
+ AudioDeviceAttributes retrieveBluetoothAddressUncheked(@NonNull AudioDeviceAttributes ada) {
+ Objects.requireNonNull(ada);
+ if (AudioSystem.isBluetoothDevice(ada.getInternalType())) {
+ String anonymizedAddress = anonymizeBluetoothAddress(ada.getAddress());
+ for (AdiDeviceState ads : mDeviceBroker.getImmutableDeviceInventory()) {
+ if (!(AudioSystem.isBluetoothDevice(ads.getInternalDeviceType())
+ && (ada.getInternalType() == ads.getInternalDeviceType())
+ && anonymizedAddress.equals(anonymizeBluetoothAddress(
+ ads.getDeviceAddress())))) {
+ continue;
+ }
+ ada.setAddress(ads.getDeviceAddress());
+ break;
+ }
+ }
+ return ada;
+ }
+
+ private String retrieveBluetoothAddress(int type, String address) {
+ if (isBluetoothPrividged() || !AudioSystem.isBluetoothDevice(type)
+ || address == null) {
+ return address;
+ }
+ String anonymizedAddress = anonymizeBluetoothAddress(address);
+ for (AdiDeviceState ads : mDeviceBroker.getImmutableDeviceInventory()) {
+ if (!(AudioSystem.isBluetoothDevice(ads.getInternalDeviceType())
+ && anonymizedAddress.equals(anonymizeBluetoothAddress(
+ ads.getDeviceAddress())))) {
+ continue;
+ }
+ return ads.getDeviceAddress();
+ }
+ return address;
+ }
+
+ /**
+ * Convert a Bluetooth MAC address to an anonymized one when exposed to a non privileged app
+ * Must match the implementation of BluetoothUtils.toAnonymizedAddress()
+ * @param address Mac address to be anonymized
+ * @return anonymized mac address
+ */
+ static String anonymizeBluetoothAddress(String address) {
+ if (address == null || address.length() != "AA:BB:CC:DD:EE:FF".length()) {
+ return null;
+ }
+ return "XX:XX:XX:XX" + address.substring("XX:XX:XX:XX".length());
+ }
+
+ private List<AudioDeviceAttributes> anonymizeAudioDeviceAttributesList(
+ List<AudioDeviceAttributes> devices) {
+ if (isBluetoothPrividged()) {
+ return devices;
+ }
+ return anonymizeAudioDeviceAttributesListUnchecked(devices);
+ }
+
+ /* package */ List<AudioDeviceAttributes> anonymizeAudioDeviceAttributesListUnchecked(
+ List<AudioDeviceAttributes> devices) {
+ List<AudioDeviceAttributes> anonymizedDevices = new ArrayList<AudioDeviceAttributes>();
+ for (AudioDeviceAttributes ada : devices) {
+ anonymizedDevices.add(anonymizeAudioDeviceAttributesUnchecked(ada));
+ }
+ return anonymizedDevices;
+ }
+
+ private AudioDeviceAttributes anonymizeAudioDeviceAttributesUnchecked(
+ AudioDeviceAttributes ada) {
+ if (!AudioSystem.isBluetoothDevice(ada.getInternalType())) {
+ return ada;
+ }
+ AudioDeviceAttributes res = new AudioDeviceAttributes(ada);
+ res.setAddress(anonymizeBluetoothAddress(ada.getAddress()));
+ return res;
+ }
+
+ private AudioDeviceAttributes anonymizeAudioDeviceAttributes(AudioDeviceAttributes ada) {
+ if (isBluetoothPrividged()) {
+ return ada;
+ }
+
+ return anonymizeAudioDeviceAttributesUnchecked(ada);
+ }
+
+ //==========================================================================================
+
private boolean readCameraSoundForced() {
return SystemProperties.getBoolean("audio.camerasound.force", false) ||
mContext.getResources().getBoolean(
@@ -10354,6 +10484,9 @@ public class AudioService extends IAudioService.Stub
@NonNull AudioDeviceAttributes device, @IntRange(from = 0) long delayMillis) {
Objects.requireNonNull(device, "device must not be null");
enforceModifyAudioRoutingPermission();
+
+ device = retrieveBluetoothAddress(device);
+
final String getterKey = "additional_output_device_delay="
+ device.getInternalType() + "," + device.getAddress(); // "getter" key as an id.
final String setterKey = getterKey + "," + delayMillis; // append the delay for setter
@@ -10374,6 +10507,9 @@ public class AudioService extends IAudioService.Stub
@IntRange(from = 0)
public long getAdditionalOutputDeviceDelay(@NonNull AudioDeviceAttributes device) {
Objects.requireNonNull(device, "device must not be null");
+
+ device = retrieveBluetoothAddress(device);
+
final String key = "additional_output_device_delay";
final String reply = AudioSystem.getParameters(
key + "=" + device.getInternalType() + "," + device.getAddress());
@@ -10401,6 +10537,9 @@ public class AudioService extends IAudioService.Stub
@IntRange(from = 0)
public long getMaxAdditionalOutputDeviceDelay(@NonNull AudioDeviceAttributes device) {
Objects.requireNonNull(device, "device must not be null");
+
+ device = retrieveBluetoothAddress(device);
+
final String key = "max_additional_output_device_delay";
final String reply = AudioSystem.getParameters(
key + "=" + device.getInternalType() + "," + device.getAddress());
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
index 5c53d43fa1df..d33ff03df02a 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
@@ -29,6 +29,7 @@ import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
+import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.AudioSystem;
import android.util.Log;
@@ -199,6 +200,37 @@ public class AudioDeviceBrokerTest {
any(Intent.class));
}
+ /**
+ * Test that constructing an AdiDeviceState instance requires a non-null address for a
+ * wireless type, but can take null for a non-wireless type;
+ * @throws Exception
+ */
+ @Test
+ public void testAdiDeviceStateNullAddressCtor() throws Exception {
+ try {
+ new AdiDeviceState(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
+ AudioManager.DEVICE_OUT_SPEAKER, null);
+ new AdiDeviceState(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
+ AudioManager.DEVICE_OUT_BLUETOOTH_A2DP, null);
+ Assert.fail();
+ } catch (NullPointerException e) { }
+ }
+
+ @Test
+ public void testAdiDeviceStateStringSerialization() throws Exception {
+ Log.i(TAG, "starting testAdiDeviceStateStringSerialization");
+ final AdiDeviceState devState = new AdiDeviceState(
+ AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioManager.DEVICE_OUT_SPEAKER, "bla");
+ devState.setHasHeadTracker(false);
+ devState.setHeadTrackerEnabled(false);
+ devState.setSAEnabled(true);
+ final String persistString = devState.toPersistableString();
+ final AdiDeviceState result = AdiDeviceState.fromPersistedString(persistString);
+ Log.i(TAG, "original:" + devState);
+ Log.i(TAG, "result :" + result);
+ Assert.assertEquals(devState, result);
+ }
+
private void doTestConnectionDisconnectionReconnection(int delayAfterDisconnection,
boolean mockMediaPlayback, boolean guaranteeSingleConnection) throws Exception {
when(mMockAudioService.getDeviceForStream(AudioManager.STREAM_MUSIC))