• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 com.android.cts.input;
18 
19 import static android.os.FileUtils.closeQuietly;
20 
21 import android.app.Instrumentation;
22 import android.app.UiAutomation;
23 import android.hardware.input.InputManager;
24 import android.os.Handler;
25 import android.os.HandlerThread;
26 import android.os.ParcelFileDescriptor;
27 import android.util.JsonReader;
28 import android.util.JsonToken;
29 import android.util.Log;
30 import android.view.InputDevice;
31 
32 import org.json.JSONException;
33 import org.json.JSONObject;
34 
35 import java.io.IOException;
36 import java.io.InputStream;
37 import java.io.InputStreamReader;
38 import java.io.OutputStream;
39 import java.io.UnsupportedEncodingException;
40 import java.util.ArrayList;
41 import java.util.concurrent.CountDownLatch;
42 import java.util.concurrent.TimeUnit;
43 
44 /**
45  * Declares a virtual INPUT device registered through /dev/uinput or /dev/hid.
46  */
47 public abstract class VirtualInputDevice implements
48         InputManager.InputDeviceListener, AutoCloseable {
49     private static final String TAG = "VirtualInputDevice";
50     private InputStream mInputStream;
51     private OutputStream mOutputStream;
52     private Instrumentation mInstrumentation;
53     private final Thread mResultThread;
54     private final HandlerThread mHandlerThread;
55     private final Handler mHandler;
56     private final InputManager mInputManager;
57     private volatile CountDownLatch mDeviceAddedSignal; // to wait for onInputDeviceAdded signal
58     private volatile CountDownLatch mDeviceRemovedSignal; // to wait for onInputDeviceRemoved signal
59     // Input device ID assigned by input manager
60     private int mDeviceId = Integer.MIN_VALUE;
61     private final int mVendorId;
62     private final int mProductId;
63     private final int mSources;
64     // Virtual device ID from the json file
65     protected final int mId;
66     protected JsonReader mReader;
67     protected final Object mLock = new Object();
68 
69     /**
70      * To be implemented with device specific shell command to execute.
71      */
getShellCommand()72     abstract String getShellCommand();
73 
74     /**
75      * To be implemented with device specific result reading function.
76      */
readResults()77     abstract void readResults();
78 
VirtualInputDevice(Instrumentation instrumentation, int id, int vendorId, int productId, int sources, String registerCommand)79     public VirtualInputDevice(Instrumentation instrumentation, int id, int vendorId, int productId,
80             int sources, String registerCommand) {
81         mInstrumentation = instrumentation;
82         mInputManager = mInstrumentation.getContext().getSystemService(InputManager.class);
83         setupPipes();
84 
85         mId = id;
86         mVendorId = vendorId;
87         mProductId = productId;
88         mSources = sources;
89         mHandlerThread = new HandlerThread("InputDeviceHandlerThread");
90         mHandlerThread.start();
91         mHandler = new Handler(mHandlerThread.getLooper());
92 
93         mDeviceAddedSignal = new CountDownLatch(1);
94         mDeviceRemovedSignal = new CountDownLatch(1);
95 
96         mResultThread = new Thread(() -> {
97             try {
98                 while (mReader.peek() != JsonToken.END_DOCUMENT) {
99                     readResults();
100                 }
101             } catch (IOException ex) {
102                 Log.w(TAG, "Exiting JSON Result reader. " + ex);
103             }
104         });
105         // Start result reader thread
106         mResultThread.start();
107         // Register input device listener
108         mInputManager.registerInputDeviceListener(VirtualInputDevice.this, mHandler);
109         // Register virtual input device
110         registerInputDevice(registerCommand);
111     }
112 
readData()113     protected byte[] readData() throws IOException {
114         ArrayList<Integer> data = new ArrayList<Integer>();
115         try {
116             mReader.beginArray();
117             while (mReader.hasNext()) {
118                 data.add(Integer.decode(mReader.nextString()));
119             }
120             mReader.endArray();
121         } catch (IllegalStateException | NumberFormatException e) {
122             mReader.endArray();
123             throw new IllegalStateException("Encountered malformed data.", e);
124         }
125         byte[] rawData = new byte[data.size()];
126         for (int i = 0; i < data.size(); i++) {
127             int d = data.get(i);
128             if ((d & 0xFF) != d) {
129                 throw new IllegalStateException("Invalid data, all values must be byte-sized");
130             }
131             rawData[i] = (byte) d;
132         }
133         return rawData;
134     }
135 
136     /**
137      * Register an input device. May cause a failure if the device added notification
138      * is not received within the timeout period
139      *
140      * @param registerCommand The full json command that specifies how to register this device
141      */
registerInputDevice(String registerCommand)142     private void registerInputDevice(String registerCommand) {
143         Log.i(TAG, "registerInputDevice: " + registerCommand);
144         writeCommands(registerCommand.getBytes());
145         try {
146             // Wait for input device added callback.
147             mDeviceAddedSignal.await(20L, TimeUnit.SECONDS);
148             if (mDeviceAddedSignal.getCount() != 0) {
149                 throw new RuntimeException("Did not receive device added notification in time");
150             }
151         } catch (InterruptedException ex) {
152             throw new RuntimeException(
153                     "Unexpectedly interrupted while waiting for device added notification.");
154         }
155     }
156 
157     /**
158      * Add a delay between processing events.
159      *
160      * @param milliSeconds The delay in milliseconds.
161      */
delay(int milliSeconds)162     public void delay(int milliSeconds) {
163         JSONObject json = new JSONObject();
164         try {
165             json.put("command", "delay");
166             json.put("id", mId);
167             json.put("duration", milliSeconds);
168         } catch (JSONException e) {
169             throw new RuntimeException(
170                     "Could not create JSON object to delay " + milliSeconds + " milliseconds");
171         }
172         writeCommands(json.toString().getBytes());
173     }
174 
175     /**
176      * Close the device, which would cause the associated input device to unregister.
177      */
178     @Override
close()179     public void close() {
180         closeQuietly(mInputStream);
181         closeQuietly(mOutputStream);
182         // mResultThread should exit when stream is closed.
183         try {
184             // Wait for input device removed callback.
185             mDeviceRemovedSignal.await(20L, TimeUnit.SECONDS);
186             if (mDeviceRemovedSignal.getCount() != 0) {
187                 throw new RuntimeException("Did not receive device removed notification in time");
188             }
189         } catch (InterruptedException ex) {
190             throw new RuntimeException(
191                     "Unexpectedly interrupted while waiting for device removed notification.");
192         }
193         // Unregister input device listener
194         mInstrumentation.runOnMainSync(() -> {
195             mInputManager.unregisterInputDeviceListener(VirtualInputDevice.this);
196         });
197     }
198 
getDeviceId()199     public int getDeviceId() {
200         return mDeviceId;
201     }
202 
getRegisterCommandDeviceId()203     public int getRegisterCommandDeviceId() {
204         return mId;
205     }
206 
getVendorId()207     public int getVendorId() {
208         return mVendorId;
209     }
210 
getProductId()211     public int getProductId() {
212         return mProductId;
213     }
214 
setupPipes()215     private void setupPipes() {
216         UiAutomation ui = mInstrumentation.getUiAutomation();
217         ParcelFileDescriptor[] pipes = ui.executeShellCommandRw(getShellCommand());
218 
219         mInputStream = new ParcelFileDescriptor.AutoCloseInputStream(pipes[0]);
220         mOutputStream = new ParcelFileDescriptor.AutoCloseOutputStream(pipes[1]);
221         try {
222             mReader = new JsonReader(new InputStreamReader(mInputStream, "UTF-8"));
223         } catch (UnsupportedEncodingException e) {
224             throw new RuntimeException(e);
225         }
226         mReader.setLenient(true);
227     }
228 
writeCommands(byte[] bytes)229     protected void writeCommands(byte[] bytes) {
230         try {
231             mOutputStream.write(bytes);
232             mOutputStream.flush();
233         } catch (IOException e) {
234             throw new RuntimeException(e);
235         }
236     }
237 
updateInputDevice(int deviceId)238     private void updateInputDevice(int deviceId) {
239         InputDevice device = mInputManager.getInputDevice(deviceId);
240         if (device == null) {
241             return;
242         }
243         // Check if the device is what we expected
244         if (device.getVendorId() == mVendorId && device.getProductId() == mProductId) {
245             if ((device.getSources() & mSources) == mSources) {
246                 mDeviceId = device.getId();
247                 mDeviceAddedSignal.countDown();
248             } else {
249                 Log.i(TAG, "Mismatching sources for " + device);
250             }
251         } else {
252             Log.w(TAG, "Unexpected input device: " + device);
253         }
254     }
255 
256     // InputManager.InputDeviceListener functions
257     @Override
onInputDeviceAdded(int deviceId)258     public void onInputDeviceAdded(int deviceId) {
259         // Check the new added input device
260         updateInputDevice(deviceId);
261     }
262 
263     @Override
onInputDeviceChanged(int deviceId)264     public void onInputDeviceChanged(int deviceId) {
265         // InputDevice may be updated with new input sources added
266         updateInputDevice(deviceId);
267     }
268 
269     @Override
onInputDeviceRemoved(int deviceId)270     public void onInputDeviceRemoved(int deviceId) {
271         if (deviceId == mDeviceId) {
272             mDeviceRemovedSignal.countDown();
273         }
274     }
275 }
276