Home | History | Annotate | Download | only in tests
      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