/* * Copyright (C) 2025 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.systemui.accessibility.hearingaid import android.content.Context import android.media.AudioManager import android.util.Log import androidx.collection.ArraySet import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.HapClientProfile import com.android.settingslib.bluetooth.HearingAidAudioRoutingConstants import com.android.settingslib.bluetooth.HearingAidAudioRoutingHelper import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.res.R import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** * The controller of the hearing device input routing. * *

It manages and update the input routing according to the value. */ open class HearingDevicesInputRoutingController @AssistedInject constructor( @Assisted context: Context, private val audioManager: AudioManager, @Background private val backgroundDispatcher: CoroutineDispatcher, ) { private val audioRoutingHelper = HearingAidAudioRoutingHelper(context) private var cachedDevice: CachedBluetoothDevice? = null private val bgCoroutineScope = CoroutineScope(backgroundDispatcher) /** Factory to create a [HearingDevicesInputRoutingController] instance. */ @AssistedFactory interface Factory { fun create(context: Context): HearingDevicesInputRoutingController } /** Possible input routing UI. Need to align with [getInputRoutingOptions] */ enum class InputRoutingValue { HEARING_DEVICE, BUILTIN_MIC, } companion object { private const val TAG = "HearingDevicesInputRoutingController" /** Gets input routing options as strings. */ @JvmStatic fun getInputRoutingOptions(context: Context): Array { return context.resources.getStringArray(R.array.hearing_device_input_routing_options) } } fun interface InputRoutingControlAvailableCallback { fun onResult(available: Boolean) } /** * Sets the device for this controller to control the input routing. * * @param device the [CachedBluetoothDevice] set to the controller */ fun setDevice(device: CachedBluetoothDevice?) { this@HearingDevicesInputRoutingController.cachedDevice = device } fun isInputRoutingControlAvailable(callback: InputRoutingControlAvailableCallback) { bgCoroutineScope.launch { val result = isInputRoutingControlAvailableInternal() callback.onResult(result) } } /** * Checks if input routing control is available for the currently set device. * * @return `true` if input routing control is available. */ private suspend fun isInputRoutingControlAvailableInternal(): Boolean { val device = cachedDevice ?: return false val memberDevices = device.memberDevice val inputInfos = withContext(backgroundDispatcher) { audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS) } val supportedInputDeviceAddresses = ArraySet() supportedInputDeviceAddresses.add(device.address) if (memberDevices.isNotEmpty()) { memberDevices.forEach { supportedInputDeviceAddresses.add(it.address) } } val isValidInputDevice = inputInfos.any { supportedInputDeviceAddresses.contains(it.address) } // Not support ASHA hearing device for input routing feature val isHapHearingDevice = device.profiles.any { profile -> profile is HapClientProfile } if (isHapHearingDevice && !isValidInputDevice) { Log.d(TAG, "Not supported input type hearing device.") } return isHapHearingDevice && isValidInputDevice } /** Gets the user's preferred [InputRoutingValue]. */ fun getUserPreferredInputRoutingValue(): Int { val device = cachedDevice ?: return InputRoutingValue.HEARING_DEVICE.ordinal return if (device.device.isMicrophonePreferredForCalls) { InputRoutingValue.HEARING_DEVICE.ordinal } else { InputRoutingValue.BUILTIN_MIC.ordinal } } /** * Sets the input routing to [android.bluetooth.BluetoothDevice.setMicrophonePreferredForCalls] * based on the input routing index. * * @param inputRoutingIndex The desired input routing index. */ fun selectInputRouting(inputRoutingIndex: Int) { val device = cachedDevice ?: return val useBuiltinMic = (inputRoutingIndex == InputRoutingValue.BUILTIN_MIC.ordinal) val status = audioRoutingHelper.setPreferredInputDeviceForCalls( device, if (useBuiltinMic) HearingAidAudioRoutingConstants.RoutingValue.BUILTIN_DEVICE else HearingAidAudioRoutingConstants.RoutingValue.AUTO, ) if (!status) { Log.d(TAG, "Fail to configure setPreferredInputDeviceForCalls") } setMicrophonePreferredForCallsForDeviceSet(device, !useBuiltinMic) } private fun setMicrophonePreferredForCallsForDeviceSet( device: CachedBluetoothDevice?, enabled: Boolean, ) { device ?: return device.device.isMicrophonePreferredForCalls = enabled val memberDevices = device.memberDevice if (memberDevices.isNotEmpty()) { memberDevices.forEach { d -> d.device.isMicrophonePreferredForCalls = enabled } } } }