diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/KeyboardBacklightDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/KeyboardBacklightDialogCoordinator.kt index 5e806b6128055..1f421fae25175 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/KeyboardBacklightDialogCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/KeyboardBacklightDialogCoordinator.kt @@ -21,41 +21,44 @@ import android.content.Context import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyboard.backlight.ui.view.KeyboardBacklightDialog +import com.android.systemui.keyboard.backlight.ui.viewmodel.BacklightDialogContentViewModel import com.android.systemui.keyboard.backlight.ui.viewmodel.BacklightDialogViewModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +private fun defaultCreateDialog(context: Context): (Int, Int) -> KeyboardBacklightDialog { + return { currentLevel: Int, maxLevel: Int -> + KeyboardBacklightDialog(context, currentLevel, maxLevel) + } +} + /** * Based on the state produced from [BacklightDialogViewModel] shows or hides keyboard backlight * indicator */ @SysUISingleton class KeyboardBacklightDialogCoordinator -@Inject constructor( @Application private val applicationScope: CoroutineScope, - private val context: Context, private val viewModel: BacklightDialogViewModel, + private val createDialog: (Int, Int) -> KeyboardBacklightDialog ) { + @Inject + constructor( + @Application applicationScope: CoroutineScope, + context: Context, + viewModel: BacklightDialogViewModel + ) : this(applicationScope, viewModel, defaultCreateDialog(context)) + var dialog: KeyboardBacklightDialog? = null fun startListening() { applicationScope.launch { - viewModel.dialogContent.collect { dialogViewModel -> - if (dialogViewModel != null) { - if (dialog == null) { - dialog = - KeyboardBacklightDialog( - context, - initialCurrentLevel = dialogViewModel.currentValue, - initialMaxLevel = dialogViewModel.maxValue - ) - dialog?.show() - } else { - dialog?.updateState(dialogViewModel.currentValue, dialogViewModel.maxValue) - } + viewModel.dialogContent.collect { contentModel -> + if (contentModel != null) { + showDialog(contentModel) } else { dialog?.dismiss() dialog = null @@ -63,4 +66,15 @@ constructor( } } } + + private fun showDialog(model: BacklightDialogContentViewModel) { + if (dialog == null) { + dialog = createDialog(model.currentValue, model.maxValue) + } else { + dialog?.updateState(model.currentValue, model.maxValue) + } + // let's always show dialog - even if we're just updating it, it might have been dismissed + // externally by tapping finger outside of it + dialog?.show() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/KeyboardBacklightDialogCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/KeyboardBacklightDialogCoordinatorTest.kt new file mode 100644 index 0000000000000..7207fbf83bd75 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/KeyboardBacklightDialogCoordinatorTest.kt @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2023 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.keyboard.backlight.ui + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyboard.backlight.domain.interactor.KeyboardBacklightInteractor +import com.android.systemui.keyboard.backlight.ui.view.KeyboardBacklightDialog +import com.android.systemui.keyboard.backlight.ui.viewmodel.BacklightDialogViewModel +import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository +import com.android.systemui.keyboard.shared.model.BacklightModel +import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +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.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class KeyboardBacklightDialogCoordinatorTest : SysuiTestCase() { + + @Mock private lateinit var accessibilityManagerWrapper: AccessibilityManagerWrapper + @Mock private lateinit var dialog: KeyboardBacklightDialog + + private val keyboardRepository = FakeKeyboardRepository() + private lateinit var underTest: KeyboardBacklightDialogCoordinator + private val timeoutMillis = 3000L + private val testScope = TestScope(StandardTestDispatcher()) + + private val createDialog = { value: Int, maxValue: Int -> + dialogCreationValue = value + dialogCreationMaxValue = maxValue + dialog + } + private var dialogCreationValue = -1 + private var dialogCreationMaxValue = -1 + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(accessibilityManagerWrapper.getRecommendedTimeoutMillis(any(), any())) + .thenReturn(timeoutMillis.toInt()) + val viewModel = + BacklightDialogViewModel( + KeyboardBacklightInteractor(keyboardRepository), + accessibilityManagerWrapper + ) + underTest = + KeyboardBacklightDialogCoordinator(testScope.backgroundScope, viewModel, createDialog) + underTest.startListening() + keyboardRepository.setIsAnyKeyboardConnected(true) + } + + @Test + fun showsDialog_afterBacklightChange() = + testScope.runTest { + setBacklightValue(1) + + verify(dialog).show() + } + + @Test + fun updatesDialog_withLatestValues_afterBacklightChange() = + testScope.runTest { + setBacklightValue(value = 1, maxValue = 5) + setBacklightValue(value = 2, maxValue = 5) + + verify(dialog).updateState(2, 5) + } + + @Test + fun showsDialog_withDataFromBacklightChange() = + testScope.runTest { + setBacklightValue(value = 4, maxValue = 5) + + Truth.assertThat(dialogCreationValue).isEqualTo(4) + Truth.assertThat(dialogCreationMaxValue).isEqualTo(5) + } + + @Test + fun dismissesDialog_afterTimeout() = + testScope.runTest { + setBacklightValue(1) + + advanceTimeBy(timeoutMillis + 1) + + verify(dialog).dismiss() + } + + @Test + fun dismissesDialog_onlyAfterTimeout_fromLastBacklightChange() = + testScope.runTest { + setBacklightValue(1) + advanceTimeBy(timeoutMillis * 2 / 3) + // majority of timeout passed + + // this should restart timeout + setBacklightValue(2) + advanceTimeBy(timeoutMillis * 2 / 3) + verify(dialog, never()).dismiss() + + advanceTimeBy(timeoutMillis * 2 / 3) + // finally timeout reached and dialog was dismissed + verify(dialog, times(1)).dismiss() + } + + @Test + fun showsDialog_ifItWasAlreadyShownAndDismissedBySomethingElse() = + testScope.runTest { + setBacklightValue(1) + // let's pretend dialog is dismissed e.g. by user tapping on the screen + whenever(dialog.isShowing).thenReturn(false) + + // no advancing time, we're still in timeout period + setBacklightValue(2) + + verify(dialog, times(2)).show() + } + + private fun TestScope.setBacklightValue(value: Int, maxValue: Int = MAX_BACKLIGHT) { + keyboardRepository.setBacklight(BacklightModel(value, maxValue)) + runCurrent() + } + + private companion object { + const val MAX_BACKLIGHT = 5 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogViewModelTest.kt deleted file mode 100644 index 1fec5a4d0f370..0000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogViewModelTest.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2023 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.keyboard.backlight.ui.viewmodel - -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.keyboard.backlight.domain.interactor.KeyboardBacklightInteractor -import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository -import com.android.systemui.keyboard.shared.model.BacklightModel -import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.whenever -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.advanceTimeBy -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.mockito.Mock -import org.mockito.MockitoAnnotations - -@OptIn(ExperimentalCoroutinesApi::class) -@SmallTest -@RunWith(JUnit4::class) -class BacklightDialogViewModelTest : SysuiTestCase() { - - private val keyboardRepository = FakeKeyboardRepository() - private lateinit var underTest: BacklightDialogViewModel - @Mock private lateinit var accessibilityManagerWrapper: AccessibilityManagerWrapper - private val timeoutMillis = 3000L - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - whenever(accessibilityManagerWrapper.getRecommendedTimeoutMillis(any(), any())) - .thenReturn(timeoutMillis.toInt()) - underTest = - BacklightDialogViewModel( - KeyboardBacklightInteractor(keyboardRepository), - accessibilityManagerWrapper - ) - keyboardRepository.setIsAnyKeyboardConnected(true) - } - - @Test - fun emitsViewModel_whenBacklightChanged() = runTest { - keyboardRepository.setBacklight(BacklightModel(1, 5)) - - assertThat(underTest.dialogContent.first()).isEqualTo(BacklightDialogContentViewModel(1, 5)) - } - - @Test - fun emitsNull_afterTimeout() = runTest { - val latest by collectLastValue(underTest.dialogContent) - keyboardRepository.setBacklight(BacklightModel(1, 5)) - - assertThat(latest).isEqualTo(BacklightDialogContentViewModel(1, 5)) - advanceTimeBy(timeoutMillis + 1) - assertThat(latest).isNull() - } - - @Test - fun emitsNull_after5secDelay_fromLastBacklightChange() = runTest { - val latest by collectLastValue(underTest.dialogContent) - keyboardRepository.setIsAnyKeyboardConnected(true) - - keyboardRepository.setBacklight(BacklightModel(1, 5)) - assertThat(latest).isEqualTo(BacklightDialogContentViewModel(1, 5)) - - advanceTimeBy(timeoutMillis * 2 / 3) - // timeout yet to pass, no new emission - keyboardRepository.setBacklight(BacklightModel(2, 5)) - assertThat(latest).isEqualTo(BacklightDialogContentViewModel(2, 5)) - - advanceTimeBy(timeoutMillis * 2 / 3) - // timeout refreshed because of last `setBacklight`, still content present - assertThat(latest).isEqualTo(BacklightDialogContentViewModel(2, 5)) - - advanceTimeBy(timeoutMillis * 2 / 3) - // finally timeout reached and null emitted - assertThat(latest).isNull() - } -}