From 2ad44bc6543402a5ad0f95b39f78e7d9ec3fe731 Mon Sep 17 00:00:00 2001 From: Eric Laurent <elaurent@google.com> Date: Fri, 13 Oct 2023 18:37:23 +0200 Subject: [PATCH] AudioService: anonymize Bluetooth MAC addresses Make sure APIs returning AudioDeviceAttributes from AudioService anonymize the Bluetooth MAC addresses because those are considered privacy sensitive. Only expose the full MAC address to system and apps with BLUETOOTH_CONNECT permission. setters, getters and listeners for preferred device for strategy, preferred device for capture preset and mute await connection are modified: - when entering AudioService, full MAC addresses are retrieved based on the known Bluetooth devices stored in AudioDeviceInventory.mDeviceInventory - when exiting AudioService, MAC addresses are anonymized if the client app does not have BLUETOOTH_CONNECT permission or is not a system component APIs based on AudioDeviceInfo do not need to be modified as the AudioDeviceInfo MAC address is for the AudioPort cached in the app process and AudioPorts are anonymized by the native audioserver before being returned to client apps. Bug: 285588444 Test: atest AudioManagerTest Test: atest RoutingTest Test: atest AudioCommunicationDeviceTest Change-Id: I67bbba2ba941c97138a068d640079b17650e3d86 Merged-In: I67bbba2ba941c97138a068d640079b17650e3d86 --- .../android/media/AudioDeviceAttributes.java | 26 ++- .../android/server/audio/AdiDeviceState.java | 11 +- .../server/audio/AudioDeviceBroker.java | 17 +- .../server/audio/AudioDeviceInventory.java | 100 ++++++--- .../android/server/audio/AudioService.java | 209 +++++++++++++++--- 5 files changed, 301 insertions(+), 62 deletions(-) diff --git a/media/java/android/media/AudioDeviceAttributes.java b/media/java/android/media/AudioDeviceAttributes.java index af3c295b8d6c..5a274353f68e 100644 --- a/media/java/android/media/AudioDeviceAttributes.java +++ b/media/java/android/media/AudioDeviceAttributes.java @@ -68,7 +68,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; /** * The non-unique name of the device. Some devices don't have names, only an empty string. * Should not be used as a unique identifier for a device. @@ -186,6 +186,21 @@ public final class AudioDeviceAttributes implements Parcelable { mAudioDescriptors = new ArrayList<>(); } + /** + * @hide + * Copy Constructor. + * @param ada the copied AudioDeviceAttributes + */ + public AudioDeviceAttributes(AudioDeviceAttributes ada) { + mRole = ada.getRole(); + mType = ada.getType(); + mAddress = ada.getAddress(); + mName = ada.getName(); + mNativeType = ada.getInternalType(); + mAudioProfiles = ada.getAudioProfiles(); + mAudioDescriptors = ada.getAudioDescriptors(); + } + /** * @hide * Returns the role of a device @@ -216,6 +231,15 @@ public final class AudioDeviceAttributes implements Parcelable { return mAddress; } + /** + * @hide + * Sets the device address. Only used by audio service. + */ + public void setAddress(@NonNull String address) { + Objects.requireNonNull(address); + mAddress = address; + } + /** * @hide * Returns the name of the audio device, or an empty string for devices without one diff --git a/services/core/java/com/android/server/audio/AdiDeviceState.java b/services/core/java/com/android/server/audio/AdiDeviceState.java index 683b3eb7a92e..1c456a781f7a 100644 --- a/services/core/java/com/android/server/audio/AdiDeviceState.java +++ b/services/core/java/com/android/server/audio/AdiDeviceState.java @@ -25,6 +25,7 @@ import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; import android.text.TextUtils; import android.util.Log; +import android.util.Pair; import java.util.Objects; @@ -43,6 +44,8 @@ import java.util.Objects; 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; @@ -68,6 +71,11 @@ import java.util.Objects; } mDeviceAddress = isBluetoothDevice(mInternalDeviceType) ? Objects.requireNonNull( address) : ""; + mDeviceId = new Pair<>(mInternalDeviceType, mDeviceAddress); + } + + public Pair<Integer, String> getDeviceId() { + return mDeviceId; } @AudioDeviceInfo.AudioDeviceType @@ -138,7 +146,8 @@ import java.util.Objects; @Override public String toString() { - return "type: " + mDeviceType + "internal type: " + mInternalDeviceType + return "type: " + mDeviceType + + " internal type: 0x" + Integer.toHexString(mInternalDeviceType) + " addr: " + mDeviceAddress + " enabled: " + mSAEnabled + " HT: " + mHasHeadTracker + " HTenabled: " + mHeadTrackerEnabled; } diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index bba85fd76127..054a26f9c0fb 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -1153,8 +1153,8 @@ public class AudioDeviceBroker { } /*package*/ void registerStrategyPreferredDevicesDispatcher( - @NonNull IStrategyPreferredDevicesDispatcher dispatcher) { - mDeviceInventory.registerStrategyPreferredDevicesDispatcher(dispatcher); + @NonNull IStrategyPreferredDevicesDispatcher dispatcher, boolean isPrivileged) { + mDeviceInventory.registerStrategyPreferredDevicesDispatcher(dispatcher, isPrivileged); } /*package*/ void unregisterStrategyPreferredDevicesDispatcher( @@ -1163,8 +1163,8 @@ public class AudioDeviceBroker { } /*package*/ void registerStrategyNonDefaultDevicesDispatcher( - @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher) { - mDeviceInventory.registerStrategyNonDefaultDevicesDispatcher(dispatcher); + @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher, boolean isPrivileged) { + mDeviceInventory.registerStrategyNonDefaultDevicesDispatcher(dispatcher, isPrivileged); } /*package*/ void unregisterStrategyNonDefaultDevicesDispatcher( @@ -1182,8 +1182,8 @@ public class AudioDeviceBroker { } /*package*/ void registerCapturePresetDevicesRoleDispatcher( - @NonNull ICapturePresetDevicesRoleDispatcher dispatcher) { - mDeviceInventory.registerCapturePresetDevicesRoleDispatcher(dispatcher); + @NonNull ICapturePresetDevicesRoleDispatcher dispatcher, boolean isPrivileged) { + mDeviceInventory.registerCapturePresetDevicesRoleDispatcher(dispatcher, isPrivileged); } /*package*/ void unregisterCapturePresetDevicesRoleDispatcher( @@ -1191,6 +1191,11 @@ public class AudioDeviceBroker { mDeviceInventory.unregisterCapturePresetDevicesRoleDispatcher(dispatcher); } + /* package */ List<AudioDeviceAttributes> anonymizeAudioDeviceAttributesListUnchecked( + List<AudioDeviceAttributes> devices) { + return mAudioService.anonymizeAudioDeviceAttributesListUnchecked(devices); + } + /*package*/ void registerCommunicationDeviceDispatcher( @NonNull ICommunicationDeviceDispatcher dispatcher) { mCommDevDispatchers.register(dispatcher); diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 387646cccd7c..5332acae13ad 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -61,6 +61,7 @@ import com.google.android.collect.Sets; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; @@ -91,31 +92,59 @@ public class AudioDeviceInventory { private final Object mDeviceInventoryLock = new Object(); @GuardedBy("mDeviceInventoryLock") - private final ArrayList<AdiDeviceState> mDeviceInventory = new ArrayList<>(0); - + private final HashMap<Pair<Integer, String>, AdiDeviceState> mDeviceInventory = new HashMap<>(); List<AdiDeviceState> getImmutableDeviceInventory() { synchronized (mDeviceInventoryLock) { - return List.copyOf(mDeviceInventory); + return new ArrayList<AdiDeviceState>(mDeviceInventory.values()); } } void addDeviceStateToInventory(AdiDeviceState deviceState) { synchronized (mDeviceInventoryLock) { - mDeviceInventory.add(deviceState); + 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(); + } + + /** + * Finds the device state that matches the passed {@link AudioDeviceAttributes} and device + * type. Note: currently this method only returns a valid device for A2DP and BLE devices. + * + * @param ada attributes of device to match + * @param canonicalDeviceType external device type to match + * @return the found {@link AdiDeviceState} matching a cached A2DP or BLE device or + * {@code null} otherwise. + */ + @Nullable AdiDeviceState findDeviceStateForAudioDeviceAttributes(AudioDeviceAttributes ada, int canonicalDeviceType) { final boolean isWireless = isBluetoothDevice(ada.getInternalType()); - synchronized (mDeviceInventoryLock) { - for (AdiDeviceState deviceSetting : mDeviceInventory) { - if (deviceSetting.getDeviceType() == canonicalDeviceType + for (AdiDeviceState deviceState : mDeviceInventory.values()) { + if (deviceState.getDeviceType() == canonicalDeviceType && (!isWireless || ada.getAddress().equals( - deviceSetting.getDeviceAddress()))) { - return deviceSetting; + deviceState.getDeviceAddress()))) { + return deviceState; } } } @@ -387,7 +416,7 @@ public class AudioDeviceInventory { + " role:" + key.second + " devices:" + devices); }); pw.println("\ndevices:\n"); synchronized (mDeviceInventoryLock) { - for (AdiDeviceState device : mDeviceInventory) { + for (AdiDeviceState device : mDeviceInventory.values()) { pw.println("\t" + device + "\n"); } } @@ -934,8 +963,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( @@ -944,8 +973,8 @@ public class AudioDeviceInventory { } /*package*/ void registerStrategyNonDefaultDevicesDispatcher( - @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher) { - mNonDefDevDispatchers.register(dispatcher); + @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher, boolean isPrivileged) { + mNonDefDevDispatchers.register(dispatcher, isPrivileged); } /*package*/ void unregisterStrategyNonDefaultDevicesDispatcher( @@ -1026,8 +1055,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( @@ -1354,6 +1383,8 @@ public class AudioDeviceInventory { updateBluetoothPreferredModes_l(connect ? btDevice : null /*connectedDevice*/); if (!connect) { purgeDevicesRoles_l(); + } else { + addAudioDeviceInInventoryIfNeeded(attributes); } } mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.CONNECTED).record(); @@ -1620,6 +1651,7 @@ public class AudioDeviceInventory { setCurrentAudioRouteNameIfPossible(name, true /*fromA2dp*/); updateBluetoothPreferredModes_l(btInfo.mDevice /*connectedDevice*/); + addAudioDeviceInInventoryIfNeeded(ada); } static final int[] CAPTURE_PRESETS = new int[] {AudioSource.MIC, AudioSource.CAMCORDER, @@ -1919,9 +1951,9 @@ public class AudioDeviceInventory { final int hearingAidVolIndex = mDeviceBroker.getVssVolumeForDevice(streamType, AudioSystem.DEVICE_OUT_HEARING_AID); mDeviceBroker.postSetHearingAidVolumeIndex(hearingAidVolIndex, streamType); - - mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes( - AudioSystem.DEVICE_OUT_HEARING_AID, address, name), + AudioDeviceAttributes ada = new AudioDeviceAttributes( + AudioSystem.DEVICE_OUT_HEARING_AID, address, name); + mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); mConnectedDevices.put( @@ -1931,6 +1963,7 @@ public class AudioDeviceInventory { mDeviceBroker.postApplyVolumeOnDevice(streamType, AudioSystem.DEVICE_OUT_HEARING_AID, "makeHearingAidDeviceAvailable"); setCurrentAudioRouteNameIfPossible(name, false /*fromA2dp*/); + addAudioDeviceInInventoryIfNeeded(ada); new MediaMetrics.Item(mMetricsId + "makeHearingAidDeviceAvailable") .set(MediaMetrics.Property.ADDRESS, address != null ? address : "") .set(MediaMetrics.Property.DEVICE, @@ -2033,6 +2066,7 @@ public class AudioDeviceInventory { sensorUuid)); mDeviceBroker.postAccessoryPlugMediaUnmute(device); setCurrentAudioRouteNameIfPossible(name, /*fromA2dp=*/false); + addAudioDeviceInInventoryIfNeeded(ada); } if (streamType == AudioSystem.STREAM_DEFAULT) { @@ -2363,6 +2397,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) { @@ -2376,6 +2413,9 @@ public class AudioDeviceInventory { final int nbDispatchers = mNonDefDevDispatchers.beginBroadcast(); for (int i = 0; i < nbDispatchers; i++) { try { + if (!((Boolean) mNonDefDevDispatchers.getBroadcastCookie(i))) { + devices = mDeviceBroker.anonymizeAudioDeviceAttributesListUnchecked(devices); + } mNonDefDevDispatchers.getBroadcastItem(i).dispatchNonDefDevicesChanged( strategy, devices); } catch (RemoteException e) { @@ -2389,6 +2429,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) { @@ -2413,19 +2456,20 @@ public class AudioDeviceInventory { int deviceCatalogSize = 0; synchronized (mDeviceInventoryLock) { deviceCatalogSize = mDeviceInventory.size(); - } - final StringBuilder settingsBuilder = new StringBuilder( + + final StringBuilder settingsBuilder = new StringBuilder( deviceCatalogSize * AdiDeviceState.getPeristedMaxSize()); - synchronized (mDeviceInventoryLock) { - for (int i = 0; i < mDeviceInventory.size(); i++) { - settingsBuilder.append(mDeviceInventory.get(i).toPersistableString()); - if (i != mDeviceInventory.size() - 1) { - settingsBuilder.append(SETTING_DEVICE_SEPARATOR_CHAR); - } + 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(); } - return settingsBuilder.toString(); } /*package*/ void setDeviceSettings(String settings) { diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index e2e979e79898..d82cef546315 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -2831,8 +2831,11 @@ public class AudioService extends IAudioService.Stub if (devices == null) { return AudioSystem.ERROR; } + + 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.enqueue(new EventLogger.StringEvent(logString).printLog(TAG)); @@ -2888,7 +2891,7 @@ public class AudioService extends IAudioService.Stub status, strategy)); return new ArrayList<AudioDeviceAttributes>(); } else { - return devices; + return anonymizeAudioDeviceAttributesList(devices); } } @@ -2903,6 +2906,9 @@ public class AudioService extends IAudioService.Stub @NonNull AudioDeviceAttributes device) { super.setDeviceAsNonDefaultForStrategy_enforcePermission(); Objects.requireNonNull(device); + + device = retrieveBluetoothAddress(device); + final String logString = String.format( "setDeviceAsNonDefaultForStrategy u/pid:%d/%d strat:%d dev:%s", Binder.getCallingUid(), Binder.getCallingPid(), strategy, device.toString()); @@ -2929,6 +2935,9 @@ public class AudioService extends IAudioService.Stub AudioDeviceAttributes device) { super.removeDeviceAsNonDefaultForStrategy_enforcePermission(); Objects.requireNonNull(device); + + device = retrieveBluetoothAddress(device); + final String logString = String.format( "removeDeviceAsNonDefaultForStrategy strat:%d dev:%s", strategy, device.toString()); sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG)); @@ -2963,7 +2972,7 @@ public class AudioService extends IAudioService.Stub status, strategy)); return new ArrayList<AudioDeviceAttributes>(); } else { - return devices; + return anonymizeAudioDeviceAttributesList(devices); } } @@ -2976,7 +2985,8 @@ public class AudioService extends IAudioService.Stub return; } enforceModifyAudioRoutingPermission(); - mDeviceBroker.registerStrategyPreferredDevicesDispatcher(dispatcher); + mDeviceBroker.registerStrategyPreferredDevicesDispatcher( + dispatcher, isBluetoothPrividged()); } /** @see AudioManager#removeOnPreferredDevicesForStrategyChangedListener( @@ -3000,7 +3010,8 @@ public class AudioService extends IAudioService.Stub return; } enforceModifyAudioRoutingPermission(); - mDeviceBroker.registerStrategyNonDefaultDevicesDispatcher(dispatcher); + mDeviceBroker.registerStrategyNonDefaultDevicesDispatcher( + dispatcher, isBluetoothPrividged()); } /** @see AudioManager#removeOnNonDefaultDevicesForStrategyChangedListener( @@ -3016,7 +3027,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) { @@ -3035,6 +3046,8 @@ public class AudioService extends IAudioService.Stub return AudioSystem.ERROR; } + devices = retrieveBluetoothAddresses(devices); + final int status = mDeviceBroker.setPreferredDevicesForCapturePresetSync( capturePreset, devices); if (status != AudioSystem.SUCCESS) { @@ -3081,7 +3094,7 @@ public class AudioService extends IAudioService.Stub status, capturePreset)); return new ArrayList<AudioDeviceAttributes>(); } else { - return devices; + return anonymizeAudioDeviceAttributesList(devices); } } @@ -3095,7 +3108,8 @@ public class AudioService extends IAudioService.Stub return; } enforceModifyAudioRoutingPermission(); - mDeviceBroker.registerCapturePresetDevicesRoleDispatcher(dispatcher); + mDeviceBroker.registerCapturePresetDevicesRoleDispatcher( + dispatcher, isBluetoothPrividged()); } /** @@ -3115,7 +3129,9 @@ public class AudioService extends IAudioService.Stub public @NonNull ArrayList<AudioDeviceAttributes> getDevicesForAttributes( @NonNull AudioAttributes attributes) { enforceQueryStateOrModifyRoutingPermission(); - return getDevicesForAttributesInt(attributes, false /* forVolume */); + + return new ArrayList<AudioDeviceAttributes>(anonymizeAudioDeviceAttributesList( + getDevicesForAttributesInt(attributes, false /* forVolume */))); } /** @see AudioManager#getAudioDevicesForAttributes(AudioAttributes) @@ -3125,7 +3141,8 @@ public class AudioService extends IAudioService.Stub */ public @NonNull ArrayList<AudioDeviceAttributes> getDevicesForAttributesUnprotected( @NonNull AudioAttributes attributes) { - return getDevicesForAttributesInt(attributes, false /* forVolume */); + return new ArrayList<AudioDeviceAttributes>(anonymizeAudioDeviceAttributesList( + getDevicesForAttributesInt(attributes, false /* forVolume */))); } /** @@ -7318,6 +7335,8 @@ public class AudioService extends IAudioService.Stub Objects.requireNonNull(device); AudioManager.enforceValidVolumeBehavior(deviceVolumeBehavior); + device = retrieveBluetoothAddress(device); + sVolumeLogger.enqueue(new EventLogger.StringEvent("setDeviceVolumeBehavior: dev:" + AudioSystem.getOutputDeviceName(device.getInternalType()) + " addr:" + device.getAddress() + " behavior:" @@ -7401,6 +7420,8 @@ public class AudioService extends IAudioService.Stub // verify parameters Objects.requireNonNull(device); + device = retrieveBluetoothAddress(device); + return getDeviceVolumeBehaviorInt(device); } @@ -7475,9 +7496,12 @@ public class AudioService extends IAudioService.Stub /** * see AudioManager.setWiredDeviceConnectionState() */ - public void setWiredDeviceConnectionState(AudioDeviceAttributes attributes, + public void setWiredDeviceConnectionState(@NonNull AudioDeviceAttributes attributes, @ConnectionState int state, String caller) { super.setWiredDeviceConnectionState_enforcePermission(); + Objects.requireNonNull(attributes); + + attributes = retrieveBluetoothAddress(attributes); if (state != CONNECTION_STATE_CONNECTED && state != CONNECTION_STATE_DISCONNECTED) { @@ -7518,6 +7542,9 @@ public class AudioService extends IAudioService.Stub boolean connected) { Objects.requireNonNull(device); enforceModifyAudioRoutingPermission(); + + device = retrieveBluetoothAddress(device); + mDeviceBroker.setTestDeviceConnectionState(device, connected ? CONNECTION_STATE_CONNECTED : CONNECTION_STATE_DISCONNECTED); // simulate a routing update from native @@ -10274,6 +10301,100 @@ public class AudioService extends IAudioService.Stub mSpatializerHelper.setFeatureEnabled(mHasSpatializerEffect); } + 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; + } + + /** + * 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); + } + //========================================================================================== // camera sound is forced if any of the resources corresponding to one active SIM @@ -10321,13 +10442,16 @@ public class AudioService extends IAudioService.Stub Objects.requireNonNull(usages); Objects.requireNonNull(device); enforceModifyAudioRoutingPermission(); + + final AudioDeviceAttributes ada = retrieveBluetoothAddress(device); + if (timeOutMs <= 0 || usages.length == 0) { throw new IllegalArgumentException("Invalid timeOutMs/usagesToMute"); } Log.i(TAG, "muteAwaitConnection dev:" + device + " timeOutMs:" + timeOutMs + " usages:" + Arrays.toString(usages)); - if (mDeviceBroker.isDeviceConnected(device)) { + if (mDeviceBroker.isDeviceConnected(ada)) { // not throwing an exception as there could be a race between a connection (server-side, // notification of connection in flight) and a mute operation (client-side) Log.i(TAG, "muteAwaitConnection ignored, device (" + device + ") already connected"); @@ -10339,12 +10463,19 @@ public class AudioService extends IAudioService.Stub + mMutingExpectedDevice); throw new IllegalStateException("muteAwaitConnection already in progress"); } - mMutingExpectedDevice = device; + mMutingExpectedDevice = ada; mMutedUsagesAwaitingConnection = usages; - mPlaybackMonitor.muteAwaitConnection(usages, device, timeOutMs); + mPlaybackMonitor.muteAwaitConnection(usages, ada, timeOutMs); } - dispatchMuteAwaitConnection(cb -> { try { - cb.dispatchOnMutedUntilConnection(device, usages); } catch (RemoteException e) { } }); + dispatchMuteAwaitConnection((cb, isPrivileged) -> { + try { + AudioDeviceAttributes dev = ada; + if (!isPrivileged) { + dev = anonymizeAudioDeviceAttributesUnchecked(ada); + } + cb.dispatchOnMutedUntilConnection(dev, usages); + } catch (RemoteException e) { } + }); } @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) @@ -10353,7 +10484,7 @@ public class AudioService extends IAudioService.Stub super.getMutingExpectedDevice_enforcePermission(); synchronized (mMuteAwaitConnectionLock) { - return mMutingExpectedDevice; + return anonymizeAudioDeviceAttributes(mMutingExpectedDevice); } } @@ -10362,6 +10493,9 @@ public class AudioService extends IAudioService.Stub public void cancelMuteAwaitConnection(@NonNull AudioDeviceAttributes device) { Objects.requireNonNull(device); enforceModifyAudioRoutingPermission(); + + final AudioDeviceAttributes ada = retrieveBluetoothAddress(device); + Log.i(TAG, "cancelMuteAwaitConnection for device:" + device); final int[] mutedUsages; synchronized (mMuteAwaitConnectionLock) { @@ -10371,7 +10505,7 @@ public class AudioService extends IAudioService.Stub Log.i(TAG, "cancelMuteAwaitConnection ignored, no expected device"); return; } - if (!device.equalTypeAddress(mMutingExpectedDevice)) { + if (!ada.equalTypeAddress(mMutingExpectedDevice)) { Log.e(TAG, "cancelMuteAwaitConnection ignored, got " + device + "] but expected device is" + mMutingExpectedDevice); throw new IllegalStateException("cancelMuteAwaitConnection for wrong device"); @@ -10381,8 +10515,14 @@ public class AudioService extends IAudioService.Stub mMutedUsagesAwaitingConnection = null; mPlaybackMonitor.cancelMuteAwaitConnection("cancelMuteAwaitConnection dev:" + device); } - dispatchMuteAwaitConnection(cb -> { try { cb.dispatchOnUnmutedEvent( - AudioManager.MuteAwaitConnectionCallback.EVENT_CANCEL, device, mutedUsages); + dispatchMuteAwaitConnection((cb, isPrivileged) -> { + try { + AudioDeviceAttributes dev = ada; + if (!isPrivileged) { + dev = anonymizeAudioDeviceAttributesUnchecked(ada); + } + cb.dispatchOnUnmutedEvent( + AudioManager.MuteAwaitConnectionCallback.EVENT_CANCEL, dev, mutedUsages); } catch (RemoteException e) { } }); } @@ -10396,7 +10536,7 @@ public class AudioService extends IAudioService.Stub super.registerMuteAwaitConnectionDispatcher_enforcePermission(); if (register) { - mMuteAwaitConnectionDispatchers.register(cb); + mMuteAwaitConnectionDispatchers.register(cb, isBluetoothPrividged()); } else { mMuteAwaitConnectionDispatchers.unregister(cb); } @@ -10420,8 +10560,14 @@ public class AudioService extends IAudioService.Stub mPlaybackMonitor.cancelMuteAwaitConnection( "checkMuteAwaitConnection device " + device + " connected, unmuting"); } - dispatchMuteAwaitConnection(cb -> { try { cb.dispatchOnUnmutedEvent( - AudioManager.MuteAwaitConnectionCallback.EVENT_CONNECTION, device, mutedUsages); + dispatchMuteAwaitConnection((cb, isPrivileged) -> { + try { + AudioDeviceAttributes ada = device; + if (!isPrivileged) { + ada = anonymizeAudioDeviceAttributesUnchecked(device); + } + cb.dispatchOnUnmutedEvent(AudioManager.MuteAwaitConnectionCallback.EVENT_CONNECTION, + ada, mutedUsages); } catch (RemoteException e) { } }); } @@ -10441,7 +10587,8 @@ public class AudioService extends IAudioService.Stub mMutingExpectedDevice = null; mMutedUsagesAwaitingConnection = null; } - dispatchMuteAwaitConnection(cb -> { try { + dispatchMuteAwaitConnection((cb, isPrivileged) -> { + try { cb.dispatchOnUnmutedEvent( AudioManager.MuteAwaitConnectionCallback.EVENT_TIMEOUT, timedOutDevice, mutedUsages); @@ -10449,13 +10596,14 @@ public class AudioService extends IAudioService.Stub } private void dispatchMuteAwaitConnection( - java.util.function.Consumer<IMuteAwaitConnectionCallback> callback) { + java.util.function.BiConsumer<IMuteAwaitConnectionCallback, Boolean> callback) { final int nbDispatchers = mMuteAwaitConnectionDispatchers.beginBroadcast(); // lazy initialization as errors unlikely ArrayList<IMuteAwaitConnectionCallback> errorList = null; for (int i = 0; i < nbDispatchers; i++) { try { - callback.accept(mMuteAwaitConnectionDispatchers.getBroadcastItem(i)); + callback.accept(mMuteAwaitConnectionDispatchers.getBroadcastItem(i), + (Boolean) mMuteAwaitConnectionDispatchers.getBroadcastCookie(i)); } catch (Exception e) { if (errorList == null) { errorList = new ArrayList<>(1); @@ -12921,6 +13069,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 @@ -12941,6 +13092,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()); @@ -12968,6 +13122,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()); -- GitLab