/* * 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.bluetooth.BluetoothHapClient import android.bluetooth.BluetoothHapPresetInfo import android.content.Context import android.util.AttributeSet import android.widget.LinearLayout import androidx.annotation.VisibleForTesting import com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT import com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT import com.android.settingslib.bluetooth.hearingdevices.ui.ExpandableControlUi.Companion.SIDE_UNIFIED import com.android.settingslib.bluetooth.hearingdevices.ui.ExpandableControlUi.Companion.VALID_SIDES import com.android.settingslib.bluetooth.hearingdevices.ui.PresetSpinner import com.android.settingslib.bluetooth.hearingdevices.ui.PresetUi import com.android.systemui.accessibility.hearingaid.HearingDevicesUiEventLogger.Companion.LAUNCH_SOURCE_UNKNOWN import com.android.systemui.res.R import com.google.common.collect.BiMap import com.google.common.collect.HashBiMap /** * A custom view that manages a set of {@link PresetSpinner} controls for hearing aid devices. This * layout dynamically displays either a unified preset control or separate controls for left and * right sides, based on the device configuration. * *

It implements the {@link PresetUi} interface to allow for control by a presenter and handles * UI-related events such as preset changes. */ class PresetLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0, ) : LinearLayout(context, attrs, defStyleAttr, defStyleRes), PresetUi { private val sideToControlMap: BiMap = HashBiMap.create() private var uiListener: PresetUi.PresetUiListener? = null private var uiEventLogger: HearingDevicesUiEventLogger? = null private var launchSourceId: Int = LAUNCH_SOURCE_UNKNOWN private var expanded = false private val onChangeListener: PresetSpinner.OnChangeListener = PresetSpinner.OnChangeListener { spinner: PresetSpinner, value: Int -> val side = sideToControlMap.inverse()[spinner] side?.let { logMetrics(side) uiListener?.onPresetChangedFromUi(side, value) } } override fun setListener(listener: PresetUi.PresetUiListener?) { uiListener = listener } override fun setupControls(sides: Set) { sides.forEach { side -> createControl(side) } createControl(SIDE_UNIFIED) removeAllViews() for (side in VALID_SIDES) { sideToControlMap[side]?.let { addView(it, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) } } updateControlLayout() } override fun setControlEnabled(side: Int, enabled: Boolean) { sideToControlMap[side]?.isEnabled = enabled } override fun setControlList(side: Int, presetInfos: List) { sideToControlMap[side]?.apply { setList(presetInfos) if (presetInfos.isEmpty()) { isEnabled = false } } } override fun setControlValue(side: Int, presetIndex: Int) { sideToControlMap[side]?.apply { if (getValue() != presetIndex) { setValue(presetIndex) } } } override fun getControlValue(side: Int): Int { return sideToControlMap[side]?.getValue() ?: BluetoothHapClient.PRESET_INDEX_UNAVAILABLE } override fun setVisible(visible: Boolean) { visibility = if (visible) VISIBLE else GONE } override fun setControlExpanded(expanded: Boolean) { if (this.expanded != expanded) { this.expanded = expanded updateControlLayout() } } override fun isControlExpanded(): Boolean { return expanded } fun setUiEventLogger(uiEventLogger: HearingDevicesUiEventLogger, launchSourceId: Int) { this.uiEventLogger = uiEventLogger this.launchSourceId = launchSourceId } private fun createControl(side: Int) { if (sideToControlMap.containsKey(side)) { return } PresetSpinner(context).apply { val titleRes = when (side) { SIDE_LEFT -> R.string.hearing_devices_preset_label_left SIDE_RIGHT -> R.string.hearing_devices_preset_label_right else -> R.string.hearing_devices_preset_label } setTitle(context.getString(titleRes)) setOnChangeListener(onChangeListener) sideToControlMap[side] = this } } private fun updateControlLayout() { sideToControlMap.forEach { (side, control) -> control.visibility = when (side) { SIDE_UNIFIED -> if (expanded) GONE else VISIBLE else -> if (expanded) VISIBLE else GONE } } } private fun logMetrics(side: Int) { val uiEvent: HearingDevicesUiEvent = when (side) { SIDE_UNIFIED -> HearingDevicesUiEvent.HEARING_DEVICES_PRESET_SELECT else -> HearingDevicesUiEvent.HEARING_DEVICES_PRESET_SELECT_SEPARATED } uiEventLogger?.log(uiEvent, launchSourceId) } @VisibleForTesting fun getControls(): Map { return sideToControlMap } }