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