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