1 /*
2  * Copyright (C) 2011 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 android.bluetooth;
18 
19 import android.Manifest;
20 import android.annotation.FlaggedApi;
21 import android.annotation.NonNull;
22 import android.annotation.RequiresPermission;
23 import android.annotation.SdkConstant;
24 import android.annotation.SdkConstant.SdkConstantType;
25 import android.annotation.SuppressLint;
26 import android.annotation.SystemApi;
27 import android.bluetooth.BluetoothDevice.Transport;
28 import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
29 import android.bluetooth.annotations.RequiresLegacyBluetoothAdminPermission;
30 import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
31 import android.content.AttributionSource;
32 import android.content.Context;
33 import android.os.IBinder;
34 import android.os.RemoteException;
35 import android.util.Log;
36 
37 import com.android.bluetooth.flags.Flags;
38 
39 import java.util.Collections;
40 import java.util.List;
41 import java.util.Objects;
42 
43 /**
44  * This class provides the public APIs to control the Bluetooth Input Device Profile.
45  *
46  * <p>BluetoothHidHost is a proxy object for controlling the Bluetooth Service via IPC. Use {@link
47  * BluetoothAdapter#getProfileProxy} to get the BluetoothHidHost proxy object.
48  *
49  * <p>Each method is protected with its appropriate permission.
50  *
51  * @hide
52  */
53 @SystemApi
54 public final class BluetoothHidHost implements BluetoothProfile {
55     private static final String TAG = "BluetoothHidHost";
56     private static final boolean DBG = true;
57     private static final boolean VDBG = false;
58 
59     /**
60      * Intent used to broadcast the change in connection state of the Input Device profile.
61      *
62      * <p>This intent will have 3 extras:
63      *
64      * <ul>
65      *   <li>{@link #EXTRA_STATE} - The current state of the profile.
66      *   <li>{@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.
67      *   <li>{@link BluetoothDevice#EXTRA_TRANSPORT} - Transport of the connection.
68      *   <li>{@link BluetoothDevice#EXTRA_DEVICE} - The remote device.
69      * </ul>
70      *
71      * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of {@link
72      * #STATE_DISCONNECTED}, {@link #STATE_CONNECTING}, {@link #STATE_CONNECTED}, {@link
73      * #STATE_DISCONNECTING}.
74      *
75      * <p>{@link BluetoothDevice#EXTRA_TRANSPORT} can be any of {@link
76      * BluetoothDevice#TRANSPORT_BREDR}, {@link BluetoothDevice#TRANSPORT_LE}.
77      */
78     @SuppressLint("ActionValue")
79     @RequiresLegacyBluetoothPermission
80     @RequiresBluetoothConnectPermission
81     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
82     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
83     public static final String ACTION_CONNECTION_STATE_CHANGED =
84             "android.bluetooth.input.profile.action.CONNECTION_STATE_CHANGED";
85 
86     /** @hide */
87     @RequiresBluetoothConnectPermission
88     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
89     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
90     public static final String ACTION_PROTOCOL_MODE_CHANGED =
91             "android.bluetooth.input.profile.action.PROTOCOL_MODE_CHANGED";
92 
93     /** @hide */
94     @RequiresBluetoothConnectPermission
95     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
96     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
97     public static final String ACTION_HANDSHAKE =
98             "android.bluetooth.input.profile.action.HANDSHAKE";
99 
100     /** @hide */
101     @RequiresBluetoothConnectPermission
102     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
103     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
104     public static final String ACTION_REPORT = "android.bluetooth.input.profile.action.REPORT";
105 
106     /** @hide */
107     @RequiresBluetoothConnectPermission
108     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
109     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
110     public static final String ACTION_VIRTUAL_UNPLUG_STATUS =
111             "android.bluetooth.input.profile.action.VIRTUAL_UNPLUG_STATUS";
112 
113     /** @hide */
114     @RequiresBluetoothConnectPermission
115     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
116     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
117     public static final String ACTION_IDLE_TIME_CHANGED =
118             "android.bluetooth.input.profile.action.IDLE_TIME_CHANGED";
119 
120     /**
121      * Return codes for the connect and disconnect Bluez / Dbus calls.
122      *
123      * @hide
124      */
125     public static final int INPUT_DISCONNECT_FAILED_NOT_CONNECTED = 5000;
126 
127     /** @hide */
128     public static final int INPUT_CONNECT_FAILED_ALREADY_CONNECTED = 5001;
129 
130     /** @hide */
131     public static final int INPUT_CONNECT_FAILED_ATTEMPT_FAILED = 5002;
132 
133     /** @hide */
134     public static final int INPUT_OPERATION_GENERIC_FAILURE = 5003;
135 
136     /** @hide */
137     public static final int INPUT_OPERATION_SUCCESS = 5004;
138 
139     /** @hide */
140     public static final int PROTOCOL_REPORT_MODE = 0;
141 
142     /** @hide */
143     public static final int PROTOCOL_BOOT_MODE = 1;
144 
145     /** @hide */
146     public static final int PROTOCOL_UNSUPPORTED_MODE = 255;
147 
148     /*  int reportType, int reportType, int bufferSize */
149     /** @hide */
150     public static final byte REPORT_TYPE_INPUT = 1;
151 
152     /** @hide */
153     public static final byte REPORT_TYPE_OUTPUT = 2;
154 
155     /** @hide */
156     public static final byte REPORT_TYPE_FEATURE = 3;
157 
158     /** @hide */
159     public static final int VIRTUAL_UNPLUG_STATUS_SUCCESS = 0;
160 
161     /** @hide */
162     public static final int VIRTUAL_UNPLUG_STATUS_FAIL = 1;
163 
164     /** @hide */
165     public static final String EXTRA_PROTOCOL_MODE =
166             "android.bluetooth.BluetoothHidHost.extra.PROTOCOL_MODE";
167 
168     /** @hide */
169     public static final String EXTRA_REPORT_TYPE =
170             "android.bluetooth.BluetoothHidHost.extra.REPORT_TYPE";
171 
172     /** @hide */
173     public static final String EXTRA_REPORT_ID =
174             "android.bluetooth.BluetoothHidHost.extra.REPORT_ID";
175 
176     /** @hide */
177     public static final String EXTRA_REPORT_BUFFER_SIZE =
178             "android.bluetooth.BluetoothHidHost.extra.REPORT_BUFFER_SIZE";
179 
180     /** @hide */
181     public static final String EXTRA_REPORT = "android.bluetooth.BluetoothHidHost.extra.REPORT";
182 
183     /** @hide */
184     public static final String EXTRA_STATUS = "android.bluetooth.BluetoothHidHost.extra.STATUS";
185 
186     /** @hide */
187     public static final String EXTRA_VIRTUAL_UNPLUG_STATUS =
188             "android.bluetooth.BluetoothHidHost.extra.VIRTUAL_UNPLUG_STATUS";
189 
190     /** @hide */
191     public static final String EXTRA_IDLE_TIME =
192             "android.bluetooth.BluetoothHidHost.extra.IDLE_TIME";
193 
194     private final BluetoothAdapter mAdapter;
195     private final AttributionSource mAttributionSource;
196 
197     private IBluetoothHidHost mService;
198 
199     /**
200      * Create a BluetoothHidHost proxy object for interacting with the local Bluetooth Service which
201      * handles the InputDevice profile
202      */
BluetoothHidHost(Context context, BluetoothAdapter adapter)203     /* package */ BluetoothHidHost(Context context, BluetoothAdapter adapter) {
204         mAdapter = adapter;
205         mAttributionSource = adapter.getAttributionSource();
206         mService = null;
207     }
208 
209     /** @hide */
210     @Override
onServiceConnected(IBinder service)211     public void onServiceConnected(IBinder service) {
212         mService = IBluetoothHidHost.Stub.asInterface(service);
213     }
214 
215     /** @hide */
216     @Override
onServiceDisconnected()217     public void onServiceDisconnected() {
218         mService = null;
219     }
220 
getService()221     private IBluetoothHidHost getService() {
222         return mService;
223     }
224 
225     /** @hide */
226     @Override
getAdapter()227     public BluetoothAdapter getAdapter() {
228         return mAdapter;
229     }
230 
231     /**
232      * Initiate connection to a profile of the remote bluetooth device.
233      *
234      * <p>The system supports connection to multiple input devices.
235      *
236      * <p>This API returns false in scenarios like the profile on the device is already connected or
237      * Bluetooth is not turned on. When this API returns true, it is guaranteed that connection
238      * state intent for the profile will be broadcasted with the state. Users can get the connection
239      * state of the profile from this intent.
240      *
241      * @param device Remote Bluetooth Device
242      * @return false on immediate error, true otherwise
243      * @hide
244      */
245     @RequiresBluetoothConnectPermission
246     @RequiresPermission(
247             allOf = {
248                 android.Manifest.permission.BLUETOOTH_CONNECT,
249                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
250             })
connect(BluetoothDevice device)251     public boolean connect(BluetoothDevice device) {
252         if (DBG) log("connect(" + device + ")");
253         final IBluetoothHidHost service = getService();
254         if (service == null) {
255             Log.w(TAG, "Proxy not attached to service");
256             if (DBG) log(Log.getStackTraceString(new Throwable()));
257         } else if (isEnabled() && isValidDevice(device)) {
258             try {
259                 return service.connect(device, mAttributionSource);
260             } catch (RemoteException e) {
261                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
262             }
263         }
264         return false;
265     }
266 
267     /**
268      * Initiate disconnection from a profile
269      *
270      * <p>This API will return false in scenarios like the profile on the Bluetooth device is not in
271      * connected state etc. When this API returns, true, it is guaranteed that the connection state
272      * change intent will be broadcasted with the state. Users can get the disconnection state of
273      * the profile from this intent.
274      *
275      * <p>If the disconnection is initiated by a remote device, the state will transition from
276      * {@link #STATE_CONNECTED} to {@link #STATE_DISCONNECTED}. If the disconnect is initiated by
277      * the host (local) device the state will transition from {@link #STATE_CONNECTED} to state
278      * {@link #STATE_DISCONNECTING} to state {@link #STATE_DISCONNECTED}. The transition to {@link
279      * #STATE_DISCONNECTING} can be used to distinguish between the two scenarios.
280      *
281      * @param device Remote Bluetooth Device
282      * @return false on immediate error, true otherwise
283      * @hide
284      */
285     @RequiresBluetoothConnectPermission
286     @RequiresPermission(
287             allOf = {
288                 android.Manifest.permission.BLUETOOTH_CONNECT,
289                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
290             })
disconnect(BluetoothDevice device)291     public boolean disconnect(BluetoothDevice device) {
292         if (DBG) log("disconnect(" + device + ")");
293         final IBluetoothHidHost service = getService();
294         if (service == null) {
295             Log.w(TAG, "Proxy not attached to service");
296             if (DBG) log(Log.getStackTraceString(new Throwable()));
297         } else if (isEnabled() && isValidDevice(device)) {
298             try {
299                 return service.disconnect(device, mAttributionSource);
300             } catch (RemoteException e) {
301                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
302             }
303         }
304         return false;
305     }
306 
307     /**
308      * {@inheritDoc}
309      *
310      * @hide
311      */
312     @SystemApi
313     @Override
314     @RequiresBluetoothConnectPermission
315     @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
getConnectedDevices()316     public @NonNull List<BluetoothDevice> getConnectedDevices() {
317         if (VDBG) log("getConnectedDevices()");
318         final IBluetoothHidHost service = getService();
319         if (service == null) {
320             Log.w(TAG, "Proxy not attached to service");
321             if (DBG) log(Log.getStackTraceString(new Throwable()));
322         } else if (isEnabled()) {
323             try {
324                 return Attributable.setAttributionSource(
325                         service.getConnectedDevices(mAttributionSource), mAttributionSource);
326             } catch (RemoteException e) {
327                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
328             }
329         }
330         return Collections.emptyList();
331     }
332 
333     /**
334      * {@inheritDoc}
335      *
336      * @hide
337      */
338     @Override
339     @RequiresBluetoothConnectPermission
340     @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
getDevicesMatchingConnectionStates(int[] states)341     public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
342         if (VDBG) log("getDevicesMatchingStates()");
343         final IBluetoothHidHost service = getService();
344         if (service == null) {
345             Log.w(TAG, "Proxy not attached to service");
346             if (DBG) log(Log.getStackTraceString(new Throwable()));
347         } else if (isEnabled()) {
348             try {
349                 return Attributable.setAttributionSource(
350                         service.getDevicesMatchingConnectionStates(states, mAttributionSource),
351                         mAttributionSource);
352             } catch (RemoteException e) {
353                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
354             }
355         }
356         return Collections.emptyList();
357     }
358 
359     /**
360      * {@inheritDoc}
361      *
362      * @hide
363      */
364     @SystemApi
365     @Override
366     @RequiresBluetoothConnectPermission
367     @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
getConnectionState(@onNull BluetoothDevice device)368     public int getConnectionState(@NonNull BluetoothDevice device) {
369         if (VDBG) log("getState(" + device + ")");
370         if (device == null) {
371             throw new IllegalArgumentException("device must not be null");
372         }
373         final IBluetoothHidHost service = getService();
374         if (service == null) {
375             Log.w(TAG, "Proxy not attached to service");
376             if (DBG) log(Log.getStackTraceString(new Throwable()));
377         } else if (isEnabled() && isValidDevice(device)) {
378             try {
379                 return service.getConnectionState(device, mAttributionSource);
380             } catch (RemoteException e) {
381                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
382             }
383         }
384         return BluetoothProfile.STATE_DISCONNECTED;
385     }
386 
387     /**
388      * Set priority of the profile
389      *
390      * <p>The device should already be paired. Priority can be one of {@link #PRIORITY_ON} or {@link
391      * #PRIORITY_OFF},
392      *
393      * @param device Paired bluetooth device
394      * @return true if priority is set, false on error
395      * @hide
396      */
397     @RequiresBluetoothConnectPermission
398     @RequiresPermission(
399             allOf = {
400                 android.Manifest.permission.BLUETOOTH_CONNECT,
401                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
402             })
setPriority(BluetoothDevice device, int priority)403     public boolean setPriority(BluetoothDevice device, int priority) {
404         if (DBG) log("setPriority(" + device + ", " + priority + ")");
405         return setConnectionPolicy(device, BluetoothAdapter.priorityToConnectionPolicy(priority));
406     }
407 
408     /**
409      * Set connection policy of the profile
410      *
411      * <p>The device should already be paired. Connection policy can be one of {@link
412      * #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN}, {@link
413      * #CONNECTION_POLICY_UNKNOWN}
414      *
415      * @param device Paired bluetooth device
416      * @param connectionPolicy is the connection policy to set to for this profile
417      * @return true if connectionPolicy is set, false on error
418      * @hide
419      */
420     @SystemApi
421     @RequiresBluetoothConnectPermission
422     @RequiresPermission(
423             allOf = {
424                 android.Manifest.permission.BLUETOOTH_CONNECT,
425                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
426             })
setConnectionPolicy( @onNull BluetoothDevice device, @ConnectionPolicy int connectionPolicy)427     public boolean setConnectionPolicy(
428             @NonNull BluetoothDevice device, @ConnectionPolicy int connectionPolicy) {
429         if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
430         if (device == null) {
431             throw new IllegalArgumentException("device must not be null");
432         }
433         final IBluetoothHidHost service = getService();
434         if (service == null) {
435             Log.w(TAG, "Proxy not attached to service");
436             if (DBG) log(Log.getStackTraceString(new Throwable()));
437         } else if (isEnabled()
438                 && isValidDevice(device)
439                 && (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN
440                         || connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED)) {
441             try {
442                 return service.setConnectionPolicy(device, connectionPolicy, mAttributionSource);
443             } catch (RemoteException e) {
444                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
445             }
446         }
447         return false;
448     }
449 
450     /**
451      * Set preferred transport for the device
452      *
453      * <p>The device should already be paired, services must have been discovered. This API is
454      * effective only if both the HID and HOGP are supported on the remote device.
455      *
456      * @param device paired bluetooth device
457      * @param transport the preferred transport to set for this device
458      * @return true if preferred transport is set, false on error
459      * @throws IllegalArgumentException if the {@code device} invalid.
460      * @hide
461      */
462     @FlaggedApi(Flags.FLAG_ALLOW_SWITCHING_HID_AND_HOGP)
463     @SystemApi
464     @RequiresBluetoothConnectPermission
465     @RequiresPermission(
466             allOf = {
467                 android.Manifest.permission.BLUETOOTH_CONNECT,
468                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
469             })
setPreferredTransport( @onNull BluetoothDevice device, @Transport int transport)470     public boolean setPreferredTransport(
471             @NonNull BluetoothDevice device, @Transport int transport) {
472         if (DBG) log("setPreferredTransport(" + device + ", " + transport + ")");
473 
474         Objects.requireNonNull(device, "device must not be null");
475 
476         if (transport != BluetoothDevice.TRANSPORT_AUTO
477                 && transport != BluetoothDevice.TRANSPORT_BREDR
478                 && transport != BluetoothDevice.TRANSPORT_LE) {
479             throw new IllegalArgumentException("Invalid transport value");
480         }
481 
482         final IBluetoothHidHost service = getService();
483 
484         if (service == null) {
485             Log.w(TAG, "Proxy not attached to service");
486             if (DBG) log(Log.getStackTraceString(new Throwable()));
487         } else if (!isEnabled()) {
488             Log.w(TAG, "Not ready");
489         } else if (!isValidDevice(device)) {
490             throw new IllegalArgumentException("Invalid device");
491         } else {
492             try {
493                 return service.setPreferredTransport(device, transport, mAttributionSource);
494             } catch (RemoteException e) {
495                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
496             }
497         }
498         return false;
499     }
500 
501     /**
502      * Get the priority of the profile.
503      *
504      * <p>The priority can be any of: {@link #PRIORITY_OFF}, {@link #PRIORITY_ON}, {@link
505      * #PRIORITY_UNDEFINED}
506      *
507      * @param device Bluetooth device
508      * @return priority of the device
509      * @hide
510      */
511     @RequiresBluetoothConnectPermission
512     @RequiresPermission(
513             allOf = {
514                 android.Manifest.permission.BLUETOOTH_CONNECT,
515                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
516             })
getPriority(BluetoothDevice device)517     public int getPriority(BluetoothDevice device) {
518         if (VDBG) log("getPriority(" + device + ")");
519         return BluetoothAdapter.connectionPolicyToPriority(getConnectionPolicy(device));
520     }
521 
522     /**
523      * Get the connection policy of the profile.
524      *
525      * <p>The connection policy can be any of: {@link #CONNECTION_POLICY_ALLOWED}, {@link
526      * #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN}
527      *
528      * @param device Bluetooth device
529      * @return connection policy of the device
530      * @hide
531      */
532     @SystemApi
533     @RequiresBluetoothConnectPermission
534     @RequiresPermission(
535             allOf = {
536                 android.Manifest.permission.BLUETOOTH_CONNECT,
537                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
538             })
getConnectionPolicy(@onNull BluetoothDevice device)539     public @ConnectionPolicy int getConnectionPolicy(@NonNull BluetoothDevice device) {
540         if (VDBG) log("getConnectionPolicy(" + device + ")");
541         if (device == null) {
542             throw new IllegalArgumentException("device must not be null");
543         }
544         final IBluetoothHidHost service = getService();
545         if (service == null) {
546             Log.w(TAG, "Proxy not attached to service");
547             if (DBG) log(Log.getStackTraceString(new Throwable()));
548         } else if (isEnabled() && isValidDevice(device)) {
549             try {
550                 return service.getConnectionPolicy(device, mAttributionSource);
551             } catch (RemoteException e) {
552                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
553             }
554         }
555         return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
556     }
557 
558     /**
559      * Get the preferred transport for the device.
560      *
561      * @param device Bluetooth device
562      * @return preferred transport for the device
563      * @throws IllegalArgumentException if the {@code device} invalid.
564      * @hide
565      */
566     @FlaggedApi(Flags.FLAG_ALLOW_SWITCHING_HID_AND_HOGP)
567     @SystemApi
568     @RequiresBluetoothConnectPermission
569     @RequiresPermission(
570             allOf = {
571                 android.Manifest.permission.BLUETOOTH_CONNECT,
572                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
573             })
getPreferredTransport(@onNull BluetoothDevice device)574     public @Transport int getPreferredTransport(@NonNull BluetoothDevice device) {
575         if (VDBG) log("getPreferredTransport(" + device + ")");
576 
577         Objects.requireNonNull(device, "device must not be null");
578 
579         final IBluetoothHidHost service = getService();
580         if (service == null) {
581             Log.w(TAG, "Proxy not attached to service");
582             if (DBG) log(Log.getStackTraceString(new Throwable()));
583         } else if (!isEnabled()) {
584             Log.w(TAG, "Not ready");
585         } else if (!isValidDevice(device)) {
586             throw new IllegalArgumentException("Invalid device");
587         } else {
588             try {
589                 return service.getPreferredTransport(device, mAttributionSource);
590             } catch (RemoteException e) {
591                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
592             }
593         }
594         return BluetoothDevice.TRANSPORT_AUTO;
595     }
596 
isEnabled()597     private boolean isEnabled() {
598         return mAdapter.getState() == BluetoothAdapter.STATE_ON;
599     }
600 
isValidDevice(BluetoothDevice device)601     private static boolean isValidDevice(BluetoothDevice device) {
602         return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
603     }
604 
605     /**
606      * Initiate virtual unplug for a HID input device.
607      *
608      * @param device Remote Bluetooth Device
609      * @return false on immediate error, true otherwise
610      * @hide
611      */
612     @RequiresLegacyBluetoothAdminPermission
613     @RequiresBluetoothConnectPermission
614     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
virtualUnplug(BluetoothDevice device)615     public boolean virtualUnplug(BluetoothDevice device) {
616         if (DBG) log("virtualUnplug(" + device + ")");
617         final IBluetoothHidHost service = getService();
618         if (service == null) {
619             Log.w(TAG, "Proxy not attached to service");
620             if (DBG) log(Log.getStackTraceString(new Throwable()));
621         } else if (isEnabled() && isValidDevice(device)) {
622             try {
623                 return service.virtualUnplug(device, mAttributionSource);
624             } catch (RemoteException e) {
625                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
626             }
627         }
628         return false;
629     }
630 
631     /**
632      * Send Get_Protocol_Mode command to the connected HID input device.
633      *
634      * @param device Remote Bluetooth Device
635      * @return false on immediate error, true otherwise
636      * @hide
637      */
638     @RequiresLegacyBluetoothAdminPermission
639     @RequiresBluetoothConnectPermission
640     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
getProtocolMode(BluetoothDevice device)641     public boolean getProtocolMode(BluetoothDevice device) {
642         if (VDBG) log("getProtocolMode(" + device + ")");
643         final IBluetoothHidHost service = getService();
644         if (service == null) {
645             Log.w(TAG, "Proxy not attached to service");
646             if (DBG) log(Log.getStackTraceString(new Throwable()));
647         } else if (isEnabled() && isValidDevice(device)) {
648             try {
649                 return service.getProtocolMode(device, mAttributionSource);
650             } catch (RemoteException e) {
651                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
652             }
653         }
654         return false;
655     }
656 
657     /**
658      * Send Set_Protocol_Mode command to the connected HID input device.
659      *
660      * @param device Remote Bluetooth Device
661      * @return false on immediate error, true otherwise
662      * @hide
663      */
664     @RequiresLegacyBluetoothAdminPermission
665     @RequiresBluetoothConnectPermission
666     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
setProtocolMode(BluetoothDevice device, int protocolMode)667     public boolean setProtocolMode(BluetoothDevice device, int protocolMode) {
668         if (DBG) log("setProtocolMode(" + device + ")");
669         final IBluetoothHidHost service = getService();
670         if (service == null) {
671             Log.w(TAG, "Proxy not attached to service");
672             if (DBG) log(Log.getStackTraceString(new Throwable()));
673         } else if (isEnabled() && isValidDevice(device)) {
674             try {
675                 return service.setProtocolMode(device, protocolMode, mAttributionSource);
676             } catch (RemoteException e) {
677                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
678             }
679         }
680         return false;
681     }
682 
683     /**
684      * Send Get_Report command to the connected HID input device.
685      *
686      * @param device Remote Bluetooth Device
687      * @param reportType Report type
688      * @param reportId Report ID
689      * @param bufferSize Report receiving buffer size
690      * @return false on immediate error, true otherwise
691      * @hide
692      */
693     @RequiresLegacyBluetoothAdminPermission
694     @RequiresBluetoothConnectPermission
695     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
getReport( BluetoothDevice device, byte reportType, byte reportId, int bufferSize)696     public boolean getReport(
697             BluetoothDevice device, byte reportType, byte reportId, int bufferSize) {
698         if (VDBG) {
699             log(
700                     "getReport("
701                             + device
702                             + "), reportType="
703                             + reportType
704                             + " reportId="
705                             + reportId
706                             + "bufferSize="
707                             + bufferSize);
708         }
709         final IBluetoothHidHost service = getService();
710         if (service == null) {
711             Log.w(TAG, "Proxy not attached to service");
712             if (DBG) log(Log.getStackTraceString(new Throwable()));
713         } else if (isEnabled() && isValidDevice(device)) {
714             try {
715                 return service.getReport(
716                         device, reportType, reportId, bufferSize, mAttributionSource);
717             } catch (RemoteException e) {
718                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
719             }
720         }
721         return false;
722     }
723 
724     /**
725      * Send Set_Report command to the connected HID input device.
726      *
727      * @param device Remote Bluetooth Device
728      * @param reportType Report type
729      * @param report Report receiving buffer size
730      * @return false on immediate error, true otherwise
731      * @hide
732      */
733     @RequiresLegacyBluetoothAdminPermission
734     @RequiresBluetoothConnectPermission
735     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
setReport(BluetoothDevice device, byte reportType, String report)736     public boolean setReport(BluetoothDevice device, byte reportType, String report) {
737         if (VDBG) log("setReport(" + device + "), reportType=" + reportType + " report=" + report);
738         final IBluetoothHidHost service = getService();
739         if (service == null) {
740             Log.w(TAG, "Proxy not attached to service");
741             if (DBG) log(Log.getStackTraceString(new Throwable()));
742         } else if (isEnabled() && isValidDevice(device)) {
743             try {
744                 return service.setReport(device, reportType, report, mAttributionSource);
745             } catch (RemoteException e) {
746                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
747             }
748         }
749         return false;
750     }
751 
752     /**
753      * Send Send_Data command to the connected HID input device.
754      *
755      * @param device Remote Bluetooth Device
756      * @param report Report to send
757      * @return false on immediate error, true otherwise
758      * @hide
759      */
760     @RequiresLegacyBluetoothAdminPermission
761     @RequiresBluetoothConnectPermission
762     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
sendData(BluetoothDevice device, String report)763     public boolean sendData(BluetoothDevice device, String report) {
764         if (DBG) log("sendData(" + device + "), report=" + report);
765         final IBluetoothHidHost service = getService();
766         if (service == null) {
767             Log.w(TAG, "Proxy not attached to service");
768             if (DBG) log(Log.getStackTraceString(new Throwable()));
769         } else if (isEnabled() && isValidDevice(device)) {
770             try {
771                 return service.sendData(device, report, mAttributionSource);
772             } catch (RemoteException e) {
773                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
774             }
775         }
776         return false;
777     }
778 
779     /**
780      * Send Get_Idle_Time command to the connected HID input device.
781      *
782      * @param device Remote Bluetooth Device
783      * @return false on immediate error, true otherwise
784      * @hide
785      */
786     @RequiresLegacyBluetoothAdminPermission
787     @RequiresBluetoothConnectPermission
788     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
getIdleTime(BluetoothDevice device)789     public boolean getIdleTime(BluetoothDevice device) {
790         if (DBG) log("getIdletime(" + device + ")");
791         final IBluetoothHidHost service = getService();
792         if (service == null) {
793             Log.w(TAG, "Proxy not attached to service");
794             if (DBG) log(Log.getStackTraceString(new Throwable()));
795         } else if (isEnabled() && isValidDevice(device)) {
796             try {
797                 return service.getIdleTime(device, mAttributionSource);
798             } catch (RemoteException e) {
799                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
800             }
801         }
802         return false;
803     }
804 
805     /**
806      * Send Set_Idle_Time command to the connected HID input device.
807      *
808      * @param device Remote Bluetooth Device
809      * @param idleTime Idle time to be set on HID Device
810      * @return false on immediate error, true otherwise
811      * @hide
812      */
813     @RequiresLegacyBluetoothAdminPermission
814     @RequiresBluetoothConnectPermission
815     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
setIdleTime(BluetoothDevice device, byte idleTime)816     public boolean setIdleTime(BluetoothDevice device, byte idleTime) {
817         if (DBG) log("setIdletime(" + device + "), idleTime=" + idleTime);
818         final IBluetoothHidHost service = getService();
819         if (service == null) {
820             Log.w(TAG, "Proxy not attached to service");
821             if (DBG) log(Log.getStackTraceString(new Throwable()));
822         } else if (isEnabled() && isValidDevice(device)) {
823             try {
824                 return service.setIdleTime(device, idleTime, mAttributionSource);
825             } catch (RemoteException e) {
826                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
827             }
828         }
829         return false;
830     }
831 
log(String msg)832     private static void log(String msg) {
833         Log.d(TAG, msg);
834     }
835 }
836