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