1 /* 2 * Copyright 2015 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.hardware.input.cts.tests; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.fail; 21 22 import android.app.Instrumentation; 23 import android.app.UiAutomation; 24 import android.hardware.input.cts.InputCallback; 25 import android.hardware.input.cts.InputCtsActivity; 26 import android.os.ParcelFileDescriptor; 27 import android.os.SystemClock; 28 import android.support.test.InstrumentationRegistry; 29 import android.support.test.rule.ActivityTestRule; 30 import android.view.KeyEvent; 31 import android.view.MotionEvent; 32 33 import java.io.ByteArrayOutputStream; 34 import java.io.IOException; 35 import java.io.InputStream; 36 import java.io.OutputStream; 37 38 import java.util.concurrent.BlockingQueue; 39 import java.util.concurrent.CountDownLatch; 40 import java.util.concurrent.LinkedBlockingQueue; 41 import java.util.concurrent.TimeUnit; 42 43 import libcore.io.IoUtils; 44 45 import org.junit.After; 46 import org.junit.Before; 47 import org.junit.Rule; 48 49 public class InputTestCase { 50 // hid executable expects "-" argument to read from stdin instead of a file 51 private static final String HID_COMMAND = "hid -"; 52 private static final String[] KEY_ACTIONS = {"DOWN", "UP", "MULTIPLE"}; 53 54 private OutputStream mOutputStream; 55 56 private final BlockingQueue<KeyEvent> mKeys; 57 private final BlockingQueue<MotionEvent> mMotions; 58 private InputListener mInputListener; 59 60 private Instrumentation mInstrumentation; 61 62 private volatile CountDownLatch mDeviceAddedSignal; // to wait for onInputDeviceAdded signal 63 64 public InputTestCase() { 65 mKeys = new LinkedBlockingQueue<KeyEvent>(); 66 mMotions = new LinkedBlockingQueue<MotionEvent>(); 67 mInputListener = new InputListener(); 68 } 69 70 @Rule 71 public ActivityTestRule<InputCtsActivity> mActivityRule = 72 new ActivityTestRule<>(InputCtsActivity.class); 73 74 @Before 75 public void setUp() throws Exception { 76 clearKeys(); 77 clearMotions(); 78 mInstrumentation = InstrumentationRegistry.getInstrumentation(); 79 mActivityRule.getActivity().setInputCallback(mInputListener); 80 setupPipes(); 81 } 82 83 @After 84 public void tearDown() throws Exception { 85 IoUtils.closeQuietly(mOutputStream); 86 } 87 88 /** 89 * Register an input device. May cause a failure if the device added notification 90 * is not received within the timeout period 91 * 92 * @param resourceId The resource id from which to send the register command. 93 */ 94 public void registerInputDevice(int resourceId) { 95 mDeviceAddedSignal = new CountDownLatch(1); 96 sendHidCommands(resourceId); 97 try { 98 // Found that in kernel 3.10, the device registration takes a very long time 99 // The wait can be decreased to 2 seconds after kernel 3.10 is no longer supported 100 mDeviceAddedSignal.await(20L, TimeUnit.SECONDS); 101 if (mDeviceAddedSignal.getCount() != 0) { 102 fail("Device added notification was not received in time."); 103 } 104 } catch (InterruptedException ex) { 105 fail("Unexpectedly interrupted while waiting for device added notification."); 106 } 107 SystemClock.sleep(100); 108 } 109 110 /** 111 * Sends the HID commands designated by the given resource id. 112 * The commands must be in the format expected by the `hid` shell command. 113 * 114 * @param id The resource id from which to load the HID commands. This must be a "raw" 115 * resource. 116 */ 117 public void sendHidCommands(int id) { 118 try { 119 mOutputStream.write(getEvents(id).getBytes()); 120 mOutputStream.flush(); 121 } catch (IOException e) { 122 throw new RuntimeException(e); 123 } 124 } 125 126 /** 127 * Asserts that the application received a {@link android.view.KeyEvent} with the given action 128 * and keycode. 129 * 130 * If other KeyEvents are received by the application prior to the expected KeyEvent, or no 131 * KeyEvents are received within a reasonable amount of time, then this will throw an 132 * AssertionFailedError. 133 * 134 * @param action The action to expect on the next KeyEvent 135 * (e.g. {@link android.view.KeyEvent#ACTION_DOWN}). 136 * @param keyCode The expected key code of the next KeyEvent. 137 */ 138 public void assertReceivedKeyEvent(int action, int keyCode) { 139 KeyEvent k = waitForKey(); 140 if (k == null) { 141 fail("Timed out waiting for " + KeyEvent.keyCodeToString(keyCode) 142 + " with action " + KEY_ACTIONS[action]); 143 return; 144 } 145 assertEquals(action, k.getAction()); 146 assertEquals(keyCode, k.getKeyCode()); 147 } 148 149 /** 150 * Asserts that no more events have been received by the application. 151 * 152 * If any more events have been received by the application, this throws an 153 * AssertionFailedError. 154 */ 155 public void assertNoMoreEvents() { 156 KeyEvent key; 157 MotionEvent motion; 158 if ((key = mKeys.poll()) != null) { 159 fail("Extraneous key events generated: " + key); 160 } 161 if ((motion = mMotions.poll()) != null) { 162 fail("Extraneous motion events generated: " + motion); 163 } 164 } 165 166 private KeyEvent waitForKey() { 167 try { 168 return mKeys.poll(1, TimeUnit.SECONDS); 169 } catch (InterruptedException e) { 170 return null; 171 } 172 } 173 174 private void clearKeys() { 175 mKeys.clear(); 176 } 177 178 private void clearMotions() { 179 mMotions.clear(); 180 } 181 182 private void setupPipes() throws IOException { 183 UiAutomation ui = mInstrumentation.getUiAutomation(); 184 ParcelFileDescriptor[] pipes = ui.executeShellCommandRw(HID_COMMAND); 185 186 mOutputStream = new ParcelFileDescriptor.AutoCloseOutputStream(pipes[1]); 187 IoUtils.closeQuietly(pipes[0]); // hid command is write-only 188 } 189 190 private String getEvents(int id) throws IOException { 191 InputStream is = 192 mInstrumentation.getTargetContext().getResources().openRawResource(id); 193 return readFully(is); 194 } 195 196 private static String readFully(InputStream is) throws IOException { 197 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 198 int read = 0; 199 byte[] buffer = new byte[1024]; 200 while ((read = is.read(buffer)) >= 0) { 201 baos.write(buffer, 0, read); 202 } 203 return baos.toString(); 204 } 205 206 private class InputListener implements InputCallback { 207 @Override 208 public void onKeyEvent(KeyEvent ev) { 209 boolean done = false; 210 do { 211 try { 212 mKeys.put(new KeyEvent(ev)); 213 done = true; 214 } catch (InterruptedException ignore) { } 215 } while (!done); 216 } 217 218 @Override 219 public void onMotionEvent(MotionEvent ev) { 220 boolean done = false; 221 do { 222 try { 223 mMotions.put(MotionEvent.obtain(ev)); 224 done = true; 225 } catch (InterruptedException ignore) { } 226 } while (!done); 227 } 228 229 @Override 230 public void onInputDeviceAdded(int deviceId) { 231 mDeviceAddedSignal.countDown(); 232 } 233 234 @Override 235 public void onInputDeviceRemoved(int deviceId) { 236 } 237 238 @Override 239 public void onInputDeviceChanged(int deviceId) { 240 } 241 } 242 243 244 } 245