| /* |
| * Copyright 2019 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 android.annotation.NonNull; |
| import android.app.ActivityManager; |
| import android.bluetooth.BluetoothA2dp; |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothHearingAid; |
| import android.bluetooth.BluetoothProfile; |
| import android.content.Intent; |
| import android.media.AudioDeviceAddress; |
| import android.media.AudioDevicePort; |
| import android.media.AudioFormat; |
| import android.media.AudioManager; |
| import android.media.AudioPort; |
| import android.media.AudioRoutesInfo; |
| import android.media.AudioSystem; |
| import android.media.IAudioRoutesObserver; |
| import android.os.Binder; |
| import android.os.RemoteCallbackList; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| import android.text.TextUtils; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| import android.util.Log; |
| import android.util.Slog; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.Set; |
| |
| /** |
| * Class to manage the inventory of all connected devices. |
| * This class is thread-safe. |
| * (non final for mocking/spying) |
| */ |
| public class AudioDeviceInventory { |
| |
| private static final String TAG = "AS.AudioDeviceInventory"; |
| |
| // lock to synchronize all access to mConnectedDevices and mApmConnectedDevices |
| private final Object mDevicesLock = new Object(); |
| |
| // List of connected devices |
| // Key for map created from DeviceInfo.makeDeviceListKey() |
| @GuardedBy("mDevicesLock") |
| private final LinkedHashMap<String, DeviceInfo> mConnectedDevices = new LinkedHashMap<>(); |
| |
| // List of devices actually connected to AudioPolicy (through AudioSystem), only one |
| // by device type, which is used as the key, value is the DeviceInfo generated key. |
| // For the moment only for A2DP sink devices. |
| // TODO: extend to all device types |
| @GuardedBy("mDevicesLock") |
| private final ArrayMap<Integer, String> mApmConnectedDevices = new ArrayMap<>(); |
| |
| // List of preferred devices for strategies |
| private final ArrayMap<Integer, AudioDeviceAddress> mPreferredDevices = new ArrayMap<>(); |
| |
| private @NonNull AudioDeviceBroker mDeviceBroker; |
| |
| // Monitoring of audio routes. Protected by mAudioRoutes. |
| final AudioRoutesInfo mCurAudioRoutes = new AudioRoutesInfo(); |
| final RemoteCallbackList<IAudioRoutesObserver> mRoutesObservers = |
| new RemoteCallbackList<IAudioRoutesObserver>(); |
| |
| /*package*/ AudioDeviceInventory(@NonNull AudioDeviceBroker broker) { |
| mDeviceBroker = broker; |
| } |
| |
| //----------------------------------------------------------- |
| /** for mocking only */ |
| /*package*/ AudioDeviceInventory() { |
| mDeviceBroker = null; |
| } |
| |
| /*package*/ void setDeviceBroker(@NonNull AudioDeviceBroker broker) { |
| mDeviceBroker = broker; |
| } |
| |
| //------------------------------------------------------------ |
| /** |
| * Class to store info about connected devices. |
| * Use makeDeviceListKey() to make a unique key for this list. |
| */ |
| private static class DeviceInfo { |
| final int mDeviceType; |
| final @NonNull String mDeviceName; |
| final @NonNull String mDeviceAddress; |
| int mDeviceCodecFormat; |
| |
| DeviceInfo(int deviceType, String deviceName, String deviceAddress, int deviceCodecFormat) { |
| mDeviceType = deviceType; |
| mDeviceName = deviceName == null ? "" : deviceName; |
| mDeviceAddress = deviceAddress == null ? "" : deviceAddress; |
| mDeviceCodecFormat = deviceCodecFormat; |
| } |
| |
| @Override |
| public String toString() { |
| return "[DeviceInfo: type:0x" + Integer.toHexString(mDeviceType) |
| + " (" + AudioSystem.getDeviceName(mDeviceType) |
| + ") name:" + mDeviceName |
| + " addr:" + mDeviceAddress |
| + " codec: " + Integer.toHexString(mDeviceCodecFormat) + "]"; |
| } |
| |
| String getKey() { |
| return makeDeviceListKey(mDeviceType, mDeviceAddress); |
| } |
| |
| /** |
| * Generate a unique key for the mConnectedDevices List by composing the device "type" |
| * and the "address" associated with a specific instance of that device type |
| */ |
| private static String makeDeviceListKey(int device, String deviceAddress) { |
| return "0x" + Integer.toHexString(device) + ":" + deviceAddress; |
| } |
| } |
| |
| /** |
| * A class just for packaging up a set of connection parameters. |
| */ |
| /*package*/ class WiredDeviceConnectionState { |
| public final int mType; |
| public final @AudioService.ConnectionState int mState; |
| public final String mAddress; |
| public final String mName; |
| public final String mCaller; |
| |
| /*package*/ WiredDeviceConnectionState(int type, @AudioService.ConnectionState int state, |
| String address, String name, String caller) { |
| mType = type; |
| mState = state; |
| mAddress = address; |
| mName = name; |
| mCaller = caller; |
| } |
| } |
| |
| //------------------------------------------------------------ |
| /*package*/ void dump(PrintWriter pw, String prefix) { |
| pw.println("\n" + prefix + "Preferred devices for strategy:"); |
| mPreferredDevices.forEach((strategy, device) -> { |
| pw.println(" " + prefix + "strategy:" + strategy + " device:" + device); }); |
| pw.println("\n" + prefix + "Connected devices:"); |
| mConnectedDevices.forEach((key, deviceInfo) -> { |
| pw.println(" " + prefix + deviceInfo.toString()); }); |
| pw.println("\n" + prefix + "APM Connected device (A2DP sink only):"); |
| mApmConnectedDevices.forEach((keyType, valueAddress) -> { |
| pw.println(" " + prefix + " type:0x" + Integer.toHexString(keyType) |
| + " (" + AudioSystem.getDeviceName(keyType) |
| + ") addr:" + valueAddress); }); |
| } |
| |
| //------------------------------------------------------------ |
| // Message handling from AudioDeviceBroker |
| |
| /** |
| * Restore previously connected devices. Use in case of audio server crash |
| * (see AudioService.onAudioServerDied() method) |
| */ |
| // Always executed on AudioDeviceBroker message queue |
| /*package*/ void onRestoreDevices() { |
| synchronized (mDevicesLock) { |
| //TODO iterate on mApmConnectedDevices instead once it handles all device types |
| for (DeviceInfo di : mConnectedDevices.values()) { |
| AudioSystem.setDeviceConnectionState( |
| di.mDeviceType, |
| AudioSystem.DEVICE_STATE_AVAILABLE, |
| di.mDeviceAddress, |
| di.mDeviceName, |
| di.mDeviceCodecFormat); |
| } |
| } |
| synchronized (mPreferredDevices) { |
| mPreferredDevices.forEach((strategy, device) -> { |
| AudioSystem.setPreferredDeviceForStrategy(strategy, device); }); |
| } |
| } |
| |
| // only public for mocking/spying |
| @GuardedBy("AudioDeviceBroker.mDeviceStateLock") |
| @VisibleForTesting |
| public void onSetA2dpSinkConnectionState(@NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo, |
| @AudioService.BtProfileConnectionState int state) { |
| final BluetoothDevice btDevice = btInfo.getBtDevice(); |
| int a2dpVolume = btInfo.getVolume(); |
| if (AudioService.DEBUG_DEVICES) { |
| Log.d(TAG, "onSetA2dpSinkConnectionState btDevice=" + btDevice + " state=" |
| + state + " vol=" + a2dpVolume); |
| } |
| String address = btDevice.getAddress(); |
| if (address == null) { |
| address = ""; |
| } |
| if (!BluetoothAdapter.checkBluetoothAddress(address)) { |
| address = ""; |
| } |
| |
| final int a2dpCodec = btInfo.getCodec(); |
| |
| AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent( |
| "A2DP sink connected: device addr=" + address + " state=" + state |
| + " codec=" + a2dpCodec |
| + " vol=" + a2dpVolume)); |
| |
| synchronized (mDevicesLock) { |
| final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, |
| btDevice.getAddress()); |
| final DeviceInfo di = mConnectedDevices.get(key); |
| boolean isConnected = di != null; |
| |
| if (isConnected) { |
| if (state == BluetoothProfile.STATE_CONNECTED) { |
| // device is already connected, but we are receiving a connection again, |
| // it could be for a codec change |
| if (a2dpCodec != di.mDeviceCodecFormat) { |
| mDeviceBroker.postBluetoothA2dpDeviceConfigChange(btDevice); |
| } |
| } else { |
| makeA2dpDeviceUnavailableNow(address, di.mDeviceCodecFormat); |
| } |
| } |
| if (a2dpVolume != -1) { |
| mDeviceBroker.postSetVolumeIndexOnDevice(AudioSystem.STREAM_MUSIC, |
| // convert index to internal representation in VolumeStreamState |
| a2dpVolume * 10, |
| AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, "onSetA2dpSinkConnectionState"); |
| } |
| makeA2dpDeviceAvailable(address, BtHelper.getName(btDevice), |
| "onSetA2dpSinkConnectionState", a2dpCodec); |
| } |
| } |
| |
| /*package*/ void onSetA2dpSourceConnectionState( |
| @NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo, int state) { |
| final BluetoothDevice btDevice = btInfo.getBtDevice(); |
| if (AudioService.DEBUG_DEVICES) { |
| Log.d(TAG, "onSetA2dpSourceConnectionState btDevice=" + btDevice + " state=" |
| + state); |
| } |
| String address = btDevice.getAddress(); |
| if (!BluetoothAdapter.checkBluetoothAddress(address)) { |
| address = ""; |
| } |
| |
| synchronized (mDevicesLock) { |
| final String key = DeviceInfo.makeDeviceListKey( |
| AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address); |
| final DeviceInfo di = mConnectedDevices.get(key); |
| boolean isConnected = di != null; |
| |
| if (isConnected && state != BluetoothProfile.STATE_CONNECTED) { |
| makeA2dpSrcUnavailable(address); |
| } else if (!isConnected && state == BluetoothProfile.STATE_CONNECTED) { |
| makeA2dpSrcAvailable(address); |
| } |
| } |
| } |
| |
| /*package*/ void onSetHearingAidConnectionState(BluetoothDevice btDevice, |
| @AudioService.BtProfileConnectionState int state, int streamType) { |
| String address = btDevice.getAddress(); |
| if (!BluetoothAdapter.checkBluetoothAddress(address)) { |
| address = ""; |
| } |
| AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent( |
| "onSetHearingAidConnectionState addr=" + address)); |
| |
| synchronized (mDevicesLock) { |
| final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID, |
| btDevice.getAddress()); |
| final DeviceInfo di = mConnectedDevices.get(key); |
| boolean isConnected = di != null; |
| |
| if (isConnected && state != BluetoothProfile.STATE_CONNECTED) { |
| makeHearingAidDeviceUnavailable(address); |
| } else if (!isConnected && state == BluetoothProfile.STATE_CONNECTED) { |
| makeHearingAidDeviceAvailable(address, BtHelper.getName(btDevice), streamType, |
| "onSetHearingAidConnectionState"); |
| } |
| } |
| } |
| |
| @GuardedBy("AudioDeviceBroker.mDeviceStateLock") |
| /*package*/ void onBluetoothA2dpActiveDeviceChange( |
| @NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo, int event) { |
| final BluetoothDevice btDevice = btInfo.getBtDevice(); |
| if (btDevice == null) { |
| return; |
| } |
| if (AudioService.DEBUG_DEVICES) { |
| Log.d(TAG, "onBluetoothA2dpActiveDeviceChange btDevice=" + btDevice); |
| } |
| int a2dpVolume = btInfo.getVolume(); |
| final int a2dpCodec = btInfo.getCodec(); |
| |
| String address = btDevice.getAddress(); |
| if (!BluetoothAdapter.checkBluetoothAddress(address)) { |
| address = ""; |
| } |
| AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent( |
| "onBluetoothA2dpActiveDeviceChange addr=" + address |
| + " event=" + BtHelper.a2dpDeviceEventToString(event))); |
| |
| synchronized (mDevicesLock) { |
| if (mDeviceBroker.hasScheduledA2dpSinkConnectionState(btDevice)) { |
| AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent( |
| "A2dp config change ignored (scheduled connection change)")); |
| return; |
| } |
| final String key = DeviceInfo.makeDeviceListKey( |
| AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address); |
| final DeviceInfo di = mConnectedDevices.get(key); |
| if (di == null) { |
| Log.e(TAG, "invalid null DeviceInfo in onBluetoothA2dpActiveDeviceChange"); |
| return; |
| } |
| |
| if (event == BtHelper.EVENT_ACTIVE_DEVICE_CHANGE) { |
| // Device is connected |
| if (a2dpVolume != -1) { |
| mDeviceBroker.postSetVolumeIndexOnDevice(AudioSystem.STREAM_MUSIC, |
| // convert index to internal representation in VolumeStreamState |
| a2dpVolume * 10, |
| AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, |
| "onBluetoothA2dpActiveDeviceChange"); |
| } |
| } else if (event == BtHelper.EVENT_DEVICE_CONFIG_CHANGE) { |
| if (di.mDeviceCodecFormat != a2dpCodec) { |
| di.mDeviceCodecFormat = a2dpCodec; |
| mConnectedDevices.replace(key, di); |
| } |
| } |
| if (AudioSystem.handleDeviceConfigChange(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address, |
| BtHelper.getName(btDevice), a2dpCodec) != AudioSystem.AUDIO_STATUS_OK) { |
| int musicDevice = mDeviceBroker.getDeviceForStream(AudioSystem.STREAM_MUSIC); |
| // force A2DP device disconnection in case of error so that AudioService state is |
| // consistent with audio policy manager state |
| setBluetoothA2dpDeviceConnectionState( |
| btDevice, BluetoothA2dp.STATE_DISCONNECTED, BluetoothProfile.A2DP, |
| false /* suppressNoisyIntent */, musicDevice, |
| -1 /* a2dpVolume */); |
| } |
| } |
| } |
| |
| /*package*/ void onMakeA2dpDeviceUnavailableNow(String address, int a2dpCodec) { |
| synchronized (mDevicesLock) { |
| makeA2dpDeviceUnavailableNow(address, a2dpCodec); |
| } |
| } |
| |
| /*package*/ void onReportNewRoutes() { |
| int n = mRoutesObservers.beginBroadcast(); |
| if (n > 0) { |
| AudioRoutesInfo routes; |
| synchronized (mCurAudioRoutes) { |
| routes = new AudioRoutesInfo(mCurAudioRoutes); |
| } |
| while (n > 0) { |
| n--; |
| IAudioRoutesObserver obs = mRoutesObservers.getBroadcastItem(n); |
| try { |
| obs.dispatchAudioRoutesChanged(routes); |
| } catch (RemoteException e) { } |
| } |
| } |
| mRoutesObservers.finishBroadcast(); |
| mDeviceBroker.postObserveDevicesForAllStreams(); |
| } |
| |
| private static final Set<Integer> DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET; |
| static { |
| DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET = new HashSet<>(); |
| DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET.add(AudioSystem.DEVICE_OUT_WIRED_HEADSET); |
| DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET.add(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE); |
| DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET.add(AudioSystem.DEVICE_OUT_LINE); |
| DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET.addAll(AudioSystem.DEVICE_OUT_ALL_USB_SET); |
| } |
| |
| /*package*/ void onSetWiredDeviceConnectionState( |
| AudioDeviceInventory.WiredDeviceConnectionState wdcs) { |
| AudioService.sDeviceLogger.log(new AudioServiceEvents.WiredDevConnectEvent(wdcs)); |
| |
| synchronized (mDevicesLock) { |
| if ((wdcs.mState == AudioService.CONNECTION_STATE_DISCONNECTED) |
| && DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET.contains(wdcs.mType)) { |
| mDeviceBroker.setBluetoothA2dpOnInt(true, |
| "onSetWiredDeviceConnectionState state DISCONNECTED"); |
| } |
| |
| if (!handleDeviceConnection(wdcs.mState == AudioService.CONNECTION_STATE_CONNECTED, |
| wdcs.mType, wdcs.mAddress, wdcs.mName)) { |
| // change of connection state failed, bailout |
| return; |
| } |
| if (wdcs.mState != AudioService.CONNECTION_STATE_DISCONNECTED) { |
| if (DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET.contains(wdcs.mType)) { |
| mDeviceBroker.setBluetoothA2dpOnInt(false, |
| "onSetWiredDeviceConnectionState state not DISCONNECTED"); |
| } |
| mDeviceBroker.checkMusicActive(wdcs.mType, wdcs.mCaller); |
| } |
| if (wdcs.mType == AudioSystem.DEVICE_OUT_HDMI) { |
| mDeviceBroker.checkVolumeCecOnHdmiConnection(wdcs.mState, wdcs.mCaller); |
| } |
| sendDeviceConnectionIntent(wdcs.mType, wdcs.mState, wdcs.mAddress, wdcs.mName); |
| updateAudioRoutes(wdcs.mType, wdcs.mState); |
| } |
| } |
| |
| /*package*/ void onToggleHdmi() { |
| synchronized (mDevicesLock) { |
| // Is HDMI connected? |
| final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HDMI, ""); |
| final DeviceInfo di = mConnectedDevices.get(key); |
| if (di == null) { |
| Log.e(TAG, "invalid null DeviceInfo in onToggleHdmi"); |
| return; |
| } |
| // Toggle HDMI to retrigger broadcast with proper formats. |
| setWiredDeviceConnectionState(AudioSystem.DEVICE_OUT_HDMI, |
| AudioSystem.DEVICE_STATE_UNAVAILABLE, "", "", |
| "android"); // disconnect |
| setWiredDeviceConnectionState(AudioSystem.DEVICE_OUT_HDMI, |
| AudioSystem.DEVICE_STATE_AVAILABLE, "", "", |
| "android"); // reconnect |
| } |
| } |
| |
| /*package*/ void onSaveSetPreferredDevice(int strategy, @NonNull AudioDeviceAddress device) { |
| mPreferredDevices.put(strategy, device); |
| } |
| |
| /*package*/ void onSaveRemovePreferredDevice(int strategy) { |
| mPreferredDevices.remove(strategy); |
| } |
| |
| //------------------------------------------------------------ |
| // |
| |
| /*package*/ int setPreferredDeviceForStrategySync(int strategy, |
| @NonNull AudioDeviceAddress device) { |
| final long identity = Binder.clearCallingIdentity(); |
| final int status = AudioSystem.setPreferredDeviceForStrategy(strategy, device); |
| Binder.restoreCallingIdentity(identity); |
| |
| if (status == AudioSystem.SUCCESS) { |
| mDeviceBroker.postSaveSetPreferredDeviceForStrategy(strategy, device); |
| } |
| return status; |
| } |
| |
| /*package*/ int removePreferredDeviceForStrategySync(int strategy) { |
| final long identity = Binder.clearCallingIdentity(); |
| final int status = AudioSystem.removePreferredDeviceForStrategy(strategy); |
| Binder.restoreCallingIdentity(identity); |
| |
| if (status == AudioSystem.SUCCESS) { |
| mDeviceBroker.postSaveRemovePreferredDeviceForStrategy(strategy); |
| } |
| return status; |
| } |
| |
| /** |
| * Implements the communication with AudioSystem to (dis)connect a device in the native layers |
| * @param connect true if connection |
| * @param device the device type |
| * @param address the address of the device |
| * @param deviceName human-readable name of device |
| * @return false if an error was reported by AudioSystem |
| */ |
| /*package*/ boolean handleDeviceConnection(boolean connect, int device, String address, |
| String deviceName) { |
| if (AudioService.DEBUG_DEVICES) { |
| Slog.i(TAG, "handleDeviceConnection(" + connect + " dev:" |
| + Integer.toHexString(device) + " address:" + address |
| + " name:" + deviceName + ")"); |
| } |
| synchronized (mDevicesLock) { |
| final String deviceKey = DeviceInfo.makeDeviceListKey(device, address); |
| if (AudioService.DEBUG_DEVICES) { |
| Slog.i(TAG, "deviceKey:" + deviceKey); |
| } |
| DeviceInfo di = mConnectedDevices.get(deviceKey); |
| boolean isConnected = di != null; |
| if (AudioService.DEBUG_DEVICES) { |
| Slog.i(TAG, "deviceInfo:" + di + " is(already)Connected:" + isConnected); |
| } |
| if (connect && !isConnected) { |
| final int res = AudioSystem.setDeviceConnectionState(device, |
| AudioSystem.DEVICE_STATE_AVAILABLE, address, deviceName, |
| AudioSystem.AUDIO_FORMAT_DEFAULT); |
| if (res != AudioSystem.AUDIO_STATUS_OK) { |
| Slog.e(TAG, "not connecting device 0x" + Integer.toHexString(device) |
| + " due to command error " + res); |
| return false; |
| } |
| mConnectedDevices.put(deviceKey, new DeviceInfo( |
| device, deviceName, address, AudioSystem.AUDIO_FORMAT_DEFAULT)); |
| mDeviceBroker.postAccessoryPlugMediaUnmute(device); |
| return true; |
| } else if (!connect && isConnected) { |
| AudioSystem.setDeviceConnectionState(device, |
| AudioSystem.DEVICE_STATE_UNAVAILABLE, address, deviceName, |
| AudioSystem.AUDIO_FORMAT_DEFAULT); |
| // always remove even if disconnection failed |
| mConnectedDevices.remove(deviceKey); |
| return true; |
| } |
| Log.w(TAG, "handleDeviceConnection() failed, deviceKey=" + deviceKey |
| + ", deviceSpec=" + di + ", connect=" + connect); |
| } |
| return false; |
| } |
| |
| |
| /*package*/ void disconnectA2dp() { |
| synchronized (mDevicesLock) { |
| final ArraySet<String> toRemove = new ArraySet<>(); |
| // Disconnect ALL DEVICE_OUT_BLUETOOTH_A2DP devices |
| mConnectedDevices.values().forEach(deviceInfo -> { |
| if (deviceInfo.mDeviceType == AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP) { |
| toRemove.add(deviceInfo.mDeviceAddress); |
| } |
| }); |
| if (toRemove.size() > 0) { |
| final int delay = checkSendBecomingNoisyIntentInt( |
| AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, |
| AudioService.CONNECTION_STATE_DISCONNECTED, AudioSystem.DEVICE_NONE); |
| toRemove.stream().forEach(deviceAddress -> |
| makeA2dpDeviceUnavailableLater(deviceAddress, delay) |
| ); |
| } |
| } |
| } |
| |
| /*package*/ void disconnectA2dpSink() { |
| synchronized (mDevicesLock) { |
| final ArraySet<String> toRemove = new ArraySet<>(); |
| // Disconnect ALL DEVICE_IN_BLUETOOTH_A2DP devices |
| mConnectedDevices.values().forEach(deviceInfo -> { |
| if (deviceInfo.mDeviceType == AudioSystem.DEVICE_IN_BLUETOOTH_A2DP) { |
| toRemove.add(deviceInfo.mDeviceAddress); |
| } |
| }); |
| toRemove.stream().forEach(deviceAddress -> makeA2dpSrcUnavailable(deviceAddress)); |
| } |
| } |
| |
| /*package*/ void disconnectHearingAid() { |
| synchronized (mDevicesLock) { |
| final ArraySet<String> toRemove = new ArraySet<>(); |
| // Disconnect ALL DEVICE_OUT_HEARING_AID devices |
| mConnectedDevices.values().forEach(deviceInfo -> { |
| if (deviceInfo.mDeviceType == AudioSystem.DEVICE_OUT_HEARING_AID) { |
| toRemove.add(deviceInfo.mDeviceAddress); |
| } |
| }); |
| if (toRemove.size() > 0) { |
| final int delay = checkSendBecomingNoisyIntentInt( |
| AudioSystem.DEVICE_OUT_HEARING_AID, 0, AudioSystem.DEVICE_NONE); |
| toRemove.stream().forEach(deviceAddress -> |
| // TODO delay not used? |
| makeHearingAidDeviceUnavailable(deviceAddress /*, delay*/) |
| ); |
| } |
| } |
| } |
| |
| // must be called before removing the device from mConnectedDevices |
| // musicDevice argument is used when not AudioSystem.DEVICE_NONE instead of querying |
| // from AudioSystem |
| /*package*/ int checkSendBecomingNoisyIntent(int device, |
| @AudioService.ConnectionState int state, int musicDevice) { |
| synchronized (mDevicesLock) { |
| return checkSendBecomingNoisyIntentInt(device, state, musicDevice); |
| } |
| } |
| |
| /*package*/ AudioRoutesInfo startWatchingRoutes(IAudioRoutesObserver observer) { |
| synchronized (mCurAudioRoutes) { |
| AudioRoutesInfo routes = new AudioRoutesInfo(mCurAudioRoutes); |
| mRoutesObservers.register(observer); |
| return routes; |
| } |
| } |
| |
| /*package*/ AudioRoutesInfo getCurAudioRoutes() { |
| return mCurAudioRoutes; |
| } |
| |
| // only public for mocking/spying |
| @GuardedBy("AudioDeviceBroker.mDeviceStateLock") |
| @VisibleForTesting |
| public void setBluetoothA2dpDeviceConnectionState( |
| @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state, |
| int profile, boolean suppressNoisyIntent, int musicDevice, int a2dpVolume) { |
| int delay; |
| if (profile != BluetoothProfile.A2DP && profile != BluetoothProfile.A2DP_SINK) { |
| throw new IllegalArgumentException("invalid profile " + profile); |
| } |
| synchronized (mDevicesLock) { |
| if (profile == BluetoothProfile.A2DP && !suppressNoisyIntent) { |
| @AudioService.ConnectionState int asState = |
| (state == BluetoothA2dp.STATE_CONNECTED) |
| ? AudioService.CONNECTION_STATE_CONNECTED |
| : AudioService.CONNECTION_STATE_DISCONNECTED; |
| delay = checkSendBecomingNoisyIntentInt(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, |
| asState, musicDevice); |
| } else { |
| delay = 0; |
| } |
| |
| final int a2dpCodec = mDeviceBroker.getA2dpCodec(device); |
| |
| if (AudioService.DEBUG_DEVICES) { |
| Log.i(TAG, "setBluetoothA2dpDeviceConnectionState device: " + device |
| + " state: " + state + " delay(ms): " + delay + "codec:" + a2dpCodec |
| + " suppressNoisyIntent: " + suppressNoisyIntent); |
| } |
| |
| final BtHelper.BluetoothA2dpDeviceInfo a2dpDeviceInfo = |
| new BtHelper.BluetoothA2dpDeviceInfo(device, a2dpVolume, a2dpCodec); |
| if (profile == BluetoothProfile.A2DP) { |
| if (delay == 0) { |
| onSetA2dpSinkConnectionState(a2dpDeviceInfo, state); |
| } else { |
| mDeviceBroker.postA2dpSinkConnection(state, |
| a2dpDeviceInfo, |
| delay); |
| } |
| } else { //profile == BluetoothProfile.A2DP_SINK |
| mDeviceBroker.postA2dpSourceConnection(state, |
| a2dpDeviceInfo, |
| delay); |
| } |
| } |
| } |
| |
| /*package*/ int setWiredDeviceConnectionState(int type, @AudioService.ConnectionState int state, |
| String address, String name, String caller) { |
| synchronized (mDevicesLock) { |
| int delay = checkSendBecomingNoisyIntentInt(type, state, AudioSystem.DEVICE_NONE); |
| mDeviceBroker.postSetWiredDeviceConnectionState( |
| new WiredDeviceConnectionState(type, state, address, name, caller), |
| delay); |
| return delay; |
| } |
| } |
| |
| /*package*/ int setBluetoothHearingAidDeviceConnectionState( |
| @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state, |
| boolean suppressNoisyIntent, int musicDevice) { |
| int delay; |
| synchronized (mDevicesLock) { |
| if (!suppressNoisyIntent) { |
| int intState = (state == BluetoothHearingAid.STATE_CONNECTED) ? 1 : 0; |
| delay = checkSendBecomingNoisyIntentInt(AudioSystem.DEVICE_OUT_HEARING_AID, |
| intState, musicDevice); |
| } else { |
| delay = 0; |
| } |
| mDeviceBroker.postSetHearingAidConnectionState(state, device, delay); |
| return delay; |
| } |
| } |
| |
| |
| //------------------------------------------------------------------- |
| // Internal utilities |
| |
| @GuardedBy("mDevicesLock") |
| private void makeA2dpDeviceAvailable(String address, String name, String eventSource, |
| int a2dpCodec) { |
| // enable A2DP before notifying A2DP connection to avoid unnecessary processing in |
| // audio policy manager |
| mDeviceBroker.setBluetoothA2dpOnInt(true, eventSource); |
| // at this point there could be another A2DP device already connected in APM, but it |
| // doesn't matter as this new one will overwrite the previous one |
| AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, |
| AudioSystem.DEVICE_STATE_AVAILABLE, address, name, a2dpCodec); |
| // Reset A2DP suspend state each time a new sink is connected |
| AudioSystem.setParameters("A2dpSuspended=false"); |
| |
| final DeviceInfo di = new DeviceInfo(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, name, |
| address, a2dpCodec); |
| final String diKey = di.getKey(); |
| mConnectedDevices.put(diKey, di); |
| // on a connection always overwrite the device seen by AudioPolicy, see comment above when |
| // calling AudioSystem |
| mApmConnectedDevices.put(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, diKey); |
| |
| mDeviceBroker.postAccessoryPlugMediaUnmute(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP); |
| setCurrentAudioRouteNameIfPossible(name); |
| } |
| |
| @GuardedBy("mDevicesLock") |
| private void makeA2dpDeviceUnavailableNow(String address, int a2dpCodec) { |
| if (address == null) { |
| return; |
| } |
| final String deviceToRemoveKey = |
| DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address); |
| |
| mConnectedDevices.remove(deviceToRemoveKey); |
| if (!mApmConnectedDevices.get(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP) |
| .equals(deviceToRemoveKey)) { |
| // removing A2DP device not currently used by AudioPolicy, log but don't act on it |
| AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent( |
| "A2DP device " + address + " made unavailable, was not used")).printLog(TAG)); |
| return; |
| } |
| |
| // device to remove was visible by APM, update APM |
| mDeviceBroker.setAvrcpAbsoluteVolumeSupported(false); |
| AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, |
| AudioSystem.DEVICE_STATE_UNAVAILABLE, address, "", a2dpCodec); |
| mApmConnectedDevices.remove(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP); |
| // Remove A2DP routes as well |
| setCurrentAudioRouteNameIfPossible(null); |
| } |
| |
| @GuardedBy("mDevicesLock") |
| private void makeA2dpDeviceUnavailableLater(String address, int delayMs) { |
| // prevent any activity on the A2DP audio output to avoid unwanted |
| // reconnection of the sink. |
| AudioSystem.setParameters("A2dpSuspended=true"); |
| // retrieve DeviceInfo before removing device |
| final String deviceKey = |
| DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address); |
| final DeviceInfo deviceInfo = mConnectedDevices.get(deviceKey); |
| final int a2dpCodec = deviceInfo != null ? deviceInfo.mDeviceCodecFormat : |
| AudioSystem.AUDIO_FORMAT_DEFAULT; |
| // the device will be made unavailable later, so consider it disconnected right away |
| mConnectedDevices.remove(deviceKey); |
| // send the delayed message to make the device unavailable later |
| mDeviceBroker.setA2dpTimeout(address, a2dpCodec, delayMs); |
| } |
| |
| |
| @GuardedBy("mDevicesLock") |
| private void makeA2dpSrcAvailable(String address) { |
| AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, |
| AudioSystem.DEVICE_STATE_AVAILABLE, address, "", |
| AudioSystem.AUDIO_FORMAT_DEFAULT); |
| mConnectedDevices.put( |
| DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address), |
| new DeviceInfo(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, "", |
| address, AudioSystem.AUDIO_FORMAT_DEFAULT)); |
| } |
| |
| @GuardedBy("mDevicesLock") |
| private void makeA2dpSrcUnavailable(String address) { |
| AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, |
| AudioSystem.DEVICE_STATE_UNAVAILABLE, address, "", |
| AudioSystem.AUDIO_FORMAT_DEFAULT); |
| mConnectedDevices.remove( |
| DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address)); |
| } |
| |
| @GuardedBy("mDevicesLock") |
| private void makeHearingAidDeviceAvailable( |
| String address, String name, int streamType, String eventSource) { |
| final int hearingAidVolIndex = mDeviceBroker.getVssVolumeForDevice(streamType, |
| AudioSystem.DEVICE_OUT_HEARING_AID); |
| mDeviceBroker.postSetHearingAidVolumeIndex(hearingAidVolIndex, streamType); |
| |
| AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_HEARING_AID, |
| AudioSystem.DEVICE_STATE_AVAILABLE, address, name, |
| AudioSystem.AUDIO_FORMAT_DEFAULT); |
| mConnectedDevices.put( |
| DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID, address), |
| new DeviceInfo(AudioSystem.DEVICE_OUT_HEARING_AID, name, |
| address, AudioSystem.AUDIO_FORMAT_DEFAULT)); |
| mDeviceBroker.postAccessoryPlugMediaUnmute(AudioSystem.DEVICE_OUT_HEARING_AID); |
| mDeviceBroker.postApplyVolumeOnDevice(streamType, |
| AudioSystem.DEVICE_OUT_HEARING_AID, "makeHearingAidDeviceAvailable"); |
| setCurrentAudioRouteNameIfPossible(name); |
| } |
| |
| @GuardedBy("mDevicesLock") |
| private void makeHearingAidDeviceUnavailable(String address) { |
| AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_HEARING_AID, |
| AudioSystem.DEVICE_STATE_UNAVAILABLE, address, "", |
| AudioSystem.AUDIO_FORMAT_DEFAULT); |
| mConnectedDevices.remove( |
| DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID, address)); |
| // Remove Hearing Aid routes as well |
| setCurrentAudioRouteNameIfPossible(null); |
| } |
| |
| @GuardedBy("mDevicesLock") |
| private void setCurrentAudioRouteNameIfPossible(String name) { |
| synchronized (mCurAudioRoutes) { |
| if (TextUtils.equals(mCurAudioRoutes.bluetoothName, name)) { |
| return; |
| } |
| if (name != null || !isCurrentDeviceConnected()) { |
| mCurAudioRoutes.bluetoothName = name; |
| mDeviceBroker.postReportNewRoutes(); |
| } |
| } |
| } |
| |
| @GuardedBy("mDevicesLock") |
| private boolean isCurrentDeviceConnected() { |
| return mConnectedDevices.values().stream().anyMatch(deviceInfo -> |
| TextUtils.equals(deviceInfo.mDeviceName, mCurAudioRoutes.bluetoothName)); |
| } |
| |
| // Devices which removal triggers intent ACTION_AUDIO_BECOMING_NOISY. The intent is only |
| // sent if: |
| // - none of these devices are connected anymore after one is disconnected AND |
| // - the device being disconnected is actually used for music. |
| // Access synchronized on mConnectedDevices |
| private static final Set<Integer> BECOMING_NOISY_INTENT_DEVICES_SET; |
| static { |
| BECOMING_NOISY_INTENT_DEVICES_SET = new HashSet<>(); |
| BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_WIRED_HEADSET); |
| BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE); |
| BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_HDMI); |
| BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_ANLG_DOCK_HEADSET); |
| BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_DGTL_DOCK_HEADSET); |
| BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_LINE); |
| BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_HEARING_AID); |
| BECOMING_NOISY_INTENT_DEVICES_SET.addAll(AudioSystem.DEVICE_OUT_ALL_A2DP_SET); |
| BECOMING_NOISY_INTENT_DEVICES_SET.addAll(AudioSystem.DEVICE_OUT_ALL_USB_SET); |
| } |
| |
| // must be called before removing the device from mConnectedDevices |
| // musicDevice argument is used when not AudioSystem.DEVICE_NONE instead of querying |
| // from AudioSystem |
| @GuardedBy("mDevicesLock") |
| private int checkSendBecomingNoisyIntentInt(int device, |
| @AudioService.ConnectionState int state, int musicDevice) { |
| if (state != AudioService.CONNECTION_STATE_DISCONNECTED) { |
| return 0; |
| } |
| if (!BECOMING_NOISY_INTENT_DEVICES_SET.contains(device)) { |
| return 0; |
| } |
| int delay = 0; |
| Set<Integer> devices = new HashSet<>(); |
| for (DeviceInfo di : mConnectedDevices.values()) { |
| if (((di.mDeviceType & AudioSystem.DEVICE_BIT_IN) == 0) |
| && BECOMING_NOISY_INTENT_DEVICES_SET.contains(di.mDeviceType)) { |
| devices.add(di.mDeviceType); |
| } |
| } |
| if (musicDevice == AudioSystem.DEVICE_NONE) { |
| musicDevice = mDeviceBroker.getDeviceForStream(AudioSystem.STREAM_MUSIC); |
| } |
| |
| // always ignore condition on device being actually used for music when in communication |
| // because music routing is altered in this case. |
| // also checks whether media routing if affected by a dynamic policy or mirroring |
| if (((device == musicDevice) || mDeviceBroker.isInCommunication()) |
| && AudioSystem.isSingleAudioDeviceType(devices, device) |
| && !mDeviceBroker.hasMediaDynamicPolicy() |
| && (musicDevice != AudioSystem.DEVICE_OUT_REMOTE_SUBMIX)) { |
| if (!AudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0 /*not looking in past*/) |
| && !mDeviceBroker.hasAudioFocusUsers()) { |
| // no media playback, not a "becoming noisy" situation, otherwise it could cause |
| // the pausing of some apps that are playing remotely |
| AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent( |
| "dropping ACTION_AUDIO_BECOMING_NOISY")).printLog(TAG)); |
| return 0; |
| } |
| mDeviceBroker.postBroadcastBecomingNoisy(); |
| delay = AudioService.BECOMING_NOISY_DELAY_MS; |
| } |
| |
| return delay; |
| } |
| |
| // Intent "extra" data keys. |
| private static final String CONNECT_INTENT_KEY_PORT_NAME = "portName"; |
| private static final String CONNECT_INTENT_KEY_STATE = "state"; |
| private static final String CONNECT_INTENT_KEY_ADDRESS = "address"; |
| private static final String CONNECT_INTENT_KEY_HAS_PLAYBACK = "hasPlayback"; |
| private static final String CONNECT_INTENT_KEY_HAS_CAPTURE = "hasCapture"; |
| private static final String CONNECT_INTENT_KEY_HAS_MIDI = "hasMIDI"; |
| private static final String CONNECT_INTENT_KEY_DEVICE_CLASS = "class"; |
| |
| private void sendDeviceConnectionIntent(int device, int state, String address, |
| String deviceName) { |
| if (AudioService.DEBUG_DEVICES) { |
| Slog.i(TAG, "sendDeviceConnectionIntent(dev:0x" + Integer.toHexString(device) |
| + " state:0x" + Integer.toHexString(state) + " address:" + address |
| + " name:" + deviceName + ");"); |
| } |
| Intent intent = new Intent(); |
| |
| switch(device) { |
| case AudioSystem.DEVICE_OUT_WIRED_HEADSET: |
| intent.setAction(Intent.ACTION_HEADSET_PLUG); |
| intent.putExtra("microphone", 1); |
| break; |
| case AudioSystem.DEVICE_OUT_WIRED_HEADPHONE: |
| case AudioSystem.DEVICE_OUT_LINE: |
| intent.setAction(Intent.ACTION_HEADSET_PLUG); |
| intent.putExtra("microphone", 0); |
| break; |
| case AudioSystem.DEVICE_OUT_USB_HEADSET: |
| intent.setAction(Intent.ACTION_HEADSET_PLUG); |
| intent.putExtra("microphone", |
| AudioSystem.getDeviceConnectionState(AudioSystem.DEVICE_IN_USB_HEADSET, "") |
| == AudioSystem.DEVICE_STATE_AVAILABLE ? 1 : 0); |
| break; |
| case AudioSystem.DEVICE_IN_USB_HEADSET: |
| if (AudioSystem.getDeviceConnectionState(AudioSystem.DEVICE_OUT_USB_HEADSET, "") |
| == AudioSystem.DEVICE_STATE_AVAILABLE) { |
| intent.setAction(Intent.ACTION_HEADSET_PLUG); |
| intent.putExtra("microphone", 1); |
| } else { |
| // do not send ACTION_HEADSET_PLUG when only the input side is seen as changing |
| return; |
| } |
| break; |
| case AudioSystem.DEVICE_OUT_HDMI: |
| case AudioSystem.DEVICE_OUT_HDMI_ARC: |
| configureHdmiPlugIntent(intent, state); |
| break; |
| } |
| |
| if (intent.getAction() == null) { |
| return; |
| } |
| |
| intent.putExtra(CONNECT_INTENT_KEY_STATE, state); |
| intent.putExtra(CONNECT_INTENT_KEY_ADDRESS, address); |
| intent.putExtra(CONNECT_INTENT_KEY_PORT_NAME, deviceName); |
| |
| intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); |
| |
| final long ident = Binder.clearCallingIdentity(); |
| try { |
| ActivityManager.broadcastStickyIntent(intent, UserHandle.USER_CURRENT); |
| } finally { |
| Binder.restoreCallingIdentity(ident); |
| } |
| } |
| |
| private void updateAudioRoutes(int device, int state) { |
| int connType = 0; |
| |
| switch (device) { |
| case AudioSystem.DEVICE_OUT_WIRED_HEADSET: |
| connType = AudioRoutesInfo.MAIN_HEADSET; |
| break; |
| case AudioSystem.DEVICE_OUT_WIRED_HEADPHONE: |
| case AudioSystem.DEVICE_OUT_LINE: |
| connType = AudioRoutesInfo.MAIN_HEADPHONES; |
| break; |
| case AudioSystem.DEVICE_OUT_HDMI: |
| case AudioSystem.DEVICE_OUT_HDMI_ARC: |
| connType = AudioRoutesInfo.MAIN_HDMI; |
| break; |
| case AudioSystem.DEVICE_OUT_USB_DEVICE: |
| case AudioSystem.DEVICE_OUT_USB_HEADSET: |
| connType = AudioRoutesInfo.MAIN_USB; |
| break; |
| } |
| |
| synchronized (mCurAudioRoutes) { |
| if (connType == 0) { |
| return; |
| } |
| int newConn = mCurAudioRoutes.mainType; |
| if (state != 0) { |
| newConn |= connType; |
| } else { |
| newConn &= ~connType; |
| } |
| if (newConn != mCurAudioRoutes.mainType) { |
| mCurAudioRoutes.mainType = newConn; |
| mDeviceBroker.postReportNewRoutes(); |
| } |
| } |
| } |
| |
| private void configureHdmiPlugIntent(Intent intent, @AudioService.ConnectionState int state) { |
| intent.setAction(AudioManager.ACTION_HDMI_AUDIO_PLUG); |
| intent.putExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, state); |
| if (state != AudioService.CONNECTION_STATE_CONNECTED) { |
| return; |
| } |
| ArrayList<AudioPort> ports = new ArrayList<AudioPort>(); |
| int[] portGeneration = new int[1]; |
| int status = AudioSystem.listAudioPorts(ports, portGeneration); |
| if (status != AudioManager.SUCCESS) { |
| Log.e(TAG, "listAudioPorts error " + status + " in configureHdmiPlugIntent"); |
| return; |
| } |
| for (AudioPort port : ports) { |
| if (!(port instanceof AudioDevicePort)) { |
| continue; |
| } |
| final AudioDevicePort devicePort = (AudioDevicePort) port; |
| if (devicePort.type() != AudioManager.DEVICE_OUT_HDMI |
| && devicePort.type() != AudioManager.DEVICE_OUT_HDMI_ARC) { |
| continue; |
| } |
| // found an HDMI port: format the list of supported encodings |
| int[] formats = AudioFormat.filterPublicFormats(devicePort.formats()); |
| if (formats.length > 0) { |
| ArrayList<Integer> encodingList = new ArrayList(1); |
| for (int format : formats) { |
| // a format in the list can be 0, skip it |
| if (format != AudioFormat.ENCODING_INVALID) { |
| encodingList.add(format); |
| } |
| } |
| final int[] encodingArray = encodingList.stream().mapToInt(i -> i).toArray(); |
| intent.putExtra(AudioManager.EXTRA_ENCODINGS, encodingArray); |
| } |
| // find the maximum supported number of channels |
| int maxChannels = 0; |
| for (int mask : devicePort.channelMasks()) { |
| int channelCount = AudioFormat.channelCountFromOutChannelMask(mask); |
| if (channelCount > maxChannels) { |
| maxChannels = channelCount; |
| } |
| } |
| intent.putExtra(AudioManager.EXTRA_MAX_CHANNEL_COUNT, maxChannels); |
| } |
| } |
| |
| //---------------------------------------------------------- |
| // For tests only |
| |
| /** |
| * Check if device is in the list of connected devices |
| * @param device |
| * @return true if connected |
| */ |
| @VisibleForTesting |
| public boolean isA2dpDeviceConnected(@NonNull BluetoothDevice device) { |
| final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, |
| device.getAddress()); |
| synchronized (mDevicesLock) { |
| return (mConnectedDevices.get(key) != null); |
| } |
| } |
| } |