1 /* 2 * Copyright (C) 2020 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 android.view.cts.input; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertNotNull; 21 22 import android.app.Instrumentation; 23 import android.content.Context; 24 import android.content.pm.PackageManager; 25 import android.content.res.Resources; 26 import android.view.InputDevice; 27 import android.view.KeyEvent; 28 import android.view.WindowManager; 29 import android.view.cts.R; 30 31 import androidx.test.ext.junit.runners.AndroidJUnit4; 32 import androidx.test.filters.MediumTest; 33 import androidx.test.platform.app.InstrumentationRegistry; 34 import androidx.test.rule.ActivityTestRule; 35 36 import com.android.compatibility.common.util.WindowUtil; 37 import com.android.cts.input.InputJsonParser; 38 import com.android.cts.input.UinputDevice; 39 40 import org.json.JSONArray; 41 import org.json.JSONException; 42 import org.json.JSONObject; 43 import org.junit.After; 44 import org.junit.Before; 45 import org.junit.Rule; 46 import org.junit.Test; 47 import org.junit.runner.RunWith; 48 49 import java.util.Arrays; 50 import java.util.HashSet; 51 import java.util.Map; 52 import java.util.Set; 53 54 /** 55 * CTS test case for generic.kl key layout mapping. 56 * This test utilize uinput command line tool to create a test device, and configure the virtual 57 * device to have all keys need to be tested. The JSON format input for device configuration 58 * and EV_KEY injection will be created directly from this test for uinput command. 59 * Keep res/raw/Generic.kl in sync with framework/base/data/keyboards/Generic.kl, this file 60 * will be loaded and parsed in this test, looping through all key labels and the corresponding 61 * EV_KEY code, injecting the KEY_UP and KEY_DOWN event to uinput, then verify the KeyEvent 62 * delivered to test application view. Except meta control keys and special keys not delivered 63 * to apps, all key codes in generic.kl will be verified. 64 * 65 */ 66 @MediumTest 67 @RunWith(AndroidJUnit4.class) 68 public class InputDeviceKeyLayoutMapTest { 69 private static final String TAG = "InputDeviceKeyLayoutMapTest"; 70 private static final String LABEL_PREFIX = "KEYCODE_"; 71 private static final int DEVICE_ID = 1; 72 private static final int EV_SYN = 0; 73 private static final int EV_KEY = 1; 74 private static final int EV_KEY_DOWN = 1; 75 private static final int EV_KEY_UP = 0; 76 private static final int UI_SET_EVBIT = 100; 77 private static final int UI_SET_KEYBIT = 101; 78 private static final int GOOGLE_VENDOR_ID = 0x18d1; 79 private static final int GOOGLE_VIRTUAL_KEYBOARD_ID = 0x001f; 80 private static final int POLL_EVENT_TIMEOUT_SECONDS = 5; 81 82 private static final Set<String> EXCLUDED_KEYS = new HashSet<>(Arrays.asList( 83 // Meta control keys. 84 "META_LEFT", 85 "META_RIGHT", 86 // KeyEvents not delivered to apps. 87 "APP_SWITCH", 88 "ASSIST", 89 "BRIGHTNESS_DOWN", 90 "BRIGHTNESS_UP", 91 "HOME", 92 "KEYBOARD_BACKLIGHT_DOWN", 93 "KEYBOARD_BACKLIGHT_TOGGLE", 94 "KEYBOARD_BACKLIGHT_UP", 95 "MACRO_1", 96 "MACRO_2", 97 "MACRO_3", 98 "MACRO_4", 99 "MUTE", 100 "POWER", 101 "RECENT_APPS", 102 "SEARCH", 103 "SLEEP", 104 "SOFT_SLEEP", 105 "SYSRQ", 106 "WAKEUP", 107 "VOICE_ASSIST", 108 // Keys that cause the test activity to lose focus 109 "CALCULATOR", 110 "CALENDAR", 111 "CONTACTS", 112 "ENVELOPE", 113 "EXPLORER", 114 "MUSIC" 115 )); 116 117 private Map<String, Integer> mKeyLayout; 118 private Instrumentation mInstrumentation; 119 private UinputDevice mUinputDevice; 120 private InputJsonParser mParser; 121 private WindowManager mWindowManager; 122 private boolean mIsLeanback; 123 private boolean mVolumeKeysHandledInWindowManager; 124 nativeLoadKeyLayout(String genericKeyLayout)125 private static native Map<String, Integer> nativeLoadKeyLayout(String genericKeyLayout); 126 127 static { 128 System.loadLibrary("ctsview_jni"); 129 } 130 131 @Rule 132 public ActivityTestRule<InputDeviceKeyLayoutMapTestActivity> mActivityRule = 133 new ActivityTestRule<>(InputDeviceKeyLayoutMapTestActivity.class); 134 135 @Before setup()136 public void setup() { 137 mInstrumentation = InstrumentationRegistry.getInstrumentation(); 138 WindowUtil.waitForFocus(mActivityRule.getActivity()); 139 Context context = mInstrumentation.getTargetContext(); 140 mParser = new InputJsonParser(context); 141 mWindowManager = context.getSystemService(WindowManager.class); 142 mIsLeanback = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK); 143 mVolumeKeysHandledInWindowManager = context.getResources().getBoolean( 144 Resources.getSystem().getIdentifier("config_handleVolumeKeysInWindowManager", 145 "bool", "android")); 146 mKeyLayout = nativeLoadKeyLayout(mParser.readRegisterCommand(R.raw.Generic)); 147 mUinputDevice = new UinputDevice(mInstrumentation, DEVICE_ID, GOOGLE_VENDOR_ID, 148 GOOGLE_VIRTUAL_KEYBOARD_ID, InputDevice.SOURCE_KEYBOARD, 149 createDeviceRegisterCommand()); 150 } 151 152 @After tearDown()153 public void tearDown() { 154 if (mUinputDevice != null) { 155 mUinputDevice.close(); 156 } 157 } 158 159 /** 160 * Get a KeyEvent from event queue or timeout. 161 * 162 * @return KeyEvent delivered to test activity, null if timeout. 163 */ getKeyEvent()164 private KeyEvent getKeyEvent() { 165 return mActivityRule.getActivity().getKeyEvent(POLL_EVENT_TIMEOUT_SECONDS); 166 } 167 assertReceivedKeyEvent(int action, int keyCode)168 private void assertReceivedKeyEvent(int action, int keyCode) { 169 KeyEvent receivedKeyEvent = getKeyEvent(); 170 assertNotNull("Did not receive " + KeyEvent.keyCodeToString(keyCode), receivedKeyEvent); 171 assertEquals(action, receivedKeyEvent.getAction()); 172 assertEquals(keyCode, receivedKeyEvent.getKeyCode()); 173 } 174 175 /** 176 * Create the uinput device registration command, in JSON format of uinput commandline tool. 177 * Refer to {@link framework/base/cmds/uinput/README.md} 178 */ createDeviceRegisterCommand()179 private String createDeviceRegisterCommand() { 180 JSONObject json = new JSONObject(); 181 JSONArray arrayConfigs = new JSONArray(); 182 try { 183 json.put("id", DEVICE_ID); 184 json.put("type", "uinput"); 185 json.put("command", "register"); 186 json.put("name", "Virtual All Buttons Device (Test)"); 187 json.put("vid", GOOGLE_VENDOR_ID); 188 json.put("pid", GOOGLE_VIRTUAL_KEYBOARD_ID); 189 json.put("bus", "bluetooth"); 190 191 JSONObject jsonSetEvBit = new JSONObject(); 192 JSONArray arraySetEvBit = new JSONArray(); 193 arraySetEvBit.put(EV_KEY); 194 jsonSetEvBit.put("type", UI_SET_EVBIT); 195 jsonSetEvBit.put("data", arraySetEvBit); 196 arrayConfigs.put(jsonSetEvBit); 197 198 // Configure device have all keys from key layout map. 199 JSONArray arraySetKeyBit = new JSONArray(); 200 for (Map.Entry<String, Integer> entry : mKeyLayout.entrySet()) { 201 arraySetKeyBit.put(entry.getValue()); 202 } 203 JSONObject jsonSetKeyBit = new JSONObject(); 204 jsonSetKeyBit.put("type", UI_SET_KEYBIT); 205 jsonSetKeyBit.put("data", arraySetKeyBit); 206 arrayConfigs.put(jsonSetKeyBit); 207 json.put("configuration", arrayConfigs); 208 } catch (JSONException e) { 209 throw new RuntimeException( 210 "Could not create JSON object"); 211 } 212 213 return json.toString(); 214 } 215 216 /** 217 * Simulate pressing a key. 218 * @param evKeyCode The key scan code 219 */ pressKey(int evKeyCode)220 private void pressKey(int evKeyCode) { 221 int[] evCodesDown = new int[] { 222 EV_KEY, evKeyCode, EV_KEY_DOWN, 223 EV_SYN, 0, 0}; 224 mUinputDevice.injectEvents(Arrays.toString(evCodesDown)); 225 226 int[] evCodesUp = new int[] { 227 EV_KEY, evKeyCode, EV_KEY_UP, 228 EV_SYN, 0, 0 }; 229 mUinputDevice.injectEvents(Arrays.toString(evCodesUp)); 230 } 231 232 /** 233 * Whether one key code is a volume key code. 234 * @param keyCode The key code 235 */ isVolumeKey(int keyCode)236 private static boolean isVolumeKey(int keyCode) { 237 return keyCode == KeyEvent.KEYCODE_VOLUME_UP 238 || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN 239 || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE; 240 } 241 242 /** 243 * Whether one key code should be forwarded to apps. 244 * @param keyCode The key code 245 */ isForwardedToApps(int keyCode)246 private boolean isForwardedToApps(int keyCode) { 247 if (mWindowManager.isGlobalKey(keyCode)) { 248 return false; 249 } 250 if (isVolumeKey(keyCode) && (mIsLeanback || mVolumeKeysHandledInWindowManager)) { 251 return false; 252 } 253 return true; 254 } 255 256 @Test testLayoutKeyEvents()257 public void testLayoutKeyEvents() { 258 for (Map.Entry<String, Integer> entry : mKeyLayout.entrySet()) { 259 if (EXCLUDED_KEYS.contains(entry.getKey())) { 260 continue; 261 } 262 263 String label = LABEL_PREFIX + entry.getKey(); 264 final int evKey = entry.getValue(); 265 final int keyCode = KeyEvent.keyCodeFromString(label); 266 267 if (!isForwardedToApps(keyCode)) { 268 continue; 269 } 270 271 pressKey(evKey); 272 assertReceivedKeyEvent(KeyEvent.ACTION_DOWN, keyCode); 273 assertReceivedKeyEvent(KeyEvent.ACTION_UP, keyCode); 274 } 275 } 276 277 } 278