| /* |
| * Copyright (C) 2022 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.telecom; |
| |
| import static com.android.server.telecom.AudioRoute.BT_AUDIO_DEVICE_INFO_TYPES; |
| |
| import android.bluetooth.BluetoothDevice; |
| import android.content.Context; |
| import android.media.AudioDeviceInfo; |
| import android.media.AudioManager; |
| import android.telecom.Log; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.server.telecom.bluetooth.BluetoothRouteManager; |
| import com.android.server.telecom.flags.Flags; |
| |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.concurrent.Semaphore; |
| import java.util.concurrent.locks.Lock; |
| import java.util.concurrent.locks.ReentrantLock; |
| |
| /** |
| * Helper class used to keep track of the requested communication device within Telecom for audio |
| * use cases. Handles the set/clear communication use case logic for all audio routes (speaker, BT, |
| * headset, and earpiece). For BT devices, this handles switches between hearing aids, SCO, and LE |
| * audio (also takes into account switching between multiple LE audio devices). |
| */ |
| public class CallAudioCommunicationDeviceTracker { |
| |
| // Use -1 indicates device is not set for any communication use case |
| private static final int sAUDIO_DEVICE_TYPE_INVALID = -1; |
| private AudioManager mAudioManager; |
| private BluetoothRouteManager mBluetoothRouteManager; |
| private @AudioDeviceInfo.AudioDeviceType int mAudioDeviceType = sAUDIO_DEVICE_TYPE_INVALID; |
| // Keep track of the locally requested BT audio device if set |
| private String mBtAudioDevice = null; |
| private final Lock mLock = new ReentrantLock(); |
| |
| public CallAudioCommunicationDeviceTracker(Context context) { |
| mAudioManager = context.getSystemService(AudioManager.class); |
| } |
| |
| public void setBluetoothRouteManager(BluetoothRouteManager bluetoothRouteManager) { |
| mBluetoothRouteManager = bluetoothRouteManager; |
| } |
| |
| public boolean isAudioDeviceSetForType(@AudioDeviceInfo.AudioDeviceType int audioDeviceType) { |
| if (Flags.communicationDeviceProtectedByLock()) { |
| mLock.lock(); |
| } |
| try { |
| return mAudioDeviceType == audioDeviceType; |
| } finally { |
| if (Flags.communicationDeviceProtectedByLock()) { |
| mLock.unlock(); |
| } |
| } |
| } |
| |
| public int getCurrentLocallyRequestedCommunicationDevice() { |
| if (Flags.communicationDeviceProtectedByLock()) { |
| mLock.lock(); |
| } |
| try { |
| return mAudioDeviceType; |
| } finally { |
| if (Flags.communicationDeviceProtectedByLock()) { |
| mLock.unlock(); |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| public void setTestCommunicationDevice(@AudioDeviceInfo.AudioDeviceType int audioDeviceType) { |
| mAudioDeviceType = audioDeviceType; |
| } |
| |
| public void clearBtCommunicationDevice() { |
| if (Flags.communicationDeviceProtectedByLock()) { |
| mLock.lock(); |
| } |
| try { |
| if (mBtAudioDevice == null) { |
| Log.i(this, "No bluetooth device was set for communication that can be cleared."); |
| } else { |
| // If mBtAudioDevice is set, we know a BT audio device was set for communication so |
| // mAudioDeviceType corresponds to a BT device type (e.g. hearing aid, SCO, LE). |
| processClearCommunicationDevice(mAudioDeviceType); |
| } |
| } finally { |
| if (Flags.communicationDeviceProtectedByLock()) { |
| mLock.unlock(); |
| } |
| } |
| } |
| |
| /* |
| * Sets the communication device for the passed in audio device type, if it's available for |
| * communication use cases. Tries to clear any communication device which was previously |
| * requested for communication before setting the new device. |
| * @param audioDeviceTypes The supported audio device types for the device. |
| * @param btDevice The bluetooth device to connect to (only used for switching between multiple |
| * LE audio devices). |
| * @return {@code true} if the device was set for communication, {@code false} if the device |
| * wasn't set. |
| */ |
| public boolean setCommunicationDevice(@AudioDeviceInfo.AudioDeviceType int audioDeviceType, |
| BluetoothDevice btDevice) { |
| if (Flags.communicationDeviceProtectedByLock()) { |
| mLock.lock(); |
| } |
| try { |
| return processSetCommunicationDevice(audioDeviceType, btDevice); |
| } finally { |
| if (Flags.communicationDeviceProtectedByLock()) { |
| mLock.unlock(); |
| } |
| } |
| } |
| |
| private boolean processSetCommunicationDevice( |
| @AudioDeviceInfo.AudioDeviceType int audioDeviceType, BluetoothDevice btDevice) { |
| // There is only one audio device type associated with each type of BT device. |
| boolean isBtDevice = BT_AUDIO_DEVICE_INFO_TYPES.contains(audioDeviceType); |
| Log.i(this, "setCommunicationDevice: type = %s, isBtDevice = %s, btDevice = %s", |
| audioDeviceType, isBtDevice, btDevice); |
| |
| // Account for switching between multiple LE audio devices. |
| boolean handleLeAudioDeviceSwitch = btDevice != null |
| && !btDevice.getAddress().equals(mBtAudioDevice); |
| if ((audioDeviceType == mAudioDeviceType |
| || isUsbHeadsetType(audioDeviceType, mAudioDeviceType) |
| || isSpeakerType(audioDeviceType, mAudioDeviceType)) |
| && !handleLeAudioDeviceSwitch) { |
| Log.i(this, "Communication device is already set for this audio type"); |
| return false; |
| } |
| |
| AudioDeviceInfo activeDevice = null; |
| List<AudioDeviceInfo> devices = mAudioManager.getAvailableCommunicationDevices(); |
| if (devices.size() == 0) { |
| Log.w(this, "No communication devices available"); |
| return false; |
| } |
| |
| for (AudioDeviceInfo device : devices) { |
| Log.i(this, "Available device type: " + device.getType()); |
| // Ensure that we do not select the same BT LE audio device for communication. |
| if ((audioDeviceType == device.getType() |
| || isUsbHeadsetType(audioDeviceType, device.getType()) |
| || isSpeakerType(audioDeviceType, device.getType())) |
| && !device.getAddress().equals(mBtAudioDevice)) { |
| activeDevice = device; |
| break; |
| } |
| } |
| |
| if (activeDevice == null) { |
| Log.i(this, "No active device of type(s) %s available", |
| audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET |
| ? Arrays.asList(AudioDeviceInfo.TYPE_WIRED_HEADSET, |
| AudioDeviceInfo.TYPE_USB_HEADSET) |
| : audioDeviceType); |
| return false; |
| } |
| |
| // Force clear previous communication device, if one was set, before setting the new device. |
| if (mAudioDeviceType != sAUDIO_DEVICE_TYPE_INVALID) { |
| processClearCommunicationDevice(mAudioDeviceType); |
| } |
| |
| // Turn activeDevice ON. |
| boolean result = mAudioManager.setCommunicationDevice(activeDevice); |
| if (!result) { |
| Log.w(this, "Could not set active device"); |
| } else { |
| Log.i(this, "Active device set"); |
| mAudioDeviceType = activeDevice.getType(); |
| if (isBtDevice) { |
| mBtAudioDevice = activeDevice.getAddress(); |
| if (audioDeviceType == AudioDeviceInfo.TYPE_BLE_HEADSET) { |
| mBluetoothRouteManager.onAudioOn(mBtAudioDevice); |
| } |
| } else if (Flags.communicationDeviceProtectedByLock()) { |
| // Clear BT device if it's still stored. Handles race condition for when a non-BT |
| // device is set for communication shortly after a BT (LE) device is set for |
| // communication but the selection hasn't been cleared yet. |
| mBtAudioDevice = null; |
| } |
| } |
| return result; |
| } |
| /* |
| * Clears the communication device for the passed in audio device types, given that the device |
| * has previously been set for communication. |
| * @param audioDeviceTypes The supported audio device types for the device. |
| */ |
| public void clearCommunicationDevice(@AudioDeviceInfo.AudioDeviceType int audioDeviceType) { |
| if (Flags.communicationDeviceProtectedByLock()) { |
| mLock.lock(); |
| } |
| try { |
| processClearCommunicationDevice(audioDeviceType); |
| } finally { |
| if (Flags.communicationDeviceProtectedByLock()) { |
| mLock.unlock(); |
| } |
| } |
| } |
| |
| public void processClearCommunicationDevice( |
| @AudioDeviceInfo.AudioDeviceType int audioDeviceType) { |
| if (audioDeviceType == sAUDIO_DEVICE_TYPE_INVALID) { |
| Log.i(this, "clearCommunicationDevice: Skip clearing communication device" |
| + "for invalid audio type (-1)."); |
| } |
| |
| // There is only one audio device type associated with each type of BT device. |
| boolean isBtDevice = BT_AUDIO_DEVICE_INFO_TYPES.contains(audioDeviceType); |
| Log.i(this, "clearCommunicationDevice: type = %s, isBtDevice = %s", |
| audioDeviceType, isBtDevice); |
| |
| if (audioDeviceType != mAudioDeviceType |
| && !isUsbHeadsetType(audioDeviceType, mAudioDeviceType) |
| && !isSpeakerType(audioDeviceType, mAudioDeviceType)) { |
| Log.i(this, "Unable to clear communication device of type(s) %s. " |
| + "Device does not correspond to the locally requested device type %s.", |
| audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET |
| ? Arrays.asList(AudioDeviceInfo.TYPE_WIRED_HEADSET, |
| AudioDeviceInfo.TYPE_USB_HEADSET) |
| : audioDeviceType, |
| mAudioDeviceType |
| ); |
| return; |
| } |
| |
| if (mAudioManager == null) { |
| Log.i(this, "clearCommunicationDevice: mAudioManager is null"); |
| return; |
| } |
| |
| // Clear device and reset locally saved device type. |
| Log.i(this, "clearCommunicationDevice: AudioManager#clearCommunicationDevice()"); |
| mAudioManager.clearCommunicationDevice(); |
| mAudioDeviceType = sAUDIO_DEVICE_TYPE_INVALID; |
| |
| if (isBtDevice && mBtAudioDevice != null) { |
| // Signal that BT audio was lost for device. |
| mBluetoothRouteManager.onAudioLost(mBtAudioDevice); |
| mBtAudioDevice = null; |
| } |
| } |
| |
| private boolean isUsbHeadsetType(@AudioDeviceInfo.AudioDeviceType int audioDeviceType, |
| @AudioDeviceInfo.AudioDeviceType int sourceType) { |
| return audioDeviceType == AudioDeviceInfo.TYPE_WIRED_HEADSET |
| && sourceType == AudioDeviceInfo.TYPE_USB_HEADSET; |
| } |
| |
| private boolean isSpeakerType(@AudioDeviceInfo.AudioDeviceType int audioDeviceType, |
| @AudioDeviceInfo.AudioDeviceType int sourceType) { |
| if (!Flags.busDeviceIsASpeaker()) return false; |
| return audioDeviceType == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER |
| && sourceType == AudioDeviceInfo.TYPE_BUS; |
| } |
| } |