Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6ae38110 authored by jasonwshsu's avatar jasonwshsu
Browse files

Add the entry for hearing device input routing in QS

* It is the same input routing feature in Settings > Device details page, but add the entry in QS.

Bug: 397314200
Bug: 397345554
Bug: 392902067
Test: manually check on go/atomviewer
Test: atest HearingDevicesDialogDelegateTest HearingDevicesInputRoutingControllerTest
Flag: com.android.systemui.hearing_devices_input_routing_ui_improvement
Change-Id: I16965d016b1278eb3c96cbf9d3b166c90af5abd8
parent 8ddbaf6a
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -155,3 +155,13 @@ flag {
      purpose: PURPOSE_BUGFIX
    }
}

flag {
    name: "hearing_devices_input_routing_ui_improvement"
    namespace: "accessibility"
    description: "UI improvement for hearing device input routing feature"
    bug: "397314200"
    metadata {
        purpose: PURPOSE_BUGFIX
    }
}
+8 −2
Original line number Diff line number Diff line
@@ -141,6 +141,10 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
    @Mock
    private QSSettingsPackageRepository mQSSettingsPackageRepository;
    @Mock
    private HearingDevicesInputRoutingController.Factory mInputRoutingFactory;
    @Mock
    private HearingDevicesInputRoutingController mInputRoutingController;
    @Mock
    private CachedBluetoothDevice mCachedDevice;
    @Mock
    private BluetoothDevice mDevice;
@@ -184,6 +188,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
        when(mCachedDevice.getBondState()).thenReturn(BOND_BONDED);
        when(mCachedDevice.getDeviceSide()).thenReturn(SIDE_LEFT);
        when(mHearingDeviceItem.getCachedBluetoothDevice()).thenReturn(mCachedDevice);
        when(mInputRoutingFactory.create(any())).thenReturn(mInputRoutingController);

        mContext.setMockPackageManager(mPackageManager);
    }
@@ -349,6 +354,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {

        setUpDeviceDialogWithoutPairNewDeviceButton();
        mDialog.show();
        mExecutor.runAllReady();

        ViewGroup ambientLayout = getAmbientLayout(mDialog);
        assertThat(ambientLayout.getVisibility()).isEqualTo(View.VISIBLE);
@@ -401,7 +407,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
                mExecutor,
                mAudioManager,
                mUiEventLogger,
                mQSSettingsPackageRepository
                mQSSettingsPackageRepository,
                mInputRoutingFactory
        );
        mDialog = mDialogDelegate.createDialog();
    }
@@ -438,7 +445,6 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
        return dialog.requireViewById(R.id.ambient_layout);
    }


    private int countChildWithoutSpace(ViewGroup viewGroup) {
        int spaceCount = 0;
        for (int i = 0; i < viewGroup.getChildCount(); i++) {
+201 −0
Original line number Diff line number Diff line
/*
 * 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.media.AudioDeviceInfo
import android.media.AudioManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.HapClientProfile
import com.android.systemui.SysuiTestCase
import com.android.systemui.accessibility.hearingaid.HearingDevicesInputRoutingController.InputRoutingControlAvailableCallback
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
import org.mockito.kotlin.verify

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class HearingDevicesInputRoutingControllerTest : SysuiTestCase() {

    private val kosmos = Kosmos()
    private val testScope = kosmos.testScope
    private var hapClientProfile: HapClientProfile = mock()
    private var cachedDevice: CachedBluetoothDevice = mock()
    private var memberCachedDevice: CachedBluetoothDevice = mock()
    private var btDevice: android.bluetooth.BluetoothDevice = mock()
    private var audioManager: AudioManager = mock()
    private lateinit var underTest: HearingDevicesInputRoutingController
    private val testDispatcher = kosmos.testDispatcher

    @Before
    fun setUp() {
        hapClientProfile.stub { on { isProfileReady } doReturn true }
        cachedDevice.stub {
            on { device } doReturn btDevice
            on { profiles } doReturn listOf(hapClientProfile)
        }
        memberCachedDevice.stub {
            on { device } doReturn btDevice
            on { profiles } doReturn listOf(hapClientProfile)
        }

        underTest = HearingDevicesInputRoutingController(mContext, audioManager, testDispatcher)
        underTest.setDevice(cachedDevice)
    }

    @Test
    fun isInputRoutingControlAvailable_validInput_supportHapProfile_returnTrue() {
        testScope.runTest {
            val mockInfoAddress = arrayOf(mockTestAddressInfo(TEST_ADDRESS))
            cachedDevice.stub {
                on { address } doReturn TEST_ADDRESS
                on { profiles } doReturn listOf(hapClientProfile)
            }
            audioManager.stub {
                on { getDevices(AudioManager.GET_DEVICES_INPUTS) } doReturn mockInfoAddress
            }

            var result: Boolean? = null
            underTest.isInputRoutingControlAvailable(
                object : InputRoutingControlAvailableCallback {
                    override fun onResult(available: Boolean) {
                        result = available
                    }
                }
            )

            runCurrent()
            assertThat(result).isTrue()
        }
    }

    @Test
    fun isInputRoutingControlAvailable_notSupportHapProfile_returnFalse() {
        testScope.runTest {
            val mockInfoAddress = arrayOf(mockTestAddressInfo(TEST_ADDRESS))
            cachedDevice.stub {
                on { address } doReturn TEST_ADDRESS
                on { profiles } doReturn emptyList()
            }
            audioManager.stub {
                on { getDevices(AudioManager.GET_DEVICES_INPUTS) } doReturn mockInfoAddress
            }

            var result: Boolean? = null
            underTest.isInputRoutingControlAvailable(
                object : InputRoutingControlAvailableCallback {
                    override fun onResult(available: Boolean) {
                        result = available
                    }
                }
            )

            runCurrent()
            assertThat(result).isFalse()
        }
    }

    @Test
    fun isInputRoutingControlAvailable_validInputMember_supportHapProfile_returnTrue() {
        testScope.runTest {
            val mockInfoAddress2 = arrayOf(mockTestAddressInfo(TEST_ADDRESS_2))
            cachedDevice.stub {
                on { address } doReturn TEST_ADDRESS
                on { profiles } doReturn listOf(hapClientProfile)
                on { memberDevice } doReturn (setOf(memberCachedDevice))
            }
            memberCachedDevice.stub { on { address } doReturn TEST_ADDRESS_2 }
            audioManager.stub {
                on { getDevices(AudioManager.GET_DEVICES_INPUTS) } doReturn mockInfoAddress2
            }

            var result: Boolean? = null
            underTest.isInputRoutingControlAvailable(
                object : InputRoutingControlAvailableCallback {
                    override fun onResult(available: Boolean) {
                        result = available
                    }
                }
            )

            runCurrent()
            assertThat(result).isTrue()
        }
    }

    @Test
    fun isAvailable_notValidInputDevice_returnFalse() {
        testScope.runTest {
            cachedDevice.stub {
                on { address } doReturn TEST_ADDRESS
                on { profiles } doReturn listOf(hapClientProfile)
            }
            audioManager.stub {
                on { getDevices(AudioManager.GET_DEVICES_INPUTS) } doReturn emptyArray()
            }

            var result: Boolean? = null
            underTest.isInputRoutingControlAvailable(
                object : InputRoutingControlAvailableCallback {
                    override fun onResult(available: Boolean) {
                        result = available
                    }
                }
            )

            runCurrent()
            assertThat(result).isFalse()
        }
    }

    @Test
    fun selectInputRouting_builtinMic_setMicrophonePreferredForCallsFalse() {
        underTest.selectInputRouting(
            HearingDevicesInputRoutingController.InputRoutingValue.BUILTIN_MIC.ordinal
        )

        verify(btDevice).isMicrophonePreferredForCalls = false
    }

    private fun mockTestAddressInfo(address: String): AudioDeviceInfo {
        val info: AudioDeviceInfo = mock()
        info.stub {
            on { type } doReturn AudioDeviceInfo.TYPE_BLE_HEADSET
            on { this.address } doReturn address
        }

        return info
    }

    companion object {
        private const val TEST_ADDRESS = "55:66:77:88:99:AA"
        private const val TEST_ADDRESS_2 = "55:66:77:88:99:BB"
    }
}
+37 −1
Original line number Diff line number Diff line
@@ -85,13 +85,49 @@
            android:longClickable="false"/>
    </LinearLayout>

    <LinearLayout
        android:id="@+id/input_routing_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@id/preset_layout"
        android:layout_marginTop="@dimen/hearing_devices_layout_margin"
        android:orientation="vertical"
        android:visibility="gone">
        <TextView
            android:id="@+id/input_routing_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="@dimen/bluetooth_dialog_layout_margin"
            android:layout_marginEnd="@dimen/bluetooth_dialog_layout_margin"
            android:paddingStart="@dimen/hearing_devices_small_title_padding_horizontal"
            android:text="@string/hearing_devices_input_routing_label"
            android:textAppearance="@style/TextAppearance.Dialog.Title"
            android:textSize="14sp"
            android:gravity="center_vertical"
            android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
            android:textDirection="locale"/>
        <Spinner
            android:id="@+id/input_routing_spinner"
            style="@style/BluetoothTileDialog.Device"
            android:layout_height="@dimen/bluetooth_dialog_device_height"
            android:layout_marginTop="4dp"
            android:paddingStart="0dp"
            android:paddingEnd="0dp"
            android:background="@drawable/hearing_devices_spinner_background"
            android:popupBackground="@drawable/hearing_devices_spinner_popup_background"
            android:dropDownWidth="match_parent"
            android:longClickable="false"/>
    </LinearLayout>

    <com.android.systemui.accessibility.hearingaid.AmbientVolumeLayout
        android:id="@+id/ambient_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@id/preset_layout"
        app:layout_constraintTop_toBottomOf="@id/input_routing_layout"
        android:layout_marginTop="@dimen/hearing_devices_layout_margin" />

    <LinearLayout
+7 −0
Original line number Diff line number Diff line
@@ -1016,6 +1016,13 @@
    <string name="hearing_devices_presets_error">Couldn\'t update preset</string>
    <!-- QuickSettings: Title for hearing aids presets. Preset is a set of hearing aid settings. User can apply different settings in different environments (e.g. Outdoor, Restaurant, Home) [CHAR LIMIT=40]-->
    <string name="hearing_devices_preset_label">Preset</string>
    <!-- QuickSettings: Title for hearing aids input routing control in hearing device dialog. [CHAR LIMIT=40]-->
    <string name="hearing_devices_input_routing_label">Default microphone for calls</string>
    <!-- QuickSettings: Option for hearing aids input routing control in hearing device dialog. It will alter input routing for calls for hearing aid. [CHAR LIMIT=40]-->
    <string-array name="hearing_device_input_routing_options">
        <item>Hearing aid microphone</item>
        <item>This phone\'s microphone</item>
    </string-array>
    <!-- QuickSettings: Content description for the icon that indicates the item is selected [CHAR LIMIT=NONE]-->
    <string name="hearing_devices_spinner_item_selected">Selected</string>
    <!-- QuickSettings: Title for ambient controls. [CHAR LIMIT=40]-->
Loading