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