/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.googlecode.android_scripting.facade.bluetooth; import android.app.Service; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHidDevice; import android.bluetooth.BluetoothHidDeviceAppQosSettings; import android.bluetooth.BluetoothHidDeviceAppSdpSettings; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.ParcelUuid; import com.googlecode.android_scripting.Log; import com.googlecode.android_scripting.facade.EventFacade; import com.googlecode.android_scripting.facade.FacadeManager; import com.googlecode.android_scripting.jsonrpc.RpcReceiver; import com.googlecode.android_scripting.rpc.Rpc; import com.googlecode.android_scripting.rpc.RpcParameter; import java.util.List; public class BluetoothHidDeviceFacade extends RpcReceiver { public static final ParcelUuid[] UUIDS = {BluetoothUuid.HID}; public static final byte ID_KEYBOARD = 1; public static final byte ID_MOUSE = 2; public static final byte[] HIDD_REPORT_DESC = { (byte) 0x05, (byte) 0x01, // Usage page (Generic Desktop) (byte) 0x09, (byte) 0x06, // Usage (Keyboard) (byte) 0xA1, (byte) 0x01, // Collection (Application) (byte) 0x85, ID_KEYBOARD, // Report ID (byte) 0x05, (byte) 0x07, // Usage page (Key Codes) (byte) 0x19, (byte) 0xE0, // Usage minimum (224) (byte) 0x29, (byte) 0xE7, // Usage maximum (231) (byte) 0x15, (byte) 0x00, // Logical minimum (0) (byte) 0x25, (byte) 0x01, // Logical maximum (1) (byte) 0x75, (byte) 0x01, // Report size (1) (byte) 0x95, (byte) 0x08, // Report count (8) (byte) 0x81, (byte) 0x02, // Input (Data, Variable, Absolute) ; Modifier byte (byte) 0x75, (byte) 0x08, // Report size (8) (byte) 0x95, (byte) 0x01, // Report count (1) (byte) 0x81, (byte) 0x01, // Input (Constant) ; Reserved byte (byte) 0x75, (byte) 0x08, // Report size (8) (byte) 0x95, (byte) 0x06, // Report count (6) (byte) 0x15, (byte) 0x00, // Logical Minimum (0) (byte) 0x25, (byte) 0x65, // Logical Maximum (101) (byte) 0x05, (byte) 0x07, // Usage page (Key Codes) (byte) 0x19, (byte) 0x00, // Usage Minimum (0) (byte) 0x29, (byte) 0x65, // Usage Maximum (101) (byte) 0x81, (byte) 0x00, // Input (Data, Array) ; Key array (6 keys) (byte) 0xC0, // End Collection (byte) 0x05, (byte) 0x01, // Usage Page (Generic Desktop) (byte) 0x09, (byte) 0x02, // Usage (Mouse) (byte) 0xA1, (byte) 0x01, // Collection (Application) (byte) 0x85, ID_MOUSE, // Report ID (byte) 0x09, (byte) 0x01, // Usage (Pointer) (byte) 0xA1, (byte) 0x00, // Collection (Physical) (byte) 0x05, (byte) 0x09, // Usage Page (Buttons) (byte) 0x19, (byte) 0x01, // Usage minimum (1) (byte) 0x29, (byte) 0x03, // Usage maximum (3) (byte) 0x15, (byte) 0x00, // Logical minimum (0) (byte) 0x25, (byte) 0x01, // Logical maximum (1) (byte) 0x75, (byte) 0x01, // Report size (1) (byte) 0x95, (byte) 0x03, // Report count (3) (byte) 0x81, (byte) 0x02, // Input (Data, Variable, Absolute) (byte) 0x75, (byte) 0x05, // Report size (5) (byte) 0x95, (byte) 0x01, // Report count (1) (byte) 0x81, (byte) 0x01, // Input (constant) ; 5 bit padding (byte) 0x05, (byte) 0x01, // Usage page (Generic Desktop) (byte) 0x09, (byte) 0x30, // Usage (X) (byte) 0x09, (byte) 0x31, // Usage (Y) (byte) 0x09, (byte) 0x38, // Usage (Wheel) (byte) 0x15, (byte) 0x81, // Logical minimum (-127) (byte) 0x25, (byte) 0x7F, // Logical maximum (127) (byte) 0x75, (byte) 0x08, // Report size (8) (byte) 0x95, (byte) 0x03, // Report count (3) (byte) 0x81, (byte) 0x06, // Input (Data, Variable, Relative) (byte) 0xC0, // End Collection (byte) 0xC0 // End Collection }; // HID mouse movement private static final byte[] RIGHT = {0, 1, 0, 0}; private static final byte[] DOWN = {0, 0, -1, 0}; private static final byte[] LEFT = {0, -1, 0, 0}; private static final byte[] UP = {0, 0, 1, 0}; // Default values. private static final int QOS_TOKEN_RATE = 800; // 9 bytes * 1000000 us / 11250 us private static final int QOS_TOKEN_BUCKET_SIZE = 9; private static final int QOS_PEAK_BANDWIDTH = 0; private static final int QOS_LATENCY = 11250; private final Service mService; private final BluetoothAdapter mBluetoothAdapter; private final EventFacade mEventFacade; private static boolean sIsHidDeviceReady = false; private static BluetoothHidDevice sHidDeviceProfile = null; private boolean mKeepMoving = false; private final HandlerThread mHandlerThread; private final Handler mHandler; private BluetoothHidDevice.Callback mCallback = new BluetoothHidDevice.Callback() { @Override public void onAppStatusChanged(BluetoothDevice pluggedDevice, boolean registered) { Log.d("onAppStatusChanged: pluggedDevice=" + pluggedDevice + " registered=" + registered); Bundle result = new Bundle(); result.putBoolean("registered", registered); mEventFacade.postEvent("onAppStatusChanged", result); } @Override public void onConnectionStateChanged(BluetoothDevice device, int state) { Log.d("onConnectionStateChanged: device=" + device + " state=" + state); Bundle result = new Bundle(); result.putInt("state", state); mEventFacade.postEvent("onConnectionStateChanged", result); } @Override public void onGetReport(BluetoothDevice device, byte type, byte id, int bufferSize) { Log.d("onGetReport: device=" + device + " type=" + type + " id=" + id + " bufferSize=" + bufferSize); Bundle result = new Bundle(); result.putByte("type", type); result.putByte("id", id); result.putInt("bufferSize", bufferSize); mEventFacade.postEvent("onGetReport", result); } @Override public void onSetReport(BluetoothDevice device, byte type, byte id, byte[] data) { Log.d("onSetReport: device=" + device + " type=" + type + " id=" + id); Bundle result = new Bundle(); result.putByte("type", type); result.putByte("id", id); result.putByteArray("data", data); mEventFacade.postEvent("onSetReport", result); } @Override public void onSetProtocol(BluetoothDevice device, byte protocol) { Log.d("onSetProtocol: device=" + device + " protocol=" + protocol); Bundle result = new Bundle(); result.putByte("protocol", protocol); mEventFacade.postEvent("onSetProtocol", result); } @Override public void onInterruptData(BluetoothDevice device, byte reportId, byte[] data) { Log.d("onInterruptData: device=" + device + " reportId=" + reportId); Bundle result = new Bundle(); result.putByte("registered", reportId); result.putByteArray("data", data); mEventFacade.postEvent("onInterruptData", result); } @Override public void onVirtualCableUnplug(BluetoothDevice device) { Log.d("onVirtualCableUnplug: device=" + device); Bundle result = new Bundle(); mEventFacade.postEvent("onVirtualCableUnplug", result); } }; private static BluetoothHidDeviceAppSdpSettings sSdpSettings = new BluetoothHidDeviceAppSdpSettings("Mock App", "Mock", "Google", BluetoothHidDevice.SUBCLASS1_COMBO, HIDD_REPORT_DESC); private static BluetoothHidDeviceAppQosSettings sQos = new BluetoothHidDeviceAppQosSettings( BluetoothHidDeviceAppQosSettings.SERVICE_BEST_EFFORT, QOS_TOKEN_RATE, QOS_TOKEN_BUCKET_SIZE, QOS_PEAK_BANDWIDTH, QOS_LATENCY, BluetoothHidDeviceAppQosSettings.MAX); public BluetoothHidDeviceFacade(FacadeManager manager) { super(manager); mService = manager.getService(); mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); mBluetoothAdapter.getProfileProxy(mService, new HidDeviceServiceListener(), BluetoothProfile.HID_DEVICE); mEventFacade = manager.getReceiver(EventFacade.class); mHandlerThread = new HandlerThread("BluetoothHidDeviceFacadeHandler", android.os.Process.THREAD_PRIORITY_BACKGROUND); mHandlerThread.start(); mHandler = new Handler(mHandlerThread.getLooper()); Log.w("Init HID Device Facade"); } class HidDeviceServiceListener implements BluetoothProfile.ServiceListener { @Override public void onServiceConnected(int profile, BluetoothProfile proxy) { Log.d("BluetoothHidDeviceFacade: onServiceConnected"); sHidDeviceProfile = (BluetoothHidDevice) proxy; sIsHidDeviceReady = true; if (proxy == null) { Log.e("proxy is still null"); } } @Override public void onServiceDisconnected(int profile) { sIsHidDeviceReady = false; } } public Boolean hidDeviceConnect(BluetoothDevice device) { return sHidDeviceProfile != null && sHidDeviceProfile.connect(device); } public Boolean hidDeviceDisconnect(BluetoothDevice device) { return sHidDeviceProfile != null && sHidDeviceProfile.disconnect(device); } /** * Check whether the HID Device profile service is ready to use. * @return true if HID Device profile is ready to use; otherwise false */ @Rpc(description = "Is HID Device profile ready.") public Boolean bluetoothHidDeviceIsReady() { Log.d("isReady"); return sHidDeviceProfile != null && sIsHidDeviceReady; } /** * Connect to a Bluetooth HID input host. * @param device name or MAC address or the HID input host * @return true if successfully connected to the HID host; otherwise false * @throws Exception error from Bluetooth HidDevService */ @Rpc(description = "Connect to an HID host.") public Boolean bluetoothHidDeviceConnect( @RpcParameter(name = "device", description = "Name or MAC address of a bluetooth device.") String device) throws Exception { if (sHidDeviceProfile == null) { return false; } BluetoothDevice mDevice = BluetoothFacade.getDevice(BluetoothFacade.DiscoveredDevices, device); Log.d("Connecting to device " + mDevice.getAlias()); return hidDeviceConnect(mDevice); } /** * Disconnect a Bluetooth HID input host. * @param device name or MAC address or the HID input host * @return true if successfully disconnected the HID host; otherwise false * @throws Exception error from Bluetooth HidDevService */ @Rpc(description = "Disconnect an HID host.") public Boolean bluetoothHidDeviceDisconnect( @RpcParameter(name = "device", description = "Name or MAC address of a device.") String device) throws Exception { if (sHidDeviceProfile == null) { return false; } Log.d("Connected devices: " + sHidDeviceProfile.getConnectedDevices()); BluetoothDevice mDevice = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(), device); return hidDeviceDisconnect(mDevice); } /** * Get all the devices connected through HID Device Service. * @return a list of all the devices connected through HID Device Service, * or null if the HID device profile is not ready. */ @Rpc(description = "Get all the devices connected through HID Device Service.") public List bluetoothHidDeviceGetConnectedDevices() { if (sHidDeviceProfile == null) { return null; } return sHidDeviceProfile.getConnectedDevices(); } /** * Get the connection status of the specified device * @param deviceID name or MAC address or the HID input host * @return the status of the device */ @Rpc(description = "Get the connection status of a device.") public Integer bluetoothHidDeviceGetConnectionStatus( @RpcParameter(name = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID) { if (sHidDeviceProfile == null) { return BluetoothProfile.STATE_DISCONNECTED; } List deviceList = sHidDeviceProfile.getConnectedDevices(); BluetoothDevice device; try { device = BluetoothFacade.getDevice(deviceList, deviceID); } catch (Exception e) { return BluetoothProfile.STATE_DISCONNECTED; } return sHidDeviceProfile.getConnectionState(device); } /** * Register app for the HID Device service using default settings. This adds a SDP record. * @return true if successfully registered the app; otherwise false * @throws Exception error from Bluetooth HidDevService */ @Rpc(description = "Register app for the HID Device service using default settings.") public Boolean bluetoothHidDeviceRegisterApp() throws Exception { return sHidDeviceProfile != null && sHidDeviceProfile.registerApp( sSdpSettings, null, sQos, command -> command.run(), mCallback); } /** * Unregister app for the HID Device service. * * @return true if successfully unregistered the app; otherwise false * @throws Exception error from Bluetooth HidDevService */ @Rpc(description = "Unregister app.") public Boolean bluetoothHidDeviceUnregisterApp() throws Exception { return sHidDeviceProfile != null && sHidDeviceProfile.unregisterApp(); } /** * Send a data report to a connected HID host using interrupt channel. * @param deviceID name or MAC address or the HID input host * @param id report Id, as defined in descriptor. Can be 0 in case Report Id are not defined in * descriptor. * @param report report data * @return true if successfully sent the report; otherwise false * @throws Exception error from Bluetooth HidDevService */ @Rpc(description = "Send report to a connected HID host using interrupt channel.") public Boolean bluetoothHidDeviceSendReport( @RpcParameter(name = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID, @RpcParameter(name = "descriptor", description = "Descriptor of the report") Integer id, @RpcParameter(name = "report") String report) throws Exception { if (sHidDeviceProfile == null) { return false; } BluetoothDevice device = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(), deviceID); byte[] reportByteArray = report.getBytes(); return sHidDeviceProfile.sendReport(device, id, reportByteArray); } /** * Send a report to the connected HID host as reply for GET_REPORT request from the HID host. * @param deviceID name or MAC address or the HID input host * @param type type of the report, as in request * @param id id of the report, as in request * @param report report data * @return true if successfully sent the reply report; otherwise false * @throws Exception error from Bluetooth HidDevService */ @Rpc(description = "Send reply report to a connected HID..") public Boolean bluetoothHidDeviceReplyReport( @RpcParameter(name = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID, @RpcParameter(name = "type", description = "Type as in the report.") Integer type, @RpcParameter(name = "id", description = "id as in the report.") Integer id, @RpcParameter(name = "report") String report) throws Exception { if (sHidDeviceProfile == null) { return false; } BluetoothDevice device = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(), deviceID); byte[] reportByteArray = report.getBytes(); return sHidDeviceProfile.replyReport( device, (byte) (int) type, (byte) (int) id, reportByteArray); } /** * Send error handshake message as reply for invalid SET_REPORT request from the HID host. * @param deviceID name or MAC address or the HID input host * @param error error byte * @return true if successfully sent the error handshake message; otherwise false * @throws Exception error from Bluetooth HidDevService */ @Rpc(description = "Send error handshake message to a connected HID host.") public Boolean bluetoothHidDeviceReportError( @RpcParameter(name = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID, @RpcParameter(name = "error", description = "Error byte") Integer error) throws Exception { if (sHidDeviceProfile == null) { return false; } BluetoothDevice device = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(), deviceID); return sHidDeviceProfile.reportError(device, (byte) (int) error); } /** * Start to send HID mouse input to HID host continuously for given duration. * @param deviceID name or MAC address for the HID input host * @param duration time in millisecond to send HID report continuously * @return true if successfully sent the error handshake message; otherwise false */ @Rpc(description = "Start to send HID report continuously") public Boolean bluetoothHidDeviceMoveRepeatedly( @RpcParameter(name = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID, @RpcParameter(name = "duration", description = "duration") Integer duration, @RpcParameter(name = "interval", description = "interval") Integer interval) throws Exception { if (sHidDeviceProfile == null || mKeepMoving) { return false; } BluetoothDevice device = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(), deviceID); mHandler.post(new Runnable() { final long mStopTime = System.currentTimeMillis() + duration; private void sendAndWait(byte[] report) { if (!mKeepMoving) { return; } sHidDeviceProfile.sendReport(device, ID_MOUSE, report); long endTime = System.currentTimeMillis() + interval; while (mKeepMoving && endTime > System.currentTimeMillis()) { //Busy waiting if (mStopTime < System.currentTimeMillis()) { mKeepMoving = false; return; } } } public void run() { mKeepMoving = true; while (mKeepMoving && mStopTime > System.currentTimeMillis()) { sendAndWait(RIGHT); sendAndWait(DOWN); sendAndWait(LEFT); sendAndWait(UP); } mKeepMoving = false; } }); return true; } /** * Stop sending HID report to HID host */ @Rpc(description = "Stop sending HID report") public void bluetoothHidDeviceStopMoving() { mKeepMoving = false; } @Override public void shutdown() { Log.w("Quit handler thread"); mHandlerThread.quit(); } }