diff options
| author | 2024-01-15 17:39:32 +0000 | |
|---|---|---|
| committer | 2024-01-16 18:26:46 +0000 | |
| commit | 14c94d325ba577efdf643d8d0debdcaed68b353d (patch) | |
| tree | ff766080376c487ecccb319cab56027b72474485 | |
| parent | 8b73725017cc2e76ec6349dae55e5b5901106d12 (diff) | |
Sticky keys UI: implementing repository and view model
Adding StickyKeysRepository which registers listener in InputManager and passes data to flow listeners.
StickyKeyIndicatorViewModel is the only layer above (which is likely to change soon when more input data is needed), domain layer seemed a bit of an overkill for now.
Classes are not used from anywhere yet so code is not active or flagged.
Test: StickyKeysIndicatorViewModelTest
Bug: 313855932
Flag: None
Change-Id: I5493d866947d8553f8b40f7fb26324eafaf312a6
9 files changed, 461 insertions, 0 deletions
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index a03fa9b39bfc..128ae63791fe 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -354,6 +354,8 @@ <uses-permission android:name="android.permission.MONITOR_KEYBOARD_BACKLIGHT" /> + <uses-permission android:name="android.permission.MONITOR_STICKY_MODIFIER_STATE" /> + <!-- Listen to (dis-)connection of external displays and enable / disable them. --> <uses-permission android:name="android.permission.MANAGE_DISPLAYS" /> diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt index 496c64e1120e..c6fb4f9d6956 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt @@ -19,6 +19,8 @@ package com.android.systemui.keyboard import com.android.systemui.keyboard.data.repository.KeyboardRepository import com.android.systemui.keyboard.data.repository.KeyboardRepositoryImpl +import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepository +import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepositoryImpl import dagger.Binds import dagger.Module @@ -27,4 +29,9 @@ abstract class KeyboardModule { @Binds abstract fun bindKeyboardRepository(repository: KeyboardRepositoryImpl): KeyboardRepository + + @Binds + abstract fun bindStickyKeysRepository( + repository: StickyKeysRepositoryImpl + ): StickyKeysRepository } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt new file mode 100644 index 000000000000..37034f63aca7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt @@ -0,0 +1,37 @@ +/* + * 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.keyboard.stickykeys + +import com.android.systemui.keyboard.stickykeys.shared.model.Locked +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel +import com.android.systemui.log.dagger.KeyboardLog +import javax.inject.Inject + +private const val TAG = "stickyKeys" + +class StickyKeysLogger @Inject constructor(@KeyboardLog private val buffer: LogBuffer) { + fun logNewStickyKeysReceived(linkedHashMap: Map<ModifierKey, Locked>) { + buffer.log( + TAG, + LogLevel.VERBOSE, + { str1 = linkedHashMap.toString() }, + { "new sticky keys state received: $str1" } + ) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt new file mode 100644 index 000000000000..34d288815570 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt @@ -0,0 +1,92 @@ +/* + * 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.keyboard.stickykeys.data.repository + +import android.hardware.input.InputManager +import android.hardware.input.InputManager.StickyModifierStateListener +import android.hardware.input.StickyModifierState +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.keyboard.stickykeys.StickyKeysLogger +import com.android.systemui.keyboard.stickykeys.shared.model.Locked +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.ALT +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.ALT_GR +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.CTRL +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.META +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.SHIFT +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +interface StickyKeysRepository { + val stickyKeys: Flow<LinkedHashMap<ModifierKey, Locked>> + val settingEnabled: Flow<Boolean> +} + +class StickyKeysRepositoryImpl +@Inject +constructor( + private val inputManager: InputManager, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val stickyKeysLogger: StickyKeysLogger, +) : StickyKeysRepository { + + override val stickyKeys: Flow<LinkedHashMap<ModifierKey, Locked>> = + conflatedCallbackFlow { + val listener = StickyModifierStateListener { stickyModifierState -> + trySendWithFailureLogging(stickyModifierState, TAG) + } + // after registering, InputManager calls listener with the current value + inputManager.registerStickyModifierStateListener(Runnable::run, listener) + awaitClose { inputManager.unregisterStickyModifierStateListener(listener) } + } + .map { toStickyKeysMap(it) } + .onEach { stickyKeysLogger.logNewStickyKeysReceived(it) } + .flowOn(backgroundDispatcher) + + // TODO(b/319837892): Implement reading actual setting + override val settingEnabled: StateFlow<Boolean> = MutableStateFlow(true) + + private fun toStickyKeysMap(state: StickyModifierState): LinkedHashMap<ModifierKey, Locked> { + val keys = linkedMapOf<ModifierKey, Locked>() + state.apply { + if (isAltGrModifierOn) keys[ALT_GR] = Locked(false) + if (isAltGrModifierLocked) keys[ALT_GR] = Locked(true) + if (isAltModifierOn) keys[ALT] = Locked(false) + if (isAltModifierLocked) keys[ALT] = Locked(true) + if (isCtrlModifierOn) keys[CTRL] = Locked(false) + if (isCtrlModifierLocked) keys[CTRL] = Locked(true) + if (isMetaModifierOn) keys[META] = Locked(false) + if (isMetaModifierLocked) keys[META] = Locked(true) + if (isShiftModifierOn) keys[SHIFT] = Locked(false) + if (isShiftModifierLocked) keys[SHIFT] = Locked(true) + } + return keys + } + + companion object { + const val TAG = "StickyKeysRepositoryImpl" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/shared/model/StickyKey.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/shared/model/StickyKey.kt new file mode 100644 index 000000000000..d5f082a2566f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/shared/model/StickyKey.kt @@ -0,0 +1,28 @@ +/* + * 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.keyboard.stickykeys.shared.model + +@JvmInline +value class Locked(val locked: Boolean) + +enum class ModifierKey(val text: String) { + ALT("ALT LEFT"), + ALT_GR("ALT RIGHT"), + CTRL("CTRL"), + META("META"), + SHIFT("SHIFT"), +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModel.kt new file mode 100644 index 000000000000..26eb706da200 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModel.kt @@ -0,0 +1,51 @@ +/* + * 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.keyboard.stickykeys.ui.viewmodel + +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyboard.data.repository.KeyboardRepository +import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepository +import com.android.systemui.keyboard.stickykeys.shared.model.Locked +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +class StickyKeysIndicatorViewModel +@Inject +constructor( + stickyKeysRepository: StickyKeysRepository, + keyboardRepository: KeyboardRepository, + @Application applicationScope: CoroutineScope, +) { + + @OptIn(ExperimentalCoroutinesApi::class) + val indicatorContent: Flow<Map<ModifierKey, Locked>> = + keyboardRepository.isAnyKeyboardConnected + .flatMapLatest { keyboardPresent -> + if (keyboardPresent) stickyKeysRepository.settingEnabled else flowOf(false) + } + .flatMapLatest { enabled -> + if (enabled) stickyKeysRepository.stickyKeys else flowOf(emptyMap()) + } + .stateIn(applicationScope, SharingStarted.Lazily, emptyMap()) +} diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyboardLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyboardLog.kt new file mode 100644 index 000000000000..5910701d9f2a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyboardLog.kt @@ -0,0 +1,25 @@ +/* + * 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.log.dagger + +import javax.inject.Qualifier + +/** A [com.android.systemui.log.LogBuffer] for keyboard-related functionality. */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class KeyboardLog diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index 1e677719e92a..a093de7057bf 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -602,6 +602,14 @@ public class LogModule { return factory.create("BluetoothTileDialogLog", 50); } + /** Provides a {@link LogBuffer} for the keyboard functionalities. */ + @Provides + @SysUISingleton + @KeyboardLog + public static LogBuffer provideKeyboardLogBuffer(LogBufferFactory factory) { + return factory.create("KeyboardLog", 50); + } + /** Provides a {@link LogBuffer} for {@link PackageChangeRepository} */ @Provides @SysUISingleton diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt new file mode 100644 index 000000000000..d397fc202637 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt @@ -0,0 +1,211 @@ +/* + * 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.keyboard.stickykeys.ui.viewmodel + +import android.hardware.input.InputManager +import android.hardware.input.StickyModifierState +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.stickykeys.StickyKeysLogger +import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepositoryImpl +import com.android.systemui.keyboard.stickykeys.shared.model.Locked +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.ALT +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.ALT_GR +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.CTRL +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.META +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.SHIFT +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +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.ArgumentCaptor +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions + +@SmallTest +@RunWith(JUnit4::class) +class StickyKeysIndicatorViewModelTest : SysuiTestCase() { + + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + private lateinit var viewModel: StickyKeysIndicatorViewModel + private val inputManager = mock<InputManager>() + private val keyboardRepository = FakeKeyboardRepository() + private val captor = + ArgumentCaptor.forClass(InputManager.StickyModifierStateListener::class.java) + + @Before + fun setup() { + val stickyKeysRepository = StickyKeysRepositoryImpl( + inputManager, + dispatcher, + mock<StickyKeysLogger>() + ) + viewModel = + StickyKeysIndicatorViewModel( + stickyKeysRepository = stickyKeysRepository, + keyboardRepository = keyboardRepository, + applicationScope = testScope.backgroundScope, + ) + } + + @Test + fun startsListeningToStickyKeysOnlyWhenKeyboardIsConnected() { + testScope.runTest { + collectLastValue(viewModel.indicatorContent) + runCurrent() + verifyZeroInteractions(inputManager) + + keyboardRepository.setIsAnyKeyboardConnected(true) + runCurrent() + + verify(inputManager) + .registerStickyModifierStateListener( + any(), + any(InputManager.StickyModifierStateListener::class.java) + ) + } + } + + @Test + fun stopsListeningToStickyKeysWhenKeyboardDisconnects() { + testScope.runTest { + collectLastValue(viewModel.indicatorContent) + keyboardRepository.setIsAnyKeyboardConnected(true) + runCurrent() + + keyboardRepository.setIsAnyKeyboardConnected(false) + runCurrent() + + verify(inputManager).unregisterStickyModifierStateListener(any()) + } + } + + @Test + fun emitsStickyKeysListWhenStickyKeyIsPressed() { + testScope.runTest { + val stickyKeys by collectLastValue(viewModel.indicatorContent) + keyboardRepository.setIsAnyKeyboardConnected(true) + + setStickyKeys(mapOf(ALT to false)) + + assertThat(stickyKeys).isEqualTo(mapOf(ALT to Locked(false))) + } + } + + @Test + fun emitsEmptyListWhenNoStickyKeysAreActive() { + testScope.runTest { + val stickyKeys by collectLastValue(viewModel.indicatorContent) + keyboardRepository.setIsAnyKeyboardConnected(true) + + setStickyKeys(emptyMap()) + + assertThat(stickyKeys).isEqualTo(emptyMap<ModifierKey, Locked>()) + } + } + + @Test + fun passesAllStickyKeysToDialog() { + testScope.runTest { + val stickyKeys by collectLastValue(viewModel.indicatorContent) + keyboardRepository.setIsAnyKeyboardConnected(true) + + setStickyKeys(mapOf( + ALT to false, + META to false, + SHIFT to false)) + + assertThat(stickyKeys).isEqualTo(mapOf( + ALT to Locked(false), + META to Locked(false), + SHIFT to Locked(false), + )) + } + } + + @Test + fun showsOnlyLockedStateIfKeyIsStickyAndLocked() { + testScope.runTest { + val stickyKeys by collectLastValue(viewModel.indicatorContent) + keyboardRepository.setIsAnyKeyboardConnected(true) + + setStickyKeys(mapOf( + ALT to false, + ALT to true)) + + assertThat(stickyKeys).isEqualTo(mapOf(ALT to Locked(true))) + } + } + + @Test + fun doesNotChangeOrderOfKeysIfTheyBecomeLocked() { + testScope.runTest { + val stickyKeys by collectLastValue(viewModel.indicatorContent) + keyboardRepository.setIsAnyKeyboardConnected(true) + + setStickyKeys(mapOf( + META to false, + SHIFT to false, // shift is sticky but not locked + CTRL to false)) + val previousShiftIndex = stickyKeys?.toList()?.indexOf(SHIFT to Locked(false)) + + setStickyKeys(mapOf( + SHIFT to false, + SHIFT to true, // shift is now locked + META to false, + CTRL to false)) + assertThat(stickyKeys?.toList()?.indexOf(SHIFT to Locked(true))) + .isEqualTo(previousShiftIndex) + } + } + + private fun TestScope.setStickyKeys(keys: Map<ModifierKey, Boolean>) { + runCurrent() + verify(inputManager).registerStickyModifierStateListener(any(), captor.capture()) + captor.value.onStickyModifierStateChanged(TestStickyModifierState(keys)) + runCurrent() + } + + private class TestStickyModifierState(private val keys: Map<ModifierKey, Boolean>) : + StickyModifierState() { + + private fun isOn(key: ModifierKey) = keys.any { it.key == key && !it.value } + private fun isLocked(key: ModifierKey) = keys.any { it.key == key && it.value } + + override fun isAltGrModifierLocked() = isLocked(ALT_GR) + override fun isAltGrModifierOn() = isOn(ALT_GR) + override fun isAltModifierLocked() = isLocked(ALT) + override fun isAltModifierOn() = isOn(ALT) + override fun isCtrlModifierLocked() = isLocked(CTRL) + override fun isCtrlModifierOn() = isOn(CTRL) + override fun isMetaModifierLocked() = isLocked(META) + override fun isMetaModifierOn() = isOn(META) + override fun isShiftModifierLocked() = isLocked(SHIFT) + override fun isShiftModifierOn() = isOn(SHIFT) + } +} |