1 /* <lambda>null2 * Copyright (C) 2025 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.accessibility.hearingaid 18 19 import android.content.Context 20 import android.media.AudioManager 21 import android.util.Log 22 import androidx.collection.ArraySet 23 import com.android.settingslib.bluetooth.CachedBluetoothDevice 24 import com.android.settingslib.bluetooth.HapClientProfile 25 import com.android.settingslib.bluetooth.HearingAidAudioRoutingConstants 26 import com.android.settingslib.bluetooth.HearingAidAudioRoutingHelper 27 import com.android.systemui.dagger.qualifiers.Background 28 import com.android.systemui.res.R 29 import dagger.assisted.Assisted 30 import dagger.assisted.AssistedFactory 31 import dagger.assisted.AssistedInject 32 import kotlinx.coroutines.CoroutineDispatcher 33 import kotlinx.coroutines.CoroutineScope 34 import kotlinx.coroutines.launch 35 import kotlinx.coroutines.withContext 36 37 /** 38 * The controller of the hearing device input routing. 39 * 40 * <p> It manages and update the input routing according to the value. 41 */ 42 open class HearingDevicesInputRoutingController 43 @AssistedInject 44 constructor( 45 @Assisted context: Context, 46 private val audioManager: AudioManager, 47 @Background private val backgroundDispatcher: CoroutineDispatcher, 48 ) { 49 private val audioRoutingHelper = HearingAidAudioRoutingHelper(context) 50 private var cachedDevice: CachedBluetoothDevice? = null 51 private val bgCoroutineScope = CoroutineScope(backgroundDispatcher) 52 53 /** Factory to create a [HearingDevicesInputRoutingController] instance. */ 54 @AssistedFactory 55 interface Factory { 56 fun create(context: Context): HearingDevicesInputRoutingController 57 } 58 59 /** Possible input routing UI. Need to align with [getInputRoutingOptions] */ 60 enum class InputRoutingValue { 61 HEARING_DEVICE, 62 BUILTIN_MIC, 63 } 64 65 companion object { 66 private const val TAG = "HearingDevicesInputRoutingController" 67 68 /** Gets input routing options as strings. */ 69 @JvmStatic 70 fun getInputRoutingOptions(context: Context): Array<String> { 71 return context.resources.getStringArray(R.array.hearing_device_input_routing_options) 72 } 73 } 74 75 fun interface InputRoutingControlAvailableCallback { 76 fun onResult(available: Boolean) 77 } 78 79 /** 80 * Sets the device for this controller to control the input routing. 81 * 82 * @param device the [CachedBluetoothDevice] set to the controller 83 */ 84 fun setDevice(device: CachedBluetoothDevice?) { 85 this@HearingDevicesInputRoutingController.cachedDevice = device 86 } 87 88 fun isInputRoutingControlAvailable(callback: InputRoutingControlAvailableCallback) { 89 bgCoroutineScope.launch { 90 val result = isInputRoutingControlAvailableInternal() 91 callback.onResult(result) 92 } 93 } 94 95 /** 96 * Checks if input routing control is available for the currently set device. 97 * 98 * @return `true` if input routing control is available. 99 */ 100 private suspend fun isInputRoutingControlAvailableInternal(): Boolean { 101 val device = cachedDevice ?: return false 102 103 val memberDevices = device.memberDevice 104 105 val inputInfos = 106 withContext(backgroundDispatcher) { 107 audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS) 108 } 109 val supportedInputDeviceAddresses = ArraySet<String>() 110 supportedInputDeviceAddresses.add(device.address) 111 if (memberDevices.isNotEmpty()) { 112 memberDevices.forEach { supportedInputDeviceAddresses.add(it.address) } 113 } 114 115 val isValidInputDevice = 116 inputInfos.any { supportedInputDeviceAddresses.contains(it.address) } 117 // Not support ASHA hearing device for input routing feature 118 val isHapHearingDevice = device.profiles.any { profile -> profile is HapClientProfile } 119 120 if (isHapHearingDevice && !isValidInputDevice) { 121 Log.d(TAG, "Not supported input type hearing device.") 122 } 123 return isHapHearingDevice && isValidInputDevice 124 } 125 126 /** Gets the user's preferred [InputRoutingValue]. */ 127 fun getUserPreferredInputRoutingValue(): Int { 128 val device = cachedDevice ?: return InputRoutingValue.HEARING_DEVICE.ordinal 129 130 return if (device.device.isMicrophonePreferredForCalls) { 131 InputRoutingValue.HEARING_DEVICE.ordinal 132 } else { 133 InputRoutingValue.BUILTIN_MIC.ordinal 134 } 135 } 136 137 /** 138 * Sets the input routing to [android.bluetooth.BluetoothDevice.setMicrophonePreferredForCalls] 139 * based on the input routing index. 140 * 141 * @param inputRoutingIndex The desired input routing index. 142 */ 143 fun selectInputRouting(inputRoutingIndex: Int) { 144 val device = cachedDevice ?: return 145 146 val useBuiltinMic = (inputRoutingIndex == InputRoutingValue.BUILTIN_MIC.ordinal) 147 val status = 148 audioRoutingHelper.setPreferredInputDeviceForCalls( 149 device, 150 if (useBuiltinMic) HearingAidAudioRoutingConstants.RoutingValue.BUILTIN_DEVICE 151 else HearingAidAudioRoutingConstants.RoutingValue.AUTO, 152 ) 153 if (!status) { 154 Log.d(TAG, "Fail to configure setPreferredInputDeviceForCalls") 155 } 156 setMicrophonePreferredForCallsForDeviceSet(device, !useBuiltinMic) 157 } 158 159 private fun setMicrophonePreferredForCallsForDeviceSet( 160 device: CachedBluetoothDevice?, 161 enabled: Boolean, 162 ) { 163 device ?: return 164 device.device.isMicrophonePreferredForCalls = enabled 165 val memberDevices = device.memberDevice 166 if (memberDevices.isNotEmpty()) { 167 memberDevices.forEach { d -> d.device.isMicrophonePreferredForCalls = enabled } 168 } 169 } 170 } 171