• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
BluetoothHidDeviceFacade(FacadeManager manager)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
onServiceConnected(int profile, BluetoothProfile proxy)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
onServiceDisconnected(int profile)279         public void onServiceDisconnected(int profile) {
280             sIsHidDeviceReady = false;
281         }
282     }
283 
hidDeviceConnect(BluetoothDevice device)284     public Boolean hidDeviceConnect(BluetoothDevice device) {
285         return sHidDeviceProfile != null && sHidDeviceProfile.connect(device);
286     }
287 
hidDeviceDisconnect(BluetoothDevice device)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.")
bluetoothHidDeviceIsReady()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.")
bluetoothHidDeviceConnect( @pcParametername = "device", description = "Name or MAC address of a bluetooth device.") String device)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.getAlias());
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.")
bluetoothHidDeviceDisconnect( @pcParametername = "device", description = "Name or MAC address of a device.") String device)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.")
bluetoothHidDeviceGetConnectedDevices()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.")
bluetoothHidDeviceGetConnectionStatus( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)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.")
bluetoothHidDeviceRegisterApp()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.")
bluetoothHidDeviceUnregisterApp()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.")
bluetoothHidDeviceSendReport( @pcParametername = "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)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..")
bluetoothHidDeviceReplyReport( @pcParametername = "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)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.")
bluetoothHidDeviceReportError( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID, @RpcParameter(name = "error", description = "Error byte") Integer error)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")
bluetoothHidDeviceMoveRepeatedly( @pcParametername = "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)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")
bluetoothHidDeviceStopMoving()545     public void bluetoothHidDeviceStopMoving() {
546         mKeepMoving = false;
547     }
548 
549     @Override
shutdown()550     public void shutdown() {
551         Log.w("Quit handler thread");
552         mHandlerThread.quit();
553     }
554 
555 }
556