1 /* 2 * Copyright (C) 2017 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.googlecode.android_scripting.facade.bluetooth; 18 19 import android.app.Service; 20 import android.bluetooth.BluetoothAdapter; 21 import android.bluetooth.BluetoothDevice; 22 import android.bluetooth.BluetoothHidDevice; 23 import android.bluetooth.BluetoothHidDeviceAppQosSettings; 24 import android.bluetooth.BluetoothHidDeviceAppSdpSettings; 25 import android.bluetooth.BluetoothProfile; 26 import android.bluetooth.BluetoothUuid; 27 import android.os.Bundle; 28 import android.os.Handler; 29 import android.os.HandlerThread; 30 import android.os.ParcelUuid; 31 32 import com.googlecode.android_scripting.Log; 33 import com.googlecode.android_scripting.facade.EventFacade; 34 import com.googlecode.android_scripting.facade.FacadeManager; 35 import com.googlecode.android_scripting.jsonrpc.RpcReceiver; 36 import com.googlecode.android_scripting.rpc.Rpc; 37 import com.googlecode.android_scripting.rpc.RpcParameter; 38 39 import java.util.List; 40 41 public class BluetoothHidDeviceFacade extends RpcReceiver { 42 43 public static final ParcelUuid[] UUIDS = {BluetoothUuid.Hid}; 44 45 public static final byte ID_KEYBOARD = 1; 46 public static final byte ID_MOUSE = 2; 47 48 public static final byte[] HIDD_REPORT_DESC = { 49 (byte) 0x05, 50 (byte) 0x01, // Usage page (Generic Desktop) 51 (byte) 0x09, 52 (byte) 0x06, // Usage (Keyboard) 53 (byte) 0xA1, 54 (byte) 0x01, // Collection (Application) 55 (byte) 0x85, 56 ID_KEYBOARD, // Report ID 57 (byte) 0x05, 58 (byte) 0x07, // Usage page (Key Codes) 59 (byte) 0x19, 60 (byte) 0xE0, // Usage minimum (224) 61 (byte) 0x29, 62 (byte) 0xE7, // Usage maximum (231) 63 (byte) 0x15, 64 (byte) 0x00, // Logical minimum (0) 65 (byte) 0x25, 66 (byte) 0x01, // Logical maximum (1) 67 (byte) 0x75, 68 (byte) 0x01, // Report size (1) 69 (byte) 0x95, 70 (byte) 0x08, // Report count (8) 71 (byte) 0x81, 72 (byte) 0x02, // Input (Data, Variable, Absolute) ; Modifier byte 73 (byte) 0x75, 74 (byte) 0x08, // Report size (8) 75 (byte) 0x95, 76 (byte) 0x01, // Report count (1) 77 (byte) 0x81, 78 (byte) 0x01, // Input (Constant) ; Reserved byte 79 (byte) 0x75, 80 (byte) 0x08, // Report size (8) 81 (byte) 0x95, 82 (byte) 0x06, // Report count (6) 83 (byte) 0x15, 84 (byte) 0x00, // Logical Minimum (0) 85 (byte) 0x25, 86 (byte) 0x65, // Logical Maximum (101) 87 (byte) 0x05, 88 (byte) 0x07, // Usage page (Key Codes) 89 (byte) 0x19, 90 (byte) 0x00, // Usage Minimum (0) 91 (byte) 0x29, 92 (byte) 0x65, // Usage Maximum (101) 93 (byte) 0x81, 94 (byte) 0x00, // Input (Data, Array) ; Key array (6 keys) 95 (byte) 0xC0, // End Collection 96 (byte) 0x05, 97 (byte) 0x01, // Usage Page (Generic Desktop) 98 (byte) 0x09, 99 (byte) 0x02, // Usage (Mouse) 100 (byte) 0xA1, 101 (byte) 0x01, // Collection (Application) 102 (byte) 0x85, 103 ID_MOUSE, // Report ID 104 (byte) 0x09, 105 (byte) 0x01, // Usage (Pointer) 106 (byte) 0xA1, 107 (byte) 0x00, // Collection (Physical) 108 (byte) 0x05, 109 (byte) 0x09, // Usage Page (Buttons) 110 (byte) 0x19, 111 (byte) 0x01, // Usage minimum (1) 112 (byte) 0x29, 113 (byte) 0x03, // Usage maximum (3) 114 (byte) 0x15, 115 (byte) 0x00, // Logical minimum (0) 116 (byte) 0x25, 117 (byte) 0x01, // Logical maximum (1) 118 (byte) 0x75, 119 (byte) 0x01, // Report size (1) 120 (byte) 0x95, 121 (byte) 0x03, // Report count (3) 122 (byte) 0x81, 123 (byte) 0x02, // Input (Data, Variable, Absolute) 124 (byte) 0x75, 125 (byte) 0x05, // Report size (5) 126 (byte) 0x95, 127 (byte) 0x01, // Report count (1) 128 (byte) 0x81, 129 (byte) 0x01, // Input (constant) ; 5 bit padding 130 (byte) 0x05, 131 (byte) 0x01, // Usage page (Generic Desktop) 132 (byte) 0x09, 133 (byte) 0x30, // Usage (X) 134 (byte) 0x09, 135 (byte) 0x31, // Usage (Y) 136 (byte) 0x09, 137 (byte) 0x38, // Usage (Wheel) 138 (byte) 0x15, 139 (byte) 0x81, // Logical minimum (-127) 140 (byte) 0x25, 141 (byte) 0x7F, // Logical maximum (127) 142 (byte) 0x75, 143 (byte) 0x08, // Report size (8) 144 (byte) 0x95, 145 (byte) 0x03, // Report count (3) 146 (byte) 0x81, 147 (byte) 0x06, // Input (Data, Variable, Relative) 148 (byte) 0xC0, // End Collection 149 (byte) 0xC0 // End Collection 150 }; 151 152 // HID mouse movement 153 private static final byte[] RIGHT = {0, 1, 0, 0}; 154 private static final byte[] DOWN = {0, 0, -1, 0}; 155 private static final byte[] LEFT = {0, -1, 0, 0}; 156 private static final byte[] UP = {0, 0, 1, 0}; 157 158 // Default values. 159 private static final int QOS_TOKEN_RATE = 800; // 9 bytes * 1000000 us / 11250 us 160 private static final int QOS_TOKEN_BUCKET_SIZE = 9; 161 private static final int QOS_PEAK_BANDWIDTH = 0; 162 private static final int QOS_LATENCY = 11250; 163 164 private final Service mService; 165 private final BluetoothAdapter mBluetoothAdapter; 166 private final EventFacade mEventFacade; 167 168 private static boolean sIsHidDeviceReady = false; 169 private static BluetoothHidDevice sHidDeviceProfile = null; 170 private boolean mKeepMoving = false; 171 172 private final HandlerThread mHandlerThread; 173 private final Handler mHandler; 174 175 private BluetoothHidDevice.Callback mCallback = new BluetoothHidDevice.Callback() { 176 @Override 177 public void onAppStatusChanged(BluetoothDevice pluggedDevice, boolean registered) { 178 Log.d("onAppStatusChanged: pluggedDevice=" + pluggedDevice + " registered=" 179 + registered); 180 Bundle result = new Bundle(); 181 result.putBoolean("registered", registered); 182 mEventFacade.postEvent("onAppStatusChanged", result); 183 } 184 185 @Override 186 public void onConnectionStateChanged(BluetoothDevice device, int state) { 187 Log.d("onConnectionStateChanged: device=" + device + " state=" + state); 188 Bundle result = new Bundle(); 189 result.putInt("state", state); 190 mEventFacade.postEvent("onConnectionStateChanged", result); 191 } 192 193 @Override 194 public void onGetReport(BluetoothDevice device, byte type, byte id, int bufferSize) { 195 Log.d("onGetReport: device=" + device + " type=" + type + " id=" + id + " bufferSize=" 196 + bufferSize); 197 Bundle result = new Bundle(); 198 result.putByte("type", type); 199 result.putByte("id", id); 200 result.putInt("bufferSize", bufferSize); 201 mEventFacade.postEvent("onGetReport", result); 202 } 203 204 @Override 205 public void onSetReport(BluetoothDevice device, byte type, byte id, byte[] data) { 206 Log.d("onSetReport: device=" + device + " type=" + type + " id=" + id); 207 Bundle result = new Bundle(); 208 result.putByte("type", type); 209 result.putByte("id", id); 210 result.putByteArray("data", data); 211 mEventFacade.postEvent("onSetReport", result); 212 } 213 214 @Override 215 public void onSetProtocol(BluetoothDevice device, byte protocol) { 216 Log.d("onSetProtocol: device=" + device + " protocol=" + protocol); 217 Bundle result = new Bundle(); 218 result.putByte("protocol", protocol); 219 mEventFacade.postEvent("onSetProtocol", result); 220 } 221 222 @Override 223 public void onInterruptData(BluetoothDevice device, byte reportId, byte[] data) { 224 Log.d("onInterruptData: device=" + device + " reportId=" + reportId); 225 Bundle result = new Bundle(); 226 result.putByte("registered", reportId); 227 result.putByteArray("data", data); 228 mEventFacade.postEvent("onInterruptData", result); 229 } 230 231 @Override 232 public void onVirtualCableUnplug(BluetoothDevice device) { 233 Log.d("onVirtualCableUnplug: device=" + device); 234 Bundle result = new Bundle(); 235 mEventFacade.postEvent("onVirtualCableUnplug", result); 236 } 237 }; 238 239 private static BluetoothHidDeviceAppSdpSettings sSdpSettings = 240 new BluetoothHidDeviceAppSdpSettings("Mock App", "Mock", "Google", 241 BluetoothHidDevice.SUBCLASS1_COMBO, HIDD_REPORT_DESC); 242 243 private static BluetoothHidDeviceAppQosSettings sQos = 244 new BluetoothHidDeviceAppQosSettings( 245 BluetoothHidDeviceAppQosSettings.SERVICE_BEST_EFFORT, 246 QOS_TOKEN_RATE, 247 QOS_TOKEN_BUCKET_SIZE, 248 QOS_PEAK_BANDWIDTH, 249 QOS_LATENCY, 250 BluetoothHidDeviceAppQosSettings.MAX); 251 252 public BluetoothHidDeviceFacade(FacadeManager manager) { 253 super(manager); 254 mService = manager.getService(); 255 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 256 mBluetoothAdapter.getProfileProxy(mService, new HidDeviceServiceListener(), 257 BluetoothProfile.HID_DEVICE); 258 mEventFacade = manager.getReceiver(EventFacade.class); 259 mHandlerThread = new HandlerThread("BluetoothHidDeviceFacadeHandler", 260 android.os.Process.THREAD_PRIORITY_BACKGROUND); 261 mHandlerThread.start(); 262 mHandler = new Handler(mHandlerThread.getLooper()); 263 Log.w("Init HID Device Facade"); 264 } 265 266 class HidDeviceServiceListener implements BluetoothProfile.ServiceListener { 267 268 @Override 269 public void onServiceConnected(int profile, BluetoothProfile proxy) { 270 Log.d("BluetoothHidDeviceFacade: onServiceConnected"); 271 sHidDeviceProfile = (BluetoothHidDevice) proxy; 272 sIsHidDeviceReady = true; 273 if (proxy == null) { 274 Log.e("proxy is still null"); 275 } 276 } 277 278 @Override 279 public void onServiceDisconnected(int profile) { 280 sIsHidDeviceReady = false; 281 } 282 } 283 284 public Boolean hidDeviceConnect(BluetoothDevice device) { 285 return sHidDeviceProfile != null && sHidDeviceProfile.connect(device); 286 } 287 288 public Boolean hidDeviceDisconnect(BluetoothDevice device) { 289 return sHidDeviceProfile != null && sHidDeviceProfile.disconnect(device); 290 } 291 292 /** 293 * Check whether the HID Device profile service is ready to use. 294 * @return true if HID Device profile is ready to use; otherwise false 295 */ 296 @Rpc(description = "Is HID Device profile ready.") 297 public Boolean bluetoothHidDeviceIsReady() { 298 Log.d("isReady"); 299 return sHidDeviceProfile != null && sIsHidDeviceReady; 300 } 301 302 /** 303 * Connect to a Bluetooth HID input host. 304 * @param device name or MAC address or the HID input host 305 * @return true if successfully connected to the HID host; otherwise false 306 * @throws Exception error from Bluetooth HidDevService 307 */ 308 @Rpc(description = "Connect to an HID host.") 309 public Boolean bluetoothHidDeviceConnect( 310 @RpcParameter(name = "device", 311 description = "Name or MAC address of a bluetooth device.") 312 String device) 313 throws Exception { 314 if (sHidDeviceProfile == null) { 315 return false; 316 } 317 BluetoothDevice mDevice = 318 BluetoothFacade.getDevice(BluetoothFacade.DiscoveredDevices, device); 319 Log.d("Connecting to device " + mDevice.getAliasName()); 320 return hidDeviceConnect(mDevice); 321 } 322 323 /** 324 * Disconnect a Bluetooth HID input host. 325 * @param device name or MAC address or the HID input host 326 * @return true if successfully disconnected the HID host; otherwise false 327 * @throws Exception error from Bluetooth HidDevService 328 */ 329 @Rpc(description = "Disconnect an HID host.") 330 public Boolean bluetoothHidDeviceDisconnect( 331 @RpcParameter(name = "device", 332 description = "Name or MAC address of a device.") 333 String device) 334 throws Exception { 335 if (sHidDeviceProfile == null) { 336 return false; 337 } 338 Log.d("Connected devices: " + sHidDeviceProfile.getConnectedDevices()); 339 BluetoothDevice mDevice = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(), 340 device); 341 return hidDeviceDisconnect(mDevice); 342 } 343 344 /** 345 * Get all the devices connected through HID Device Service. 346 * @return a list of all the devices connected through HID Device Service, 347 * or null if the HID device profile is not ready. 348 */ 349 @Rpc(description = "Get all the devices connected through HID Device Service.") 350 public List<BluetoothDevice> bluetoothHidDeviceGetConnectedDevices() { 351 if (sHidDeviceProfile == null) { 352 return null; 353 } 354 return sHidDeviceProfile.getConnectedDevices(); 355 } 356 357 /** 358 * Get the connection status of the specified device 359 * @param deviceID name or MAC address or the HID input host 360 * @return the status of the device 361 */ 362 @Rpc(description = "Get the connection status of a device.") 363 public Integer bluetoothHidDeviceGetConnectionStatus( 364 @RpcParameter(name = "deviceID", 365 description = "Name or MAC address of a bluetooth device.") 366 String deviceID) { 367 if (sHidDeviceProfile == null) { 368 return BluetoothProfile.STATE_DISCONNECTED; 369 } 370 List<BluetoothDevice> deviceList = sHidDeviceProfile.getConnectedDevices(); 371 BluetoothDevice device; 372 try { 373 device = BluetoothFacade.getDevice(deviceList, deviceID); 374 } catch (Exception e) { 375 return BluetoothProfile.STATE_DISCONNECTED; 376 } 377 return sHidDeviceProfile.getConnectionState(device); 378 } 379 380 /** 381 * Register app for the HID Device service using default settings. This adds a SDP record. 382 * @return true if successfully registered the app; otherwise false 383 * @throws Exception error from Bluetooth HidDevService 384 */ 385 @Rpc(description = "Register app for the HID Device service using default settings.") 386 public Boolean bluetoothHidDeviceRegisterApp() throws Exception { 387 return sHidDeviceProfile != null 388 && sHidDeviceProfile.registerApp( 389 sSdpSettings, null, sQos, command -> command.run(), mCallback); 390 } 391 392 /** 393 * Unregister app for the HID Device service. 394 * 395 * @return true if successfully unregistered the app; otherwise false 396 * @throws Exception error from Bluetooth HidDevService 397 */ 398 @Rpc(description = "Unregister app.") 399 public Boolean bluetoothHidDeviceUnregisterApp() throws Exception { 400 return sHidDeviceProfile != null && sHidDeviceProfile.unregisterApp(); 401 } 402 403 /** 404 * Send a data report to a connected HID host using interrupt channel. 405 * @param deviceID name or MAC address or the HID input host 406 * @param id report Id, as defined in descriptor. Can be 0 in case Report Id are not defined in 407 * descriptor. 408 * @param report report data 409 * @return true if successfully sent the report; otherwise false 410 * @throws Exception error from Bluetooth HidDevService 411 */ 412 @Rpc(description = "Send report to a connected HID host using interrupt channel.") 413 public Boolean bluetoothHidDeviceSendReport( 414 @RpcParameter(name = "deviceID", 415 description = "Name or MAC address of a bluetooth device.") 416 String deviceID, 417 @RpcParameter(name = "descriptor", 418 description = "Descriptor of the report") 419 Integer id, 420 @RpcParameter(name = "report") 421 String report) throws Exception { 422 if (sHidDeviceProfile == null) { 423 return false; 424 } 425 426 BluetoothDevice device = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(), 427 deviceID); 428 byte[] reportByteArray = report.getBytes(); 429 return sHidDeviceProfile.sendReport(device, id, reportByteArray); 430 } 431 432 /** 433 * Send a report to the connected HID host as reply for GET_REPORT request from the HID host. 434 * @param deviceID name or MAC address or the HID input host 435 * @param type type of the report, as in request 436 * @param id id of the report, as in request 437 * @param report report data 438 * @return true if successfully sent the reply report; otherwise false 439 * @throws Exception error from Bluetooth HidDevService 440 */ 441 @Rpc(description = "Send reply report to a connected HID..") 442 public Boolean bluetoothHidDeviceReplyReport( 443 @RpcParameter(name = "deviceID", 444 description = "Name or MAC address of a bluetooth device.") 445 String deviceID, 446 @RpcParameter(name = "type", 447 description = "Type as in the report.") 448 Integer type, 449 @RpcParameter(name = "id", 450 description = "id as in the report.") 451 Integer id, 452 @RpcParameter(name = "report") 453 String report) throws Exception { 454 if (sHidDeviceProfile == null) { 455 return false; 456 } 457 458 BluetoothDevice device = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(), 459 deviceID); 460 byte[] reportByteArray = report.getBytes(); 461 return sHidDeviceProfile.replyReport( 462 device, (byte) (int) type, (byte) (int) id, reportByteArray); 463 } 464 465 /** 466 * Send error handshake message as reply for invalid SET_REPORT request from the HID host. 467 * @param deviceID name or MAC address or the HID input host 468 * @param error error byte 469 * @return true if successfully sent the error handshake message; otherwise false 470 * @throws Exception error from Bluetooth HidDevService 471 */ 472 @Rpc(description = "Send error handshake message to a connected HID host.") 473 public Boolean bluetoothHidDeviceReportError( 474 @RpcParameter(name = "deviceID", 475 description = "Name or MAC address of a bluetooth device.") 476 String deviceID, 477 @RpcParameter(name = "error", 478 description = "Error byte") 479 Integer error) throws Exception { 480 if (sHidDeviceProfile == null) { 481 return false; 482 } 483 484 BluetoothDevice device = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(), 485 deviceID); 486 return sHidDeviceProfile.reportError(device, (byte) (int) error); 487 } 488 489 /** 490 * Start to send HID mouse input to HID host continuously for given duration. 491 * @param deviceID name or MAC address for the HID input host 492 * @param duration time in millisecond to send HID report continuously 493 * @return true if successfully sent the error handshake message; otherwise false 494 */ 495 @Rpc(description = "Start to send HID report continuously") 496 public Boolean bluetoothHidDeviceMoveRepeatedly( 497 @RpcParameter(name = "deviceID", 498 description = "Name or MAC address of a bluetooth device.") 499 String deviceID, 500 @RpcParameter(name = "duration", 501 description = "duration") 502 Integer duration, 503 @RpcParameter(name = "interval", 504 description = "interval") 505 Integer interval) throws Exception { 506 if (sHidDeviceProfile == null || mKeepMoving) { 507 return false; 508 } 509 BluetoothDevice device = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(), 510 deviceID); 511 mHandler.post(new Runnable() { 512 final long mStopTime = System.currentTimeMillis() + duration; 513 private void sendAndWait(byte[] report) { 514 if (!mKeepMoving) { 515 return; 516 } 517 sHidDeviceProfile.sendReport(device, ID_MOUSE, report); 518 long endTime = System.currentTimeMillis() + interval; 519 while (mKeepMoving && endTime > System.currentTimeMillis()) { 520 //Busy waiting 521 if (mStopTime < System.currentTimeMillis()) { 522 mKeepMoving = false; 523 return; 524 } 525 } 526 } 527 public void run() { 528 mKeepMoving = true; 529 while (mKeepMoving && mStopTime > System.currentTimeMillis()) { 530 sendAndWait(RIGHT); 531 sendAndWait(DOWN); 532 sendAndWait(LEFT); 533 sendAndWait(UP); 534 } 535 mKeepMoving = false; 536 } 537 }); 538 return true; 539 } 540 541 /** 542 * Stop sending HID report to HID host 543 */ 544 @Rpc(description = "Stop sending HID report") 545 public void bluetoothHidDeviceStopMoving() { 546 mKeepMoving = false; 547 } 548 549 @Override 550 public void shutdown() { 551 Log.w("Quit handler thread"); 552 mHandlerThread.quit(); 553 } 554 555 } 556