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

Commit 9e89253e authored by Jason Hsu's avatar Jason Hsu Committed by Android (Google) Code Review
Browse files

Merge "Add the entry for hearing device input routing in QS" into main

parents 1ee63562 6ae38110
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