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 bytes array data report to a connected HID host using interrupt channel.
434      * @param deviceID name or MAC address or the HID input host
435      * @param id report Id, as defined in descriptor. Can be 0 in case Report Id are not defined in
436      * descriptor.
437      * @param report byte array to be sent into HID device
438      * @return true if successfully sent the report; otherwise false
439      * @throws Exception error from Bluetooth HidDevService
440      */
441     @Rpc(description = "Send bytes array report to a connected HID host using interrupt channel.")
bluetoothHidDeviceSendBytesArrayReport( @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") byte[] report)442     public Boolean bluetoothHidDeviceSendBytesArrayReport(
443             @RpcParameter(name = "deviceID",
444                     description = "Name or MAC address of a bluetooth device.")
445                     String deviceID,
446             @RpcParameter(name = "descriptor",
447                     description = "Descriptor of the report")
448                     Integer id,
449             @RpcParameter(name = "report")
450                     byte[] report) throws Exception {
451         if (sHidDeviceProfile == null) {
452             return false;
453         }
454 
455         BluetoothDevice device = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(),
456                 deviceID);
457         return sHidDeviceProfile.sendReport(device, id, report);
458     }
459 
460     /**
461      * Send a report to the connected HID host as reply for GET_REPORT request from the HID host.
462      * @param deviceID name or MAC address or the HID input host
463      * @param type type of the report, as in request
464      * @param id id of the report, as in request
465      * @param report report data
466      * @return true if successfully sent the reply report; otherwise false
467      * @throws Exception error from Bluetooth HidDevService
468      */
469     @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)470     public Boolean bluetoothHidDeviceReplyReport(
471             @RpcParameter(name = "deviceID",
472                     description = "Name or MAC address of a bluetooth device.")
473                     String deviceID,
474             @RpcParameter(name = "type",
475                     description = "Type as in the report.")
476                     Integer type,
477             @RpcParameter(name = "id",
478                     description = "id as in the report.")
479                     Integer id,
480             @RpcParameter(name = "report")
481                     String report) throws Exception {
482         if (sHidDeviceProfile == null) {
483             return false;
484         }
485 
486         BluetoothDevice device = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(),
487                 deviceID);
488         byte[] reportByteArray = report.getBytes();
489         return sHidDeviceProfile.replyReport(
490                 device, (byte) (int) type, (byte) (int) id, reportByteArray);
491     }
492 
493     /**
494      * Send a bytes array report to the connected HID host as reply for GET_REPORT request
495      * from the HID host.
496      * @param deviceID name or MAC address or the HID input host
497      * @param type type of the report, as in request
498      * @param id id of the report, as in request
499      * @param report byte array to be sent into HID device
500      * @return true if successfully sent the reply report; otherwise false
501      * @throws Exception error from Bluetooth HidDevService
502      */
503     @Rpc(description = "Send reply bytes array report to a connected HID..")
bluetoothHidDeviceReplyBytesArrayReport( @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") byte[] report)504     public Boolean bluetoothHidDeviceReplyBytesArrayReport(
505             @RpcParameter(name = "deviceID",
506                     description = "Name or MAC address of a bluetooth device.")
507                     String deviceID,
508             @RpcParameter(name = "type",
509                     description = "Type as in the report.")
510                     Integer type,
511             @RpcParameter(name = "id",
512                     description = "id as in the report.")
513                     Integer id,
514             @RpcParameter(name = "report")
515                     byte[] report) throws Exception {
516         if (sHidDeviceProfile == null) {
517             return false;
518         }
519 
520         BluetoothDevice device = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(),
521                 deviceID);
522         return sHidDeviceProfile.replyReport(
523                 device, (byte) (int) type, (byte) (int) id, report);
524     }
525 
526     /**
527      * Send error handshake message as reply for invalid SET_REPORT request from the HID host.
528      * @param deviceID name or MAC address or the HID input host
529      * @param error error byte
530      * @return true if successfully sent the error handshake message; otherwise false
531      * @throws Exception error from Bluetooth HidDevService
532      */
533     @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)534     public Boolean bluetoothHidDeviceReportError(
535             @RpcParameter(name = "deviceID",
536                     description = "Name or MAC address of a bluetooth device.")
537                     String deviceID,
538             @RpcParameter(name = "error",
539                     description = "Error byte")
540                     Integer error) throws Exception {
541         if (sHidDeviceProfile == null) {
542             return false;
543         }
544 
545         BluetoothDevice device = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(),
546                 deviceID);
547         return sHidDeviceProfile.reportError(device, (byte) (int) error);
548     }
549 
550    /**
551      * Start to send HID mouse input to HID host continuously for given duration.
552      * @param deviceID name or MAC address for the HID input host
553      * @param duration time in millisecond to send HID report continuously
554      * @return true if successfully sent the error handshake message; otherwise false
555      */
556     @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)557     public Boolean bluetoothHidDeviceMoveRepeatedly(
558             @RpcParameter(name = "deviceID",
559                     description = "Name or MAC address of a bluetooth device.")
560                     String deviceID,
561             @RpcParameter(name = "duration",
562                     description = "duration")
563                     Integer duration,
564             @RpcParameter(name = "interval",
565                     description = "interval")
566                     Integer interval) throws Exception {
567         if (sHidDeviceProfile == null || mKeepMoving) {
568             return false;
569         }
570         BluetoothDevice device = BluetoothFacade.getDevice(sHidDeviceProfile.getConnectedDevices(),
571                 deviceID);
572         mHandler.post(new Runnable() {
573             final long mStopTime = System.currentTimeMillis() + duration;
574             private void sendAndWait(byte[] report) {
575                 if (!mKeepMoving) {
576                     return;
577                 }
578                 sHidDeviceProfile.sendReport(device, ID_MOUSE, report);
579                 long endTime = System.currentTimeMillis() + interval;
580                 while (mKeepMoving && endTime > System.currentTimeMillis()) {
581                     //Busy waiting
582                     if (mStopTime < System.currentTimeMillis()) {
583                         mKeepMoving = false;
584                         return;
585                     }
586                 }
587             }
588             public void run() {
589                 mKeepMoving = true;
590                 while (mKeepMoving && mStopTime > System.currentTimeMillis()) {
591                     sendAndWait(RIGHT);
592                     sendAndWait(DOWN);
593                     sendAndWait(LEFT);
594                     sendAndWait(UP);
595                 }
596                 mKeepMoving = false;
597             }
598         });
599         return true;
600     }
601 
602     /**
603      * Stop sending HID report to HID host
604      */
605     @Rpc(description = "Stop sending HID report")
bluetoothHidDeviceStopMoving()606     public void bluetoothHidDeviceStopMoving() {
607         mKeepMoving = false;
608     }
609 
610     @Override
shutdown()611     public void shutdown() {
612         Log.w("Quit handler thread");
613         mHandlerThread.quit();
614     }
615 
616 }
617