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