1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.keyboard.stickykeys.ui.viewmodel 18 19 import android.hardware.input.InputManager 20 import android.hardware.input.StickyModifierState 21 import android.provider.Settings.Secure.ACCESSIBILITY_STICKY_KEYS 22 import androidx.test.ext.junit.runners.AndroidJUnit4 23 import androidx.test.filters.SmallTest 24 import com.android.systemui.SysuiTestCase 25 import com.android.systemui.coroutines.collectLastValue 26 import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository 27 import com.android.systemui.keyboard.stickykeys.StickyKeysLogger 28 import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepositoryImpl 29 import com.android.systemui.keyboard.stickykeys.shared.model.Locked 30 import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey 31 import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.ALT 32 import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.ALT_GR 33 import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.CTRL 34 import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.META 35 import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.SHIFT 36 import com.android.systemui.kosmos.Kosmos 37 import com.android.systemui.user.data.repository.fakeUserRepository 38 import com.android.systemui.util.mockito.any 39 import com.android.systemui.util.mockito.mock 40 import com.android.systemui.util.settings.FakeSettings 41 import com.android.systemui.util.settings.repository.UserAwareSecureSettingsRepositoryImpl 42 import com.google.common.truth.Truth.assertThat 43 import kotlinx.coroutines.ExperimentalCoroutinesApi 44 import kotlinx.coroutines.test.StandardTestDispatcher 45 import kotlinx.coroutines.test.TestScope 46 import kotlinx.coroutines.test.runCurrent 47 import kotlinx.coroutines.test.runTest 48 import org.junit.Before 49 import org.junit.Test 50 import org.junit.runner.RunWith 51 import org.mockito.ArgumentCaptor 52 import org.mockito.Mockito.verify 53 import org.mockito.Mockito.verifyZeroInteractions 54 55 @OptIn(ExperimentalCoroutinesApi::class) 56 @SmallTest 57 @RunWith(AndroidJUnit4::class) 58 class StickyKeysIndicatorViewModelTest : SysuiTestCase() { 59 60 private val dispatcher = StandardTestDispatcher() 61 private val testScope = TestScope(dispatcher) 62 private lateinit var viewModel: StickyKeysIndicatorViewModel 63 private val inputManager = mock<InputManager>() 64 private val keyboardRepository = FakeKeyboardRepository() 65 private val secureSettings = FakeSettings() 66 private val userRepository = Kosmos().fakeUserRepository 67 private val captor = 68 ArgumentCaptor.forClass(InputManager.StickyModifierStateListener::class.java) 69 70 @Before setupnull71 fun setup() { 72 val settingsRepository = UserAwareSecureSettingsRepositoryImpl( 73 secureSettings, 74 userRepository, 75 dispatcher 76 ) 77 val stickyKeysRepository = StickyKeysRepositoryImpl( 78 inputManager, 79 dispatcher, 80 settingsRepository, 81 mock<StickyKeysLogger>() 82 ) 83 setStickyKeySetting(enabled = false) 84 viewModel = 85 StickyKeysIndicatorViewModel( 86 stickyKeysRepository = stickyKeysRepository, 87 keyboardRepository = keyboardRepository, 88 applicationScope = testScope.backgroundScope, 89 ) 90 } 91 92 @Test doesntListenToStickyKeysOnlyWhenKeyboardIsConnectednull93 fun doesntListenToStickyKeysOnlyWhenKeyboardIsConnected() { 94 testScope.runTest { 95 collectLastValue(viewModel.indicatorContent) 96 97 keyboardRepository.setIsAnyKeyboardConnected(true) 98 runCurrent() 99 100 verifyZeroInteractions(inputManager) 101 } 102 } 103 104 @Test startsListeningToStickyKeysOnlyWhenKeyboardIsConnectedAndSettingIsOnnull105 fun startsListeningToStickyKeysOnlyWhenKeyboardIsConnectedAndSettingIsOn() { 106 testScope.runTest { 107 collectLastValue(viewModel.indicatorContent) 108 keyboardRepository.setIsAnyKeyboardConnected(true) 109 110 setStickyKeySetting(enabled = true) 111 runCurrent() 112 113 verify(inputManager) 114 .registerStickyModifierStateListener( 115 any(), 116 any(InputManager.StickyModifierStateListener::class.java) 117 ) 118 } 119 } 120 setStickyKeySettingnull121 private fun setStickyKeySetting(enabled: Boolean) { 122 val newValue = if (enabled) "1" else "0" 123 val defaultUser = userRepository.getSelectedUserInfo().id 124 secureSettings.putStringForUser(ACCESSIBILITY_STICKY_KEYS, newValue, defaultUser) 125 } 126 127 @Test stopsListeningToStickyKeysWhenStickyKeySettingsIsTurnedOffnull128 fun stopsListeningToStickyKeysWhenStickyKeySettingsIsTurnedOff() { 129 testScope.runTest { 130 collectLastValue(viewModel.indicatorContent) 131 setStickyKeysActive() 132 runCurrent() 133 134 setStickyKeySetting(enabled = false) 135 runCurrent() 136 137 verify(inputManager).unregisterStickyModifierStateListener(any()) 138 } 139 } 140 141 @Test stopsListeningToStickyKeysWhenKeyboardDisconnectsnull142 fun stopsListeningToStickyKeysWhenKeyboardDisconnects() { 143 testScope.runTest { 144 collectLastValue(viewModel.indicatorContent) 145 setStickyKeysActive() 146 runCurrent() 147 148 keyboardRepository.setIsAnyKeyboardConnected(false) 149 runCurrent() 150 151 verify(inputManager).unregisterStickyModifierStateListener(any()) 152 } 153 } 154 155 @Test emitsStickyKeysListWhenStickyKeyIsPressednull156 fun emitsStickyKeysListWhenStickyKeyIsPressed() { 157 testScope.runTest { 158 val stickyKeys by collectLastValue(viewModel.indicatorContent) 159 setStickyKeysActive() 160 161 setStickyKeys(mapOf(ALT to false)) 162 163 assertThat(stickyKeys).isEqualTo(mapOf(ALT to Locked(false))) 164 } 165 } 166 167 @Test emitsEmptyListWhenNoStickyKeysAreActivenull168 fun emitsEmptyListWhenNoStickyKeysAreActive() { 169 testScope.runTest { 170 val stickyKeys by collectLastValue(viewModel.indicatorContent) 171 setStickyKeysActive() 172 173 setStickyKeys(emptyMap()) 174 175 assertThat(stickyKeys).isEqualTo(emptyMap<ModifierKey, Locked>()) 176 } 177 } 178 179 @Test passesAllStickyKeysToDialognull180 fun passesAllStickyKeysToDialog() { 181 testScope.runTest { 182 val stickyKeys by collectLastValue(viewModel.indicatorContent) 183 setStickyKeysActive() 184 185 setStickyKeys(mapOf( 186 ALT to false, 187 META to false, 188 SHIFT to false)) 189 190 assertThat(stickyKeys).isEqualTo(mapOf( 191 ALT to Locked(false), 192 META to Locked(false), 193 SHIFT to Locked(false), 194 )) 195 } 196 } 197 198 @Test showsOnlyLockedStateIfKeyIsStickyAndLockednull199 fun showsOnlyLockedStateIfKeyIsStickyAndLocked() { 200 testScope.runTest { 201 val stickyKeys by collectLastValue(viewModel.indicatorContent) 202 setStickyKeysActive() 203 204 setStickyKeys(mapOf( 205 ALT to false, 206 ALT to true)) 207 208 assertThat(stickyKeys).isEqualTo(mapOf(ALT to Locked(true))) 209 } 210 } 211 212 @Test doesNotChangeOrderOfKeysIfTheyBecomeLockednull213 fun doesNotChangeOrderOfKeysIfTheyBecomeLocked() { 214 testScope.runTest { 215 val stickyKeys by collectLastValue(viewModel.indicatorContent) 216 setStickyKeysActive() 217 218 setStickyKeys(mapOf( 219 META to false, 220 SHIFT to false, // shift is sticky but not locked 221 CTRL to false)) 222 val previousShiftIndex = stickyKeys?.toList()?.indexOf(SHIFT to Locked(false)) 223 224 setStickyKeys(mapOf( 225 SHIFT to false, 226 SHIFT to true, // shift is now locked 227 META to false, 228 CTRL to false)) 229 assertThat(stickyKeys?.toList()?.indexOf(SHIFT to Locked(true))) 230 .isEqualTo(previousShiftIndex) 231 } 232 } 233 setStickyKeysActivenull234 private fun setStickyKeysActive() { 235 keyboardRepository.setIsAnyKeyboardConnected(true) 236 setStickyKeySetting(enabled = true) 237 } 238 setStickyKeysnull239 private fun TestScope.setStickyKeys(keys: Map<ModifierKey, Boolean>) { 240 runCurrent() 241 verify(inputManager).registerStickyModifierStateListener(any(), captor.capture()) 242 captor.value.onStickyModifierStateChanged(TestStickyModifierState(keys)) 243 runCurrent() 244 } 245 246 private class TestStickyModifierState(private val keys: Map<ModifierKey, Boolean>) : 247 StickyModifierState() { 248 <lambda>null249 private fun isOn(key: ModifierKey) = keys.any { it.key == key && !it.value } <lambda>null250 private fun isLocked(key: ModifierKey) = keys.any { it.key == key && it.value } 251 isAltGrModifierLockednull252 override fun isAltGrModifierLocked() = isLocked(ALT_GR) 253 override fun isAltGrModifierOn() = isOn(ALT_GR) 254 override fun isAltModifierLocked() = isLocked(ALT) 255 override fun isAltModifierOn() = isOn(ALT) 256 override fun isCtrlModifierLocked() = isLocked(CTRL) 257 override fun isCtrlModifierOn() = isOn(CTRL) 258 override fun isMetaModifierLocked() = isLocked(META) 259 override fun isMetaModifierOn() = isOn(META) 260 override fun isShiftModifierLocked() = isLocked(SHIFT) 261 override fun isShiftModifierOn() = isOn(SHIFT) 262 } 263 } 264