diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt index b0f9c4edb073f4ef6745480e87e021698e7b9cd0..d078688e5944adb12aafb6bedf245345d58492da 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt @@ -21,6 +21,7 @@ import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyboard.backlight.ui.KeyboardBacklightDialogCoordinator import javax.inject.Inject /** A [CoreStartable] that launches components interested in physical keyboard interaction. */ @@ -28,11 +29,12 @@ import javax.inject.Inject class PhysicalKeyboardCoreStartable @Inject constructor( + private val keyboardBacklightDialogCoordinator: KeyboardBacklightDialogCoordinator, private val featureFlags: FeatureFlags, ) : CoreStartable { override fun start() { if (featureFlags.isEnabled(Flags.KEYBOARD_BACKLIGHT_INDICATOR)) { - // TODO(b/268645743) start listening for keyboard backlight brightness + keyboardBacklightDialogCoordinator.startListening() } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractor.kt new file mode 100644 index 0000000000000000000000000000000000000000..65e70b319923c758a2130fc1a74589639c9b7224 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractor.kt @@ -0,0 +1,41 @@ +/* + * 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.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyboard.data.repository.KeyboardRepository +import com.android.systemui.keyboard.shared.model.BacklightModel +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf + +/** Allows listening to changes to keyboard backlight level */ +@SysUISingleton +class KeyboardBacklightInteractor +@Inject +constructor( + private val keyboardRepository: KeyboardRepository, +) { + + /** Emits current backlight level as [BacklightModel] or null if keyboard is not connected */ + val backlight: Flow = + keyboardRepository.keyboardConnected.flatMapLatest { connected -> + if (connected) keyboardRepository.backlight else flowOf(null) + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..85d0379a77db84774ad888b0fa707a89a3495056 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/KeyboardBacklightDialogCoordinator.kt @@ -0,0 +1,59 @@ +/* + * 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 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.BacklightDialogViewModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * 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, +) { + + var dialog: KeyboardBacklightDialog? = null + + fun startListening() { + applicationScope.launch { + viewModel.dialogContent.collect { dialogViewModel -> + if (dialogViewModel != null) { + if (dialog == null) { + dialog = KeyboardBacklightDialog(context, dialogViewModel) + // pass viewModel and show + } + } else { + dialog?.dismiss() + dialog = null + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt new file mode 100644 index 0000000000000000000000000000000000000000..b68a2a84b5d1356013d6a6755b9fb6ad6c32b4d7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt @@ -0,0 +1,32 @@ +/* + * 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.view + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import com.android.systemui.keyboard.backlight.ui.viewmodel.BacklightDialogContentViewModel + +class KeyboardBacklightDialog(context: Context, val viewModel: BacklightDialogContentViewModel) : + Dialog(context) { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // TODO(b/268650355) Implement the dialog + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogContentViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..3ef0ca39b8f37802fa17782bb4ecfb10a507c60e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogContentViewModel.kt @@ -0,0 +1,20 @@ +/* + * 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 + +data class BacklightDialogContentViewModel(val currentValue: Int, val maxValue: Int) diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..86abca5faaf3ce7acc0886bafbc2f6c8efb232c0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogViewModel.kt @@ -0,0 +1,72 @@ +/* + * 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 android.view.accessibility.AccessibilityManager +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyboard.backlight.domain.interactor.KeyboardBacklightInteractor +import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper +import javax.inject.Inject +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map + +/** + * Responsible for dialog visibility and content - emits [BacklightDialogContentViewModel] if dialog + * should be shown and hidden otherwise + */ +@SysUISingleton +class BacklightDialogViewModel +@Inject +constructor( + interactor: KeyboardBacklightInteractor, + private val accessibilityManagerWrapper: AccessibilityManagerWrapper, +) { + + private val timeoutMillis: Long + get() = + accessibilityManagerWrapper + .getRecommendedTimeoutMillis( + DEFAULT_DIALOG_TIMEOUT_MILLIS, + AccessibilityManager.FLAG_CONTENT_ICONS + ) + .toLong() + + val dialogContent: Flow = + interactor.backlight + .filterNotNull() + .map { BacklightDialogContentViewModel(it.level, it.maxLevel) } + .timeout(timeoutMillis, emitAfterTimeout = null) + + private fun Flow.timeout(timeoutMillis: Long, emitAfterTimeout: T): Flow { + return flatMapLatest { + flow { + emit(it) + delay(timeoutMillis) + emit(emitAfterTimeout) + } + } + } + + private companion object { + const val DEFAULT_DIALOG_TIMEOUT_MILLIS = 3000 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt index 70faf406d62133fb66950c7f0c06a5658d08a0ec..9449ece0933b2fcc3bf3cdab2b2d290f87698631 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt @@ -23,7 +23,7 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.keyboard.data.model.BacklightModel +import com.android.systemui.keyboard.shared.model.BacklightModel import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/model/BacklightModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shared/model/BacklightModel.kt similarity index 94% rename from packages/SystemUI/src/com/android/systemui/keyboard/data/model/BacklightModel.kt rename to packages/SystemUI/src/com/android/systemui/keyboard/shared/model/BacklightModel.kt index ea15a9f185846a4fceeed3c57a92ebf333e1b401..4a32f79285e37a41009065fc44f1f7cba17b1e21 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/data/model/BacklightModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shared/model/BacklightModel.kt @@ -15,7 +15,7 @@ * */ -package com.android.systemui.keyboard.data.model +package com.android.systemui.keyboard.shared.model /** * Model for current state of keyboard backlight brightness. [level] indicates current level of diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractorTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..ec94cdec78f0c6160aeaf352ece0d72aa72c9dba --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractorTest.kt @@ -0,0 +1,73 @@ +/* + * 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.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository +import com.android.systemui.keyboard.shared.model.BacklightModel +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class KeyboardBacklightInteractorTest : SysuiTestCase() { + + private val keyboardRepository = FakeKeyboardRepository() + private lateinit var underTest: KeyboardBacklightInteractor + + @Before + fun setUp() { + underTest = KeyboardBacklightInteractor(keyboardRepository) + } + + @Test + fun emitsNull_whenKeyboardJustConnected() = runTest { + val latest by collectLastValue(underTest.backlight) + keyboardRepository.setKeyboardConnected(true) + + assertThat(latest).isNull() + } + + @Test + fun emitsBacklight_whenKeyboardConnectedAndBacklightChanged() = runTest { + keyboardRepository.setKeyboardConnected(true) + keyboardRepository.setBacklight(BacklightModel(1, 5)) + + assertThat(underTest.backlight.first()).isEqualTo(BacklightModel(1, 5)) + } + + @Test + fun emitsNull_afterKeyboardDisconnecting() = runTest { + val latest by collectLastValue(underTest.backlight) + keyboardRepository.setKeyboardConnected(true) + keyboardRepository.setBacklight(BacklightModel(1, 5)) + + keyboardRepository.setKeyboardConnected(false) + + assertThat(latest).isNull() + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..ec05d10b793cc81ccdc90d1321e95b286ff54259 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogViewModelTest.kt @@ -0,0 +1,102 @@ +/* + * 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.setKeyboardConnected(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.setKeyboardConnected(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() + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..4e435462be5086a843b1f794f5c62cefab6f2cdb --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt @@ -0,0 +1,41 @@ +/* + * 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.data.repository + +import com.android.systemui.keyboard.shared.model.BacklightModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull + +class FakeKeyboardRepository : KeyboardRepository { + + private val _keyboardConnected = MutableStateFlow(false) + override val keyboardConnected: Flow = _keyboardConnected + + private val _backlightState: MutableStateFlow = MutableStateFlow(null) + // filtering to make sure backlight doesn't have default initial value + override val backlight: Flow = _backlightState.filterNotNull() + + fun setBacklight(state: BacklightModel) { + _backlightState.value = state + } + + fun setKeyboardConnected(connected: Boolean) { + _keyboardConnected.value = connected + } +}