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