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