1 /*
2 * Copyright (C) 2022 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.server.input
18
19 import android.content.Context
20 import android.content.ContextWrapper
21 import android.hardware.input.IInputManager
22 import android.hardware.input.InputManager
23 import android.hardware.input.InputManagerGlobal
24 import android.os.test.TestLooper
25 import android.platform.test.annotations.Presubmit
26 import android.provider.Settings
27 import android.view.InputDevice
28 import android.view.KeyEvent
29 import androidx.test.core.app.ApplicationProvider
30 import org.junit.After
31 import org.junit.Assert.assertEquals
32 import org.junit.Before
33 import org.junit.Rule
34 import org.junit.Test
35 import org.mockito.Mock
36 import org.mockito.Mockito
37 import org.mockito.junit.MockitoJUnit
38 import java.io.FileNotFoundException
39 import java.io.FileOutputStream
40 import java.io.IOException
41 import java.io.InputStream
42
createKeyboardnull43 private fun createKeyboard(deviceId: Int): InputDevice =
44 InputDevice.Builder()
45 .setId(deviceId)
46 .setName("Device $deviceId")
47 .setDescriptor("descriptor $deviceId")
48 .setSources(InputDevice.SOURCE_KEYBOARD)
49 .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC)
50 .setExternal(true)
51 .build()
52
53 /**
54 * Tests for {@link KeyRemapper}.
55 *
56 * Build/Install/Run:
57 * atest InputTests:KeyRemapperTests
58 */
59 @Presubmit
60 class KeyRemapperTests {
61
62 companion object {
63 const val DEVICE_ID = 1
64 val REMAPPABLE_KEYS = intArrayOf(
65 KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_CTRL_RIGHT,
66 KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_META_RIGHT,
67 KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.KEYCODE_ALT_RIGHT,
68 KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SHIFT_RIGHT,
69 KeyEvent.KEYCODE_CAPS_LOCK
70 )
71 }
72
73 @get:Rule
74 val rule = MockitoJUnit.rule()!!
75
76 @Mock
77 private lateinit var iInputManager: IInputManager
78 @Mock
79 private lateinit var native: NativeInputManagerService
80 private lateinit var mKeyRemapper: KeyRemapper
81 private lateinit var context: Context
82 private lateinit var dataStore: PersistentDataStore
83 private lateinit var testLooper: TestLooper
84 private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession
85
86 @Before
87 fun setup() {
88 context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext()))
89 dataStore = PersistentDataStore(object : PersistentDataStore.Injector() {
90 override fun openRead(): InputStream? {
91 throw FileNotFoundException()
92 }
93
94 override fun startWrite(): FileOutputStream? {
95 throw IOException()
96 }
97
98 override fun finishWrite(fos: FileOutputStream?, success: Boolean) {}
99 })
100 testLooper = TestLooper()
101 mKeyRemapper = KeyRemapper(
102 context,
103 native,
104 dataStore,
105 testLooper.looper
106 )
107 inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager)
108 val inputManager = InputManager(context)
109 Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE)))
110 .thenReturn(inputManager)
111 Mockito.`when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID))
112 }
113
114 @After
115 fun tearDown() {
116 if (this::inputManagerGlobalSession.isInitialized) {
117 inputManagerGlobalSession.close()
118 }
119 }
120
121 @Test
122 fun testKeyRemapping_whenRemappingEnabled() {
123 ModifierRemappingFlag(true).use {
124 val keyboard = createKeyboard(DEVICE_ID)
125 Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboard)
126
127 for (i in REMAPPABLE_KEYS.indices) {
128 val fromKeyCode = REMAPPABLE_KEYS[i]
129 val toKeyCode = REMAPPABLE_KEYS[(i + 1) % REMAPPABLE_KEYS.size]
130 mKeyRemapper.remapKey(fromKeyCode, toKeyCode)
131 testLooper.dispatchNext()
132 }
133
134 val remapping = mKeyRemapper.keyRemapping
135 val expectedSize = REMAPPABLE_KEYS.size
136 assertEquals("Remapping size should be $expectedSize", expectedSize, remapping.size)
137
138 for (i in REMAPPABLE_KEYS.indices) {
139 val fromKeyCode = REMAPPABLE_KEYS[i]
140 val toKeyCode = REMAPPABLE_KEYS[(i + 1) % REMAPPABLE_KEYS.size]
141 assertEquals(
142 "Remapping should include mapping from $fromKeyCode to $toKeyCode",
143 toKeyCode,
144 remapping.getOrDefault(fromKeyCode, -1)
145 )
146 }
147
148 mKeyRemapper.clearAllKeyRemappings()
149 testLooper.dispatchNext()
150
151 assertEquals(
152 "Remapping size should be 0 after clearAllModifierKeyRemappings",
153 0,
154 mKeyRemapper.keyRemapping.size
155 )
156 }
157 }
158
159 @Test
160 fun testKeyRemapping_whenRemappingDisabled() {
161 ModifierRemappingFlag(false).use {
162 val keyboard = createKeyboard(DEVICE_ID)
163 Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboard)
164
165 mKeyRemapper.remapKey(REMAPPABLE_KEYS[0], REMAPPABLE_KEYS[1])
166 testLooper.dispatchAll()
167
168 val remapping = mKeyRemapper.keyRemapping
169 assertEquals(
170 "Remapping should not be done if modifier key remapping is disabled",
171 0,
172 remapping.size
173 )
174 }
175 }
176
177 private inner class ModifierRemappingFlag constructor(enabled: Boolean) : AutoCloseable {
178 init {
179 Settings.Global.putString(
180 context.contentResolver,
181 "settings_new_keyboard_modifier_key", enabled.toString()
182 )
183 }
184
185 override fun close() {
186 Settings.Global.putString(
187 context.contentResolver,
188 "settings_new_keyboard_modifier_key",
189 ""
190 )
191 }
192 }
193 }