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.kosmos.testDispatcher 38 import com.android.systemui.kosmos.testScope 39 import com.android.systemui.testKosmos 40 import com.android.systemui.user.data.repository.fakeUserRepository 41 import com.android.systemui.util.mockito.any 42 import com.android.systemui.util.mockito.mock 43 import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository 44 import com.android.systemui.util.settings.fakeSettings 45 import com.google.common.truth.Truth.assertThat 46 import kotlinx.coroutines.ExperimentalCoroutinesApi 47 import kotlinx.coroutines.test.TestScope 48 import kotlinx.coroutines.test.runCurrent 49 import kotlinx.coroutines.test.runTest 50 import org.junit.Before 51 import org.junit.Test 52 import org.junit.runner.RunWith 53 import org.mockito.ArgumentCaptor 54 import org.mockito.Mockito.verify 55 import org.mockito.Mockito.verifyNoMoreInteractions 56 57 @OptIn(ExperimentalCoroutinesApi::class) 58 @SmallTest 59 @RunWith(AndroidJUnit4::class) 60 class StickyKeysIndicatorViewModelTest : SysuiTestCase() { 61 62 private val kosmos = testKosmos() 63 private val dispatcher = kosmos.testDispatcher 64 private val testScope = kosmos.testScope 65 private lateinit var viewModel: StickyKeysIndicatorViewModel 66 private val inputManager = mock<InputManager>() 67 private val keyboardRepository = FakeKeyboardRepository() 68 private val secureSettings = kosmos.fakeSettings 69 private val userRepository = Kosmos().fakeUserRepository 70 private val captor = 71 ArgumentCaptor.forClass(InputManager.StickyModifierStateListener::class.java) 72 73 @Before setupnull74 fun setup() { 75 val settingsRepository = kosmos.userAwareSecureSettingsRepository 76 val stickyKeysRepository = 77 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 verifyNoMoreInteractions(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(ALT to false, META to false, SHIFT to false)) 186 187 assertThat(stickyKeys) 188 .isEqualTo( 189 mapOf(ALT to Locked(false), META to Locked(false), SHIFT to Locked(false)) 190 ) 191 } 192 } 193 194 @Test showsOnlyLockedStateIfKeyIsStickyAndLockednull195 fun showsOnlyLockedStateIfKeyIsStickyAndLocked() { 196 testScope.runTest { 197 val stickyKeys by collectLastValue(viewModel.indicatorContent) 198 setStickyKeysActive() 199 200 setStickyKeys(mapOf(ALT to false, ALT to true)) 201 202 assertThat(stickyKeys).isEqualTo(mapOf(ALT to Locked(true))) 203 } 204 } 205 206 @Test doesNotChangeOrderOfKeysIfTheyBecomeLockednull207 fun doesNotChangeOrderOfKeysIfTheyBecomeLocked() { 208 testScope.runTest { 209 val stickyKeys by collectLastValue(viewModel.indicatorContent) 210 setStickyKeysActive() 211 212 setStickyKeys( 213 mapOf( 214 META to false, 215 SHIFT to false, // shift is sticky but not locked 216 CTRL to false, 217 ) 218 ) 219 val previousShiftIndex = stickyKeys?.toList()?.indexOf(SHIFT to Locked(false)) 220 221 setStickyKeys( 222 mapOf( 223 SHIFT to false, 224 SHIFT to true, // shift is now locked 225 META to false, 226 CTRL to false, 227 ) 228 ) 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 } 250 <lambda>null251 private fun isLocked(key: ModifierKey) = keys.any { it.key == key && it.value } 252 isAltGrModifierLockednull253 override fun isAltGrModifierLocked() = isLocked(ALT_GR) 254 255 override fun isAltGrModifierOn() = isOn(ALT_GR) 256 257 override fun isAltModifierLocked() = isLocked(ALT) 258 259 override fun isAltModifierOn() = isOn(ALT) 260 261 override fun isCtrlModifierLocked() = isLocked(CTRL) 262 263 override fun isCtrlModifierOn() = isOn(CTRL) 264 265 override fun isMetaModifierLocked() = isLocked(META) 266 267 override fun isMetaModifierOn() = isOn(META) 268 269 override fun isShiftModifierLocked() = isLocked(SHIFT) 270 271 override fun isShiftModifierOn() = isOn(SHIFT) 272 } 273 } 274