Loading packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/HearingDevicesTileMapperTest.kt 0 → 100644 +118 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.qs.tiles.impl.hearingdevices.domain import android.graphics.drawable.TestStubDrawable import android.widget.Switch import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.kosmos.Kosmos import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject import com.android.systemui.qs.tiles.impl.hearingdevices.domain.model.HearingDevicesTileModel import com.android.systemui.qs.tiles.impl.hearingdevices.qsHearingDevicesTileConfig import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.res.R import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class HearingDevicesTileMapperTest : SysuiTestCase() { private val kosmos = Kosmos() private val qsTileConfig = kosmos.qsHearingDevicesTileConfig private val mapper by lazy { HearingDevicesTileMapper( context.orCreateTestableResources .apply { addOverride(R.drawable.qs_hearing_devices_icon, TestStubDrawable()) } .resources, context.theme, ) } @Test fun map_anyActiveHearingDevice_anyPairedHearingDevice_activeState() { val tileState: QSTileState = mapper.map( qsTileConfig, HearingDevicesTileModel( isAnyActiveHearingDevice = true, isAnyPairedHearingDevice = true, ), ) val expectedState = createHearingDevicesTileState( QSTileState.ActivationState.ACTIVE, context.getString(R.string.quick_settings_hearing_devices_connected), ) QSTileStateSubject.assertThat(tileState).isEqualTo(expectedState) } @Test fun map_noActiveHearingDevice_anyPairedHearingDevice_inactiveState() { val tileState: QSTileState = mapper.map( qsTileConfig, HearingDevicesTileModel( isAnyActiveHearingDevice = false, isAnyPairedHearingDevice = true, ), ) val expectedState = createHearingDevicesTileState( QSTileState.ActivationState.INACTIVE, context.getString(R.string.quick_settings_hearing_devices_disconnected), ) QSTileStateSubject.assertThat(tileState).isEqualTo(expectedState) } @Test fun map_noActiveHearingDevice_noPairedHearingDevice_inactiveState() { val tileState: QSTileState = mapper.map( qsTileConfig, HearingDevicesTileModel( isAnyActiveHearingDevice = false, isAnyPairedHearingDevice = false, ), ) val expectedState = createHearingDevicesTileState(QSTileState.ActivationState.INACTIVE, secondaryLabel = "") QSTileStateSubject.assertThat(tileState).isEqualTo(expectedState) } private fun createHearingDevicesTileState( activationState: QSTileState.ActivationState, secondaryLabel: String, ): QSTileState { val label = context.getString(R.string.quick_settings_hearing_devices_label) val iconRes = R.drawable.qs_hearing_devices_icon return QSTileState( { Icon.Loaded(context.getDrawable(iconRes)!!, null) }, iconRes, label, activationState, secondaryLabel, setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK), label, null, QSTileState.SideViewIcon.Chevron, QSTileState.EnabledState.ENABLED, Switch::class.qualifiedName, ) } } packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileDataInteractorTest.kt 0 → 100644 +158 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.qs.tiles.impl.hearingdevices.domain.interactor import android.os.UserHandle import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.accessibility.hearingaid.HearingDevicesChecker import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger import com.android.systemui.qs.tiles.impl.hearingdevices.domain.model.HearingDevicesTileModel import com.android.systemui.statusbar.policy.fakeBluetoothController import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class HearingDevicesTileDataInteractorTest : SysuiTestCase() { private val kosmos = Kosmos() private val testScope = kosmos.testScope private val testUser = UserHandle.of(1) private val controller = kosmos.fakeBluetoothController private lateinit var underTest: HearingDevicesTileDataInteractor @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule() @Mock private lateinit var checker: HearingDevicesChecker @Before fun setup() { underTest = HearingDevicesTileDataInteractor(testScope.testScheduler, controller, checker) } @EnableFlags(Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG) @Test fun availability_flagEnabled_returnTrue() = testScope.runTest { val availability by collectLastValue(underTest.availability(testUser)) assertThat(availability).isTrue() } @DisableFlags(Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG) @Test fun availability_flagDisabled_returnFalse() = testScope.runTest { val availability by collectLastValue(underTest.availability(testUser)) assertThat(availability).isFalse() } @Test fun tileData_bluetoothStateChanged_dataMatchesChecker() = testScope.runTest { val flowValues: List<HearingDevicesTileModel> by collectValues( underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest)) ) runCurrent() assertThat(flowValues.size).isEqualTo(1) // from addCallback in setup() whenever(checker.isAnyPairedHearingDevice).thenReturn(false) whenever(checker.isAnyActiveHearingDevice).thenReturn(false) controller.isBluetoothEnabled = false runCurrent() assertThat(flowValues.size).isEqualTo(1) // model unchanged, no new flow value whenever(checker.isAnyPairedHearingDevice).thenReturn(true) whenever(checker.isAnyActiveHearingDevice).thenReturn(false) controller.isBluetoothEnabled = true runCurrent() assertThat(flowValues.size).isEqualTo(2) whenever(checker.isAnyPairedHearingDevice).thenReturn(true) whenever(checker.isAnyActiveHearingDevice).thenReturn(true) controller.isBluetoothEnabled = true runCurrent() assertThat(flowValues.size).isEqualTo(3) assertThat(flowValues.map { it.isAnyPairedHearingDevice }) .containsExactly(false, true, true) .inOrder() assertThat(flowValues.map { it.isAnyActiveHearingDevice }) .containsExactly(false, false, true) .inOrder() } @Test fun tileData_bluetoothDeviceChanged_dataMatchesChecker() = testScope.runTest { val flowValues: List<HearingDevicesTileModel> by collectValues( underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest)) ) runCurrent() assertThat(flowValues.size).isEqualTo(1) // from addCallback in setup() whenever(checker.isAnyPairedHearingDevice).thenReturn(false) whenever(checker.isAnyActiveHearingDevice).thenReturn(false) controller.onBluetoothDevicesChanged() runCurrent() assertThat(flowValues.size).isEqualTo(1) // model unchanged, no new flow value whenever(checker.isAnyPairedHearingDevice).thenReturn(true) whenever(checker.isAnyActiveHearingDevice).thenReturn(false) controller.onBluetoothDevicesChanged() runCurrent() assertThat(flowValues.size).isEqualTo(2) whenever(checker.isAnyPairedHearingDevice).thenReturn(true) whenever(checker.isAnyActiveHearingDevice).thenReturn(true) controller.onBluetoothDevicesChanged() runCurrent() assertThat(flowValues.size).isEqualTo(3) assertThat(flowValues.map { it.isAnyPairedHearingDevice }) .containsExactly(false, true, true) .inOrder() assertThat(flowValues.map { it.isAnyActiveHearingDevice }) .containsExactly(false, false, true) .inOrder() } } packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractorTest.kt 0 → 100644 +96 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.qs.tiles.impl.hearingdevices.domain.interactor import android.platform.test.annotations.EnabledOnRavenwood import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.accessibility.hearingaid.HearingDevicesDialogManager import com.android.systemui.accessibility.hearingaid.HearingDevicesUiEventLogger.Companion.LAUNCH_SOURCE_QS_TILE import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx import com.android.systemui.qs.tiles.impl.hearingdevices.domain.model.HearingDevicesTileModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.verify @SmallTest @EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class HearingDevicesTileUserActionInteractorTest : SysuiTestCase() { private val kosmos = Kosmos() private val testScope = kosmos.testScope private val inputHandler = FakeQSTileIntentUserInputHandler() private lateinit var underTest: HearingDevicesTileUserActionInteractor @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule() @Mock private lateinit var dialogManager: HearingDevicesDialogManager @Before fun setUp() { underTest = HearingDevicesTileUserActionInteractor( testScope.coroutineContext, inputHandler, dialogManager, ) } @Test fun handleClick_launchDialog() = testScope.runTest { val input = HearingDevicesTileModel( isAnyActiveHearingDevice = true, isAnyPairedHearingDevice = true, ) underTest.handleInput(QSTileInputTestKtx.click(input)) verify(dialogManager).showDialog(anyOrNull(), eq(LAUNCH_SOURCE_QS_TILE)) } @Test fun handleLongClick_launchSettings() = testScope.runTest { val input = HearingDevicesTileModel( isAnyActiveHearingDevice = true, isAnyPairedHearingDevice = true, ) underTest.handleInput(QSTileInputTestKtx.longClick(input)) QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { assertThat(it.intent.action).isEqualTo(Settings.ACTION_HEARING_DEVICES_SETTINGS) } } } packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt +55 −21 Original line number Diff line number Diff line Loading @@ -39,6 +39,10 @@ import com.android.systemui.qs.tiles.impl.fontscaling.domain.FontScalingTileMapp import com.android.systemui.qs.tiles.impl.fontscaling.domain.interactor.FontScalingTileDataInteractor import com.android.systemui.qs.tiles.impl.fontscaling.domain.interactor.FontScalingTileUserActionInteractor import com.android.systemui.qs.tiles.impl.fontscaling.domain.model.FontScalingTileModel import com.android.systemui.qs.tiles.impl.hearingdevices.domain.HearingDevicesTileMapper import com.android.systemui.qs.tiles.impl.hearingdevices.domain.interactor.HearingDevicesTileDataInteractor import com.android.systemui.qs.tiles.impl.hearingdevices.domain.interactor.HearingDevicesTileUserActionInteractor import com.android.systemui.qs.tiles.impl.hearingdevices.domain.model.HearingDevicesTileModel import com.android.systemui.qs.tiles.impl.inversion.domain.ColorInversionTileMapper import com.android.systemui.qs.tiles.impl.inversion.domain.interactor.ColorInversionTileDataInteractor import com.android.systemui.qs.tiles.impl.inversion.domain.interactor.ColorInversionUserActionInteractor Loading Loading @@ -159,6 +163,13 @@ interface QSAccessibilityModule { impl: NightDisplayTileDataInteractor ): QSTileAvailabilityInteractor @Binds @IntoMap @StringKey(HEARING_DEVICES_TILE_SPEC) fun provideHearingDevicesAvailabilityInteractor( impl: HearingDevicesTileDataInteractor ): QSTileAvailabilityInteractor companion object { const val COLOR_CORRECTION_TILE_SPEC = "color_correction" const val COLOR_INVERSION_TILE_SPEC = "inversion" Loading Loading @@ -191,7 +202,7 @@ interface QSAccessibilityModule { factory: QSTileViewModelFactory.Static<ColorCorrectionTileModel>, mapper: ColorCorrectionTileMapper, stateInteractor: ColorCorrectionTileDataInteractor, userActionInteractor: ColorCorrectionUserActionInteractor userActionInteractor: ColorCorrectionUserActionInteractor, ): QSTileViewModel = factory.create( TileSpec.create(COLOR_CORRECTION_TILE_SPEC), Loading Loading @@ -223,7 +234,7 @@ interface QSAccessibilityModule { factory: QSTileViewModelFactory.Static<ColorInversionTileModel>, mapper: ColorInversionTileMapper, stateInteractor: ColorInversionTileDataInteractor, userActionInteractor: ColorInversionUserActionInteractor userActionInteractor: ColorInversionUserActionInteractor, ): QSTileViewModel = factory.create( TileSpec.create(COLOR_INVERSION_TILE_SPEC), Loading Loading @@ -255,7 +266,7 @@ interface QSAccessibilityModule { factory: QSTileViewModelFactory.Static<FontScalingTileModel>, mapper: FontScalingTileMapper, stateInteractor: FontScalingTileDataInteractor, userActionInteractor: FontScalingTileUserActionInteractor userActionInteractor: FontScalingTileUserActionInteractor, ): QSTileViewModel = factory.create( TileSpec.create(FONT_SCALING_TILE_SPEC), Loading @@ -279,21 +290,6 @@ interface QSAccessibilityModule { category = TileCategory.DISPLAY, ) @Provides @IntoMap @StringKey(HEARING_DEVICES_TILE_SPEC) fun provideHearingDevicesTileConfig(uiEventLogger: QsEventLogger): QSTileConfig = QSTileConfig( tileSpec = TileSpec.create(HEARING_DEVICES_TILE_SPEC), uiConfig = QSTileUIConfig.Resource( iconRes = R.drawable.qs_hearing_devices_icon, labelRes = R.string.quick_settings_hearing_devices_label, ), instanceId = uiEventLogger.getNewInstanceId(), category = TileCategory.ACCESSIBILITY, ) /** * Inject Reduce Bright Colors Tile into tileViewModelMap in QSModule. The tile is hidden * behind a flag. Loading @@ -305,7 +301,7 @@ interface QSAccessibilityModule { factory: QSTileViewModelFactory.Static<ReduceBrightColorsTileModel>, mapper: ReduceBrightColorsTileMapper, stateInteractor: ReduceBrightColorsTileDataInteractor, userActionInteractor: ReduceBrightColorsTileUserActionInteractor userActionInteractor: ReduceBrightColorsTileUserActionInteractor, ): QSTileViewModel = if (Flags.qsNewTilesFuture()) factory.create( Loading Loading @@ -339,7 +335,7 @@ interface QSAccessibilityModule { factory: QSTileViewModelFactory.Static<OneHandedModeTileModel>, mapper: OneHandedModeTileMapper, stateInteractor: OneHandedModeTileDataInteractor, userActionInteractor: OneHandedModeTileUserActionInteractor userActionInteractor: OneHandedModeTileUserActionInteractor, ): QSTileViewModel = if (Flags.qsNewTilesFuture()) factory.create( Loading Loading @@ -376,7 +372,7 @@ interface QSAccessibilityModule { factory: QSTileViewModelFactory.Static<NightDisplayTileModel>, mapper: NightDisplayTileMapper, stateInteractor: NightDisplayTileDataInteractor, userActionInteractor: NightDisplayTileUserActionInteractor userActionInteractor: NightDisplayTileUserActionInteractor, ): QSTileViewModel = if (Flags.qsNewTilesFuture()) factory.create( Loading @@ -386,5 +382,43 @@ interface QSAccessibilityModule { mapper, ) else StubQSTileViewModel @Provides @IntoMap @StringKey(HEARING_DEVICES_TILE_SPEC) fun provideHearingDevicesTileConfig(uiEventLogger: QsEventLogger): QSTileConfig = QSTileConfig( tileSpec = TileSpec.create(HEARING_DEVICES_TILE_SPEC), uiConfig = QSTileUIConfig.Resource( iconRes = R.drawable.qs_hearing_devices_icon, labelRes = R.string.quick_settings_hearing_devices_label, ), instanceId = uiEventLogger.getNewInstanceId(), category = TileCategory.ACCESSIBILITY, ) /** * Inject HearingDevices Tile into tileViewModelMap in QSModule. The tile is hidden behind a * flag. */ @Provides @IntoMap @StringKey(HEARING_DEVICES_TILE_SPEC) fun provideHearingDevicesTileViewModel( factory: QSTileViewModelFactory.Static<HearingDevicesTileModel>, mapper: HearingDevicesTileMapper, stateInteractor: HearingDevicesTileDataInteractor, userActionInteractor: HearingDevicesTileUserActionInteractor, ): QSTileViewModel { return if (Flags.hearingAidsQsTileDialog() && Flags.qsNewTilesFuture()) { factory.create( TileSpec.create(HEARING_DEVICES_TILE_SPEC), userActionInteractor, stateInteractor, mapper, ) } else StubQSTileViewModel } } } packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/HearingDevicesTileMapper.kt 0 → 100644 +59 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.qs.tiles.impl.hearingdevices.domain import android.content.res.Resources import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper import com.android.systemui.qs.tiles.impl.hearingdevices.domain.model.HearingDevicesTileModel import com.android.systemui.qs.tiles.viewmodel.QSTileConfig import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.res.R import javax.inject.Inject /** Maps [HearingDevicesTileModel] to [QSTileState]. */ class HearingDevicesTileMapper @Inject constructor(@Main private val resources: Resources, private val theme: Resources.Theme) : QSTileDataToStateMapper<HearingDevicesTileModel> { override fun map(config: QSTileConfig, data: HearingDevicesTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { label = resources.getString(R.string.quick_settings_hearing_devices_label) iconRes = R.drawable.qs_hearing_devices_icon val loadedIcon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), contentDescription = null) icon = { loadedIcon } sideViewIcon = QSTileState.SideViewIcon.Chevron contentDescription = label if (data.isAnyActiveHearingDevice) { activationState = QSTileState.ActivationState.ACTIVE secondaryLabel = resources.getString(R.string.quick_settings_hearing_devices_connected) } else if (data.isAnyPairedHearingDevice) { activationState = QSTileState.ActivationState.INACTIVE secondaryLabel = resources.getString(R.string.quick_settings_hearing_devices_disconnected) } else { activationState = QSTileState.ActivationState.INACTIVE secondaryLabel = "" } supportedActions = setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) } } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/HearingDevicesTileMapperTest.kt 0 → 100644 +118 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.qs.tiles.impl.hearingdevices.domain import android.graphics.drawable.TestStubDrawable import android.widget.Switch import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.kosmos.Kosmos import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject import com.android.systemui.qs.tiles.impl.hearingdevices.domain.model.HearingDevicesTileModel import com.android.systemui.qs.tiles.impl.hearingdevices.qsHearingDevicesTileConfig import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.res.R import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class HearingDevicesTileMapperTest : SysuiTestCase() { private val kosmos = Kosmos() private val qsTileConfig = kosmos.qsHearingDevicesTileConfig private val mapper by lazy { HearingDevicesTileMapper( context.orCreateTestableResources .apply { addOverride(R.drawable.qs_hearing_devices_icon, TestStubDrawable()) } .resources, context.theme, ) } @Test fun map_anyActiveHearingDevice_anyPairedHearingDevice_activeState() { val tileState: QSTileState = mapper.map( qsTileConfig, HearingDevicesTileModel( isAnyActiveHearingDevice = true, isAnyPairedHearingDevice = true, ), ) val expectedState = createHearingDevicesTileState( QSTileState.ActivationState.ACTIVE, context.getString(R.string.quick_settings_hearing_devices_connected), ) QSTileStateSubject.assertThat(tileState).isEqualTo(expectedState) } @Test fun map_noActiveHearingDevice_anyPairedHearingDevice_inactiveState() { val tileState: QSTileState = mapper.map( qsTileConfig, HearingDevicesTileModel( isAnyActiveHearingDevice = false, isAnyPairedHearingDevice = true, ), ) val expectedState = createHearingDevicesTileState( QSTileState.ActivationState.INACTIVE, context.getString(R.string.quick_settings_hearing_devices_disconnected), ) QSTileStateSubject.assertThat(tileState).isEqualTo(expectedState) } @Test fun map_noActiveHearingDevice_noPairedHearingDevice_inactiveState() { val tileState: QSTileState = mapper.map( qsTileConfig, HearingDevicesTileModel( isAnyActiveHearingDevice = false, isAnyPairedHearingDevice = false, ), ) val expectedState = createHearingDevicesTileState(QSTileState.ActivationState.INACTIVE, secondaryLabel = "") QSTileStateSubject.assertThat(tileState).isEqualTo(expectedState) } private fun createHearingDevicesTileState( activationState: QSTileState.ActivationState, secondaryLabel: String, ): QSTileState { val label = context.getString(R.string.quick_settings_hearing_devices_label) val iconRes = R.drawable.qs_hearing_devices_icon return QSTileState( { Icon.Loaded(context.getDrawable(iconRes)!!, null) }, iconRes, label, activationState, secondaryLabel, setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK), label, null, QSTileState.SideViewIcon.Chevron, QSTileState.EnabledState.ENABLED, Switch::class.qualifiedName, ) } }
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileDataInteractorTest.kt 0 → 100644 +158 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.qs.tiles.impl.hearingdevices.domain.interactor import android.os.UserHandle import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.accessibility.hearingaid.HearingDevicesChecker import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger import com.android.systemui.qs.tiles.impl.hearingdevices.domain.model.HearingDevicesTileModel import com.android.systemui.statusbar.policy.fakeBluetoothController import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class HearingDevicesTileDataInteractorTest : SysuiTestCase() { private val kosmos = Kosmos() private val testScope = kosmos.testScope private val testUser = UserHandle.of(1) private val controller = kosmos.fakeBluetoothController private lateinit var underTest: HearingDevicesTileDataInteractor @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule() @Mock private lateinit var checker: HearingDevicesChecker @Before fun setup() { underTest = HearingDevicesTileDataInteractor(testScope.testScheduler, controller, checker) } @EnableFlags(Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG) @Test fun availability_flagEnabled_returnTrue() = testScope.runTest { val availability by collectLastValue(underTest.availability(testUser)) assertThat(availability).isTrue() } @DisableFlags(Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG) @Test fun availability_flagDisabled_returnFalse() = testScope.runTest { val availability by collectLastValue(underTest.availability(testUser)) assertThat(availability).isFalse() } @Test fun tileData_bluetoothStateChanged_dataMatchesChecker() = testScope.runTest { val flowValues: List<HearingDevicesTileModel> by collectValues( underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest)) ) runCurrent() assertThat(flowValues.size).isEqualTo(1) // from addCallback in setup() whenever(checker.isAnyPairedHearingDevice).thenReturn(false) whenever(checker.isAnyActiveHearingDevice).thenReturn(false) controller.isBluetoothEnabled = false runCurrent() assertThat(flowValues.size).isEqualTo(1) // model unchanged, no new flow value whenever(checker.isAnyPairedHearingDevice).thenReturn(true) whenever(checker.isAnyActiveHearingDevice).thenReturn(false) controller.isBluetoothEnabled = true runCurrent() assertThat(flowValues.size).isEqualTo(2) whenever(checker.isAnyPairedHearingDevice).thenReturn(true) whenever(checker.isAnyActiveHearingDevice).thenReturn(true) controller.isBluetoothEnabled = true runCurrent() assertThat(flowValues.size).isEqualTo(3) assertThat(flowValues.map { it.isAnyPairedHearingDevice }) .containsExactly(false, true, true) .inOrder() assertThat(flowValues.map { it.isAnyActiveHearingDevice }) .containsExactly(false, false, true) .inOrder() } @Test fun tileData_bluetoothDeviceChanged_dataMatchesChecker() = testScope.runTest { val flowValues: List<HearingDevicesTileModel> by collectValues( underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest)) ) runCurrent() assertThat(flowValues.size).isEqualTo(1) // from addCallback in setup() whenever(checker.isAnyPairedHearingDevice).thenReturn(false) whenever(checker.isAnyActiveHearingDevice).thenReturn(false) controller.onBluetoothDevicesChanged() runCurrent() assertThat(flowValues.size).isEqualTo(1) // model unchanged, no new flow value whenever(checker.isAnyPairedHearingDevice).thenReturn(true) whenever(checker.isAnyActiveHearingDevice).thenReturn(false) controller.onBluetoothDevicesChanged() runCurrent() assertThat(flowValues.size).isEqualTo(2) whenever(checker.isAnyPairedHearingDevice).thenReturn(true) whenever(checker.isAnyActiveHearingDevice).thenReturn(true) controller.onBluetoothDevicesChanged() runCurrent() assertThat(flowValues.size).isEqualTo(3) assertThat(flowValues.map { it.isAnyPairedHearingDevice }) .containsExactly(false, true, true) .inOrder() assertThat(flowValues.map { it.isAnyActiveHearingDevice }) .containsExactly(false, false, true) .inOrder() } }
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractorTest.kt 0 → 100644 +96 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.qs.tiles.impl.hearingdevices.domain.interactor import android.platform.test.annotations.EnabledOnRavenwood import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.accessibility.hearingaid.HearingDevicesDialogManager import com.android.systemui.accessibility.hearingaid.HearingDevicesUiEventLogger.Companion.LAUNCH_SOURCE_QS_TILE import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx import com.android.systemui.qs.tiles.impl.hearingdevices.domain.model.HearingDevicesTileModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.verify @SmallTest @EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class HearingDevicesTileUserActionInteractorTest : SysuiTestCase() { private val kosmos = Kosmos() private val testScope = kosmos.testScope private val inputHandler = FakeQSTileIntentUserInputHandler() private lateinit var underTest: HearingDevicesTileUserActionInteractor @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule() @Mock private lateinit var dialogManager: HearingDevicesDialogManager @Before fun setUp() { underTest = HearingDevicesTileUserActionInteractor( testScope.coroutineContext, inputHandler, dialogManager, ) } @Test fun handleClick_launchDialog() = testScope.runTest { val input = HearingDevicesTileModel( isAnyActiveHearingDevice = true, isAnyPairedHearingDevice = true, ) underTest.handleInput(QSTileInputTestKtx.click(input)) verify(dialogManager).showDialog(anyOrNull(), eq(LAUNCH_SOURCE_QS_TILE)) } @Test fun handleLongClick_launchSettings() = testScope.runTest { val input = HearingDevicesTileModel( isAnyActiveHearingDevice = true, isAnyPairedHearingDevice = true, ) underTest.handleInput(QSTileInputTestKtx.longClick(input)) QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { assertThat(it.intent.action).isEqualTo(Settings.ACTION_HEARING_DEVICES_SETTINGS) } } }
packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt +55 −21 Original line number Diff line number Diff line Loading @@ -39,6 +39,10 @@ import com.android.systemui.qs.tiles.impl.fontscaling.domain.FontScalingTileMapp import com.android.systemui.qs.tiles.impl.fontscaling.domain.interactor.FontScalingTileDataInteractor import com.android.systemui.qs.tiles.impl.fontscaling.domain.interactor.FontScalingTileUserActionInteractor import com.android.systemui.qs.tiles.impl.fontscaling.domain.model.FontScalingTileModel import com.android.systemui.qs.tiles.impl.hearingdevices.domain.HearingDevicesTileMapper import com.android.systemui.qs.tiles.impl.hearingdevices.domain.interactor.HearingDevicesTileDataInteractor import com.android.systemui.qs.tiles.impl.hearingdevices.domain.interactor.HearingDevicesTileUserActionInteractor import com.android.systemui.qs.tiles.impl.hearingdevices.domain.model.HearingDevicesTileModel import com.android.systemui.qs.tiles.impl.inversion.domain.ColorInversionTileMapper import com.android.systemui.qs.tiles.impl.inversion.domain.interactor.ColorInversionTileDataInteractor import com.android.systemui.qs.tiles.impl.inversion.domain.interactor.ColorInversionUserActionInteractor Loading Loading @@ -159,6 +163,13 @@ interface QSAccessibilityModule { impl: NightDisplayTileDataInteractor ): QSTileAvailabilityInteractor @Binds @IntoMap @StringKey(HEARING_DEVICES_TILE_SPEC) fun provideHearingDevicesAvailabilityInteractor( impl: HearingDevicesTileDataInteractor ): QSTileAvailabilityInteractor companion object { const val COLOR_CORRECTION_TILE_SPEC = "color_correction" const val COLOR_INVERSION_TILE_SPEC = "inversion" Loading Loading @@ -191,7 +202,7 @@ interface QSAccessibilityModule { factory: QSTileViewModelFactory.Static<ColorCorrectionTileModel>, mapper: ColorCorrectionTileMapper, stateInteractor: ColorCorrectionTileDataInteractor, userActionInteractor: ColorCorrectionUserActionInteractor userActionInteractor: ColorCorrectionUserActionInteractor, ): QSTileViewModel = factory.create( TileSpec.create(COLOR_CORRECTION_TILE_SPEC), Loading Loading @@ -223,7 +234,7 @@ interface QSAccessibilityModule { factory: QSTileViewModelFactory.Static<ColorInversionTileModel>, mapper: ColorInversionTileMapper, stateInteractor: ColorInversionTileDataInteractor, userActionInteractor: ColorInversionUserActionInteractor userActionInteractor: ColorInversionUserActionInteractor, ): QSTileViewModel = factory.create( TileSpec.create(COLOR_INVERSION_TILE_SPEC), Loading Loading @@ -255,7 +266,7 @@ interface QSAccessibilityModule { factory: QSTileViewModelFactory.Static<FontScalingTileModel>, mapper: FontScalingTileMapper, stateInteractor: FontScalingTileDataInteractor, userActionInteractor: FontScalingTileUserActionInteractor userActionInteractor: FontScalingTileUserActionInteractor, ): QSTileViewModel = factory.create( TileSpec.create(FONT_SCALING_TILE_SPEC), Loading @@ -279,21 +290,6 @@ interface QSAccessibilityModule { category = TileCategory.DISPLAY, ) @Provides @IntoMap @StringKey(HEARING_DEVICES_TILE_SPEC) fun provideHearingDevicesTileConfig(uiEventLogger: QsEventLogger): QSTileConfig = QSTileConfig( tileSpec = TileSpec.create(HEARING_DEVICES_TILE_SPEC), uiConfig = QSTileUIConfig.Resource( iconRes = R.drawable.qs_hearing_devices_icon, labelRes = R.string.quick_settings_hearing_devices_label, ), instanceId = uiEventLogger.getNewInstanceId(), category = TileCategory.ACCESSIBILITY, ) /** * Inject Reduce Bright Colors Tile into tileViewModelMap in QSModule. The tile is hidden * behind a flag. Loading @@ -305,7 +301,7 @@ interface QSAccessibilityModule { factory: QSTileViewModelFactory.Static<ReduceBrightColorsTileModel>, mapper: ReduceBrightColorsTileMapper, stateInteractor: ReduceBrightColorsTileDataInteractor, userActionInteractor: ReduceBrightColorsTileUserActionInteractor userActionInteractor: ReduceBrightColorsTileUserActionInteractor, ): QSTileViewModel = if (Flags.qsNewTilesFuture()) factory.create( Loading Loading @@ -339,7 +335,7 @@ interface QSAccessibilityModule { factory: QSTileViewModelFactory.Static<OneHandedModeTileModel>, mapper: OneHandedModeTileMapper, stateInteractor: OneHandedModeTileDataInteractor, userActionInteractor: OneHandedModeTileUserActionInteractor userActionInteractor: OneHandedModeTileUserActionInteractor, ): QSTileViewModel = if (Flags.qsNewTilesFuture()) factory.create( Loading Loading @@ -376,7 +372,7 @@ interface QSAccessibilityModule { factory: QSTileViewModelFactory.Static<NightDisplayTileModel>, mapper: NightDisplayTileMapper, stateInteractor: NightDisplayTileDataInteractor, userActionInteractor: NightDisplayTileUserActionInteractor userActionInteractor: NightDisplayTileUserActionInteractor, ): QSTileViewModel = if (Flags.qsNewTilesFuture()) factory.create( Loading @@ -386,5 +382,43 @@ interface QSAccessibilityModule { mapper, ) else StubQSTileViewModel @Provides @IntoMap @StringKey(HEARING_DEVICES_TILE_SPEC) fun provideHearingDevicesTileConfig(uiEventLogger: QsEventLogger): QSTileConfig = QSTileConfig( tileSpec = TileSpec.create(HEARING_DEVICES_TILE_SPEC), uiConfig = QSTileUIConfig.Resource( iconRes = R.drawable.qs_hearing_devices_icon, labelRes = R.string.quick_settings_hearing_devices_label, ), instanceId = uiEventLogger.getNewInstanceId(), category = TileCategory.ACCESSIBILITY, ) /** * Inject HearingDevices Tile into tileViewModelMap in QSModule. The tile is hidden behind a * flag. */ @Provides @IntoMap @StringKey(HEARING_DEVICES_TILE_SPEC) fun provideHearingDevicesTileViewModel( factory: QSTileViewModelFactory.Static<HearingDevicesTileModel>, mapper: HearingDevicesTileMapper, stateInteractor: HearingDevicesTileDataInteractor, userActionInteractor: HearingDevicesTileUserActionInteractor, ): QSTileViewModel { return if (Flags.hearingAidsQsTileDialog() && Flags.qsNewTilesFuture()) { factory.create( TileSpec.create(HEARING_DEVICES_TILE_SPEC), userActionInteractor, stateInteractor, mapper, ) } else StubQSTileViewModel } } }
packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/HearingDevicesTileMapper.kt 0 → 100644 +59 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.qs.tiles.impl.hearingdevices.domain import android.content.res.Resources import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper import com.android.systemui.qs.tiles.impl.hearingdevices.domain.model.HearingDevicesTileModel import com.android.systemui.qs.tiles.viewmodel.QSTileConfig import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.res.R import javax.inject.Inject /** Maps [HearingDevicesTileModel] to [QSTileState]. */ class HearingDevicesTileMapper @Inject constructor(@Main private val resources: Resources, private val theme: Resources.Theme) : QSTileDataToStateMapper<HearingDevicesTileModel> { override fun map(config: QSTileConfig, data: HearingDevicesTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { label = resources.getString(R.string.quick_settings_hearing_devices_label) iconRes = R.drawable.qs_hearing_devices_icon val loadedIcon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), contentDescription = null) icon = { loadedIcon } sideViewIcon = QSTileState.SideViewIcon.Chevron contentDescription = label if (data.isAnyActiveHearingDevice) { activationState = QSTileState.ActivationState.ACTIVE secondaryLabel = resources.getString(R.string.quick_settings_hearing_devices_connected) } else if (data.isAnyPairedHearingDevice) { activationState = QSTileState.ActivationState.INACTIVE secondaryLabel = resources.getString(R.string.quick_settings_hearing_devices_disconnected) } else { activationState = QSTileState.ActivationState.INACTIVE secondaryLabel = "" } supportedActions = setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) } }