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 }