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
17package android.bluetooth;
18
19import android.annotation.SdkConstant;
20import android.annotation.SdkConstant.SdkConstantType;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.Intent;
24import android.content.ServiceConnection;
25import android.os.IBinder;
26import android.os.RemoteException;
27import android.os.ServiceManager;
28import android.util.Log;
29
30import java.util.ArrayList;
31import java.util.List;
32
33
34/**
35 * This class provides the public APIs to control the Bluetooth Input
36 * Device Profile.
37 *
38 *<p>BluetoothInputDevice is a proxy object for controlling the Bluetooth
39 * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get
40 * the BluetoothInputDevice proxy object.
41 *
42 *<p>Each method is protected with its appropriate permission.
43 *@hide
44 */
45public final class BluetoothInputDevice implements BluetoothProfile {
46    private static final String TAG = "BluetoothInputDevice";
47    private static final boolean DBG = true;
48    private static final boolean VDBG = false;
49
50    /**
51     * Intent used to broadcast the change in connection state of the Input
52     * Device profile.
53     *
54     * <p>This intent will have 3 extras:
55     * <ul>
56     *   <li> {@link #EXTRA_STATE} - The current state of the profile. </li>
57     *   <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.</li>
58     *   <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
59     * </ul>
60     *
61     * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of
62     * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING},
63     * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}.
64     *
65     * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to
66     * receive.
67     */
68    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
69    public static final String ACTION_CONNECTION_STATE_CHANGED =
70        "android.bluetooth.input.profile.action.CONNECTION_STATE_CHANGED";
71
72    /**
73     * @hide
74     */
75    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
76    public static final String ACTION_PROTOCOL_MODE_CHANGED =
77        "android.bluetooth.input.profile.action.PROTOCOL_MODE_CHANGED";
78
79
80    /**
81     * @hide
82     */
83    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
84    public static final String ACTION_VIRTUAL_UNPLUG_STATUS =
85        "android.bluetooth.input.profile.action.VIRTUAL_UNPLUG_STATUS";
86
87
88    /**
89     * Return codes for the connect and disconnect Bluez / Dbus calls.
90     * @hide
91     */
92    public static final int INPUT_DISCONNECT_FAILED_NOT_CONNECTED = 5000;
93
94    /**
95     * @hide
96     */
97    public static final int INPUT_CONNECT_FAILED_ALREADY_CONNECTED = 5001;
98
99    /**
100     * @hide
101     */
102    public static final int INPUT_CONNECT_FAILED_ATTEMPT_FAILED = 5002;
103
104    /**
105     * @hide
106     */
107    public static final int INPUT_OPERATION_GENERIC_FAILURE = 5003;
108
109    /**
110     * @hide
111     */
112    public static final int INPUT_OPERATION_SUCCESS = 5004;
113
114    /**
115     * @hide
116     */
117    public static final int PROTOCOL_REPORT_MODE = 0;
118
119    /**
120     * @hide
121     */
122    public static final int PROTOCOL_BOOT_MODE = 1;
123
124    /**
125     * @hide
126     */
127    public static final int PROTOCOL_UNSUPPORTED_MODE = 255;
128
129    /*  int reportType, int reportType, int bufferSize */
130    /**
131     * @hide
132     */
133    public static final byte REPORT_TYPE_INPUT = 0;
134
135    /**
136     * @hide
137     */
138    public static final byte REPORT_TYPE_OUTPUT = 1;
139
140    /**
141     * @hide
142     */
143    public static final byte REPORT_TYPE_FEATURE = 2;
144
145    /**
146     * @hide
147     */
148    public static final int VIRTUAL_UNPLUG_STATUS_SUCCESS = 0;
149
150    /**
151     * @hide
152     */
153    public static final int VIRTUAL_UNPLUG_STATUS_FAIL = 1;
154
155    /**
156     * @hide
157     */
158    public static final String EXTRA_PROTOCOL_MODE = "android.bluetooth.BluetoothInputDevice.extra.PROTOCOL_MODE";
159
160    /**
161     * @hide
162     */
163    public static final String EXTRA_REPORT_TYPE = "android.bluetooth.BluetoothInputDevice.extra.REPORT_TYPE";
164
165    /**
166     * @hide
167     */
168    public static final String EXTRA_REPORT_ID = "android.bluetooth.BluetoothInputDevice.extra.REPORT_ID";
169
170    /**
171     * @hide
172     */
173    public static final String EXTRA_REPORT_BUFFER_SIZE = "android.bluetooth.BluetoothInputDevice.extra.REPORT_BUFFER_SIZE";
174
175    /**
176     * @hide
177     */
178    public static final String EXTRA_REPORT = "android.bluetooth.BluetoothInputDevice.extra.REPORT";
179
180    /**
181     * @hide
182     */
183    public static final String EXTRA_VIRTUAL_UNPLUG_STATUS = "android.bluetooth.BluetoothInputDevice.extra.VIRTUAL_UNPLUG_STATUS";
184
185    private Context mContext;
186    private ServiceListener mServiceListener;
187    private BluetoothAdapter mAdapter;
188    private IBluetoothInputDevice mService;
189
190    final private IBluetoothStateChangeCallback mBluetoothStateChangeCallback =
191            new IBluetoothStateChangeCallback.Stub() {
192                public void onBluetoothStateChange(boolean up) {
193                    if (DBG) Log.d(TAG, "onBluetoothStateChange: up=" + up);
194                    if (!up) {
195                        if (VDBG) Log.d(TAG,"Unbinding service...");
196                        synchronized (mConnection) {
197                            try {
198                                mService = null;
199                                mContext.unbindService(mConnection);
200                            } catch (Exception re) {
201                                Log.e(TAG,"",re);
202                            }
203                        }
204                    } else {
205                        synchronized (mConnection) {
206                            try {
207                                if (mService == null) {
208                                    if (VDBG) Log.d(TAG,"Binding service...");
209                                    if (!mContext.bindService(new Intent(IBluetoothInputDevice.class.getName()), mConnection, 0)) {
210                                        Log.e(TAG, "Could not bind to Bluetooth HID Service");
211                                    }
212                                }
213                            } catch (Exception re) {
214                                Log.e(TAG,"",re);
215                            }
216                        }
217                    }
218                }
219        };
220
221    /**
222     * Create a BluetoothInputDevice proxy object for interacting with the local
223     * Bluetooth Service which handles the InputDevice profile
224     *
225     */
226    /*package*/ BluetoothInputDevice(Context context, ServiceListener l) {
227        mContext = context;
228        mServiceListener = l;
229        mAdapter = BluetoothAdapter.getDefaultAdapter();
230
231        IBluetoothManager mgr = mAdapter.getBluetoothManager();
232        if (mgr != null) {
233            try {
234                mgr.registerStateChangeCallback(mBluetoothStateChangeCallback);
235            } catch (RemoteException e) {
236                Log.e(TAG,"",e);
237            }
238        }
239
240        if (!context.bindService(new Intent(IBluetoothInputDevice.class.getName()),
241                                 mConnection, 0)) {
242            Log.e(TAG, "Could not bind to Bluetooth HID Service");
243        }
244    }
245
246    /*package*/ void close() {
247        if (VDBG) log("close()");
248        IBluetoothManager mgr = mAdapter.getBluetoothManager();
249        if (mgr != null) {
250            try {
251                mgr.unregisterStateChangeCallback(mBluetoothStateChangeCallback);
252            } catch (Exception e) {
253                Log.e(TAG,"",e);
254            }
255        }
256
257        synchronized (mConnection) {
258            if (mService != null) {
259                try {
260                    mService = null;
261                    mContext.unbindService(mConnection);
262                } catch (Exception re) {
263                    Log.e(TAG,"",re);
264                }
265           }
266        }
267        mServiceListener = null;
268    }
269
270    /**
271     * Initiate connection to a profile of the remote bluetooth device.
272     *
273     * <p> The system supports connection to multiple input devices.
274     *
275     * <p> This API returns false in scenarios like the profile on the
276     * device is already connected or Bluetooth is not turned on.
277     * When this API returns true, it is guaranteed that
278     * connection state intent for the profile will be broadcasted with
279     * the state. Users can get the connection state of the profile
280     * from this intent.
281     *
282     * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
283     * permission.
284     *
285     * @param device Remote Bluetooth Device
286     * @return false on immediate error,
287     *               true otherwise
288     * @hide
289     */
290    public boolean connect(BluetoothDevice device) {
291        if (DBG) log("connect(" + device + ")");
292        if (mService != null && isEnabled() && isValidDevice(device)) {
293            try {
294                return mService.connect(device);
295            } catch (RemoteException e) {
296                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
297                return false;
298            }
299        }
300        if (mService == null) Log.w(TAG, "Proxy not attached to service");
301        return false;
302    }
303
304    /**
305     * Initiate disconnection from a profile
306     *
307     * <p> This API will return false in scenarios like the profile on the
308     * Bluetooth device is not in connected state etc. When this API returns,
309     * true, it is guaranteed that the connection state change
310     * intent will be broadcasted with the state. Users can get the
311     * disconnection state of the profile from this intent.
312     *
313     * <p> If the disconnection is initiated by a remote device, the state
314     * will transition from {@link #STATE_CONNECTED} to
315     * {@link #STATE_DISCONNECTED}. If the disconnect is initiated by the
316     * host (local) device the state will transition from
317     * {@link #STATE_CONNECTED} to state {@link #STATE_DISCONNECTING} to
318     * state {@link #STATE_DISCONNECTED}. The transition to
319     * {@link #STATE_DISCONNECTING} can be used to distinguish between the
320     * two scenarios.
321     *
322     * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
323     * permission.
324     *
325     * @param device Remote Bluetooth Device
326     * @return false on immediate error,
327     *               true otherwise
328     * @hide
329     */
330    public boolean disconnect(BluetoothDevice device) {
331        if (DBG) log("disconnect(" + device + ")");
332        if (mService != null && isEnabled() && isValidDevice(device)) {
333            try {
334                return mService.disconnect(device);
335            } catch (RemoteException e) {
336                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
337                return false;
338            }
339        }
340        if (mService == null) Log.w(TAG, "Proxy not attached to service");
341        return false;
342    }
343
344    /**
345     * {@inheritDoc}
346     */
347    public List<BluetoothDevice> getConnectedDevices() {
348        if (VDBG) log("getConnectedDevices()");
349        if (mService != null && isEnabled()) {
350            try {
351                return mService.getConnectedDevices();
352            } catch (RemoteException e) {
353                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
354                return new ArrayList<BluetoothDevice>();
355            }
356        }
357        if (mService == null) Log.w(TAG, "Proxy not attached to service");
358        return new ArrayList<BluetoothDevice>();
359    }
360
361    /**
362     * {@inheritDoc}
363     */
364    public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
365        if (VDBG) log("getDevicesMatchingStates()");
366        if (mService != null && isEnabled()) {
367            try {
368                return mService.getDevicesMatchingConnectionStates(states);
369            } catch (RemoteException e) {
370                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
371                return new ArrayList<BluetoothDevice>();
372            }
373        }
374        if (mService == null) Log.w(TAG, "Proxy not attached to service");
375        return new ArrayList<BluetoothDevice>();
376    }
377
378    /**
379     * {@inheritDoc}
380     */
381    public int getConnectionState(BluetoothDevice device) {
382        if (VDBG) log("getState(" + device + ")");
383        if (mService != null && isEnabled() && isValidDevice(device)) {
384            try {
385                return mService.getConnectionState(device);
386            } catch (RemoteException e) {
387                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
388                return BluetoothProfile.STATE_DISCONNECTED;
389            }
390        }
391        if (mService == null) Log.w(TAG, "Proxy not attached to service");
392        return BluetoothProfile.STATE_DISCONNECTED;
393    }
394
395    /**
396     * Set priority of the profile
397     *
398     * <p> The device should already be paired.
399     *  Priority can be one of {@link #PRIORITY_ON} or
400     * {@link #PRIORITY_OFF},
401     *
402     * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
403     * permission.
404     *
405     * @param device Paired bluetooth device
406     * @param priority
407     * @return true if priority is set, false on error
408     * @hide
409     */
410    public boolean setPriority(BluetoothDevice device, int priority) {
411        if (DBG) log("setPriority(" + device + ", " + priority + ")");
412        if (mService != null && isEnabled() && isValidDevice(device)) {
413            if (priority != BluetoothProfile.PRIORITY_OFF &&
414                priority != BluetoothProfile.PRIORITY_ON) {
415              return false;
416            }
417            try {
418                return mService.setPriority(device, priority);
419            } catch (RemoteException e) {
420                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
421                return false;
422            }
423        }
424        if (mService == null) Log.w(TAG, "Proxy not attached to service");
425        return false;
426    }
427
428    /**
429     * Get the priority of the profile.
430     *
431     * <p> The priority can be any of:
432     * {@link #PRIORITY_AUTO_CONNECT}, {@link #PRIORITY_OFF},
433     * {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED}
434     *
435     * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
436     *
437     * @param device Bluetooth device
438     * @return priority of the device
439     * @hide
440     */
441    public int getPriority(BluetoothDevice device) {
442        if (VDBG) log("getPriority(" + device + ")");
443        if (mService != null && isEnabled() && isValidDevice(device)) {
444            try {
445                return mService.getPriority(device);
446            } catch (RemoteException e) {
447                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
448                return BluetoothProfile.PRIORITY_OFF;
449            }
450        }
451        if (mService == null) Log.w(TAG, "Proxy not attached to service");
452        return BluetoothProfile.PRIORITY_OFF;
453    }
454
455    private ServiceConnection mConnection = new ServiceConnection() {
456        public void onServiceConnected(ComponentName className, IBinder service) {
457            if (DBG) Log.d(TAG, "Proxy object connected");
458            mService = IBluetoothInputDevice.Stub.asInterface(service);
459
460            if (mServiceListener != null) {
461                mServiceListener.onServiceConnected(BluetoothProfile.INPUT_DEVICE, BluetoothInputDevice.this);
462            }
463        }
464        public void onServiceDisconnected(ComponentName className) {
465            if (DBG) Log.d(TAG, "Proxy object disconnected");
466            mService = null;
467            if (mServiceListener != null) {
468                mServiceListener.onServiceDisconnected(BluetoothProfile.INPUT_DEVICE);
469            }
470        }
471    };
472
473    private boolean isEnabled() {
474       if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true;
475       return false;
476    }
477
478    private boolean isValidDevice(BluetoothDevice device) {
479       if (device == null) return false;
480
481       if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true;
482       return false;
483    }
484
485
486    /**
487     * Initiate virtual unplug for a HID input device.
488     *
489     * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
490     *
491     * @param device Remote Bluetooth Device
492     * @return false on immediate error,
493     *               true otherwise
494     * @hide
495     */
496    public boolean virtualUnplug(BluetoothDevice device) {
497        if (DBG) log("virtualUnplug(" + device + ")");
498        if (mService != null && isEnabled() && isValidDevice(device)) {
499            try {
500                return mService.virtualUnplug(device);
501            } catch (RemoteException e) {
502                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
503                return false;
504            }
505        }
506
507        if (mService == null) Log.w(TAG, "Proxy not attached to service");
508        return false;
509
510    }
511
512    /**
513    * Send Get_Protocol_Mode command to the connected HID input device.
514    *
515    * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
516    *
517    * @param device Remote Bluetooth Device
518    * @return false on immediate error,
519    *true otherwise
520    * @hide
521    */
522    public boolean getProtocolMode(BluetoothDevice device) {
523        if (VDBG) log("getProtocolMode(" + device + ")");
524        if (mService != null && isEnabled() && isValidDevice(device)) {
525            try {
526                return mService.getProtocolMode(device);
527            } catch (RemoteException e) {
528                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
529                return false;
530            }
531        }
532        if (mService == null) Log.w(TAG, "Proxy not attached to service");
533            return false;
534    }
535
536    /**
537     * Send Set_Protocol_Mode command to the connected HID input device.
538     *
539     * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
540     *
541     * @param device Remote Bluetooth Device
542     * @return false on immediate error,
543     *               true otherwise
544     * @hide
545     */
546    public boolean setProtocolMode(BluetoothDevice device, int protocolMode) {
547        if (DBG) log("setProtocolMode(" + device + ")");
548        if (mService != null && isEnabled() && isValidDevice(device)) {
549            try {
550                return mService.setProtocolMode(device, protocolMode);
551            } catch (RemoteException e) {
552                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
553                return false;
554            }
555        }
556        if (mService == null) Log.w(TAG, "Proxy not attached to service");
557        return false;
558    }
559
560    /**
561     * Send Get_Report command to the connected HID input device.
562     *
563     * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
564     *
565     * @param device Remote Bluetooth Device
566     * @param reportType Report type
567     * @param reportId Report ID
568     * @param bufferSize Report receiving buffer size
569     * @return false on immediate error,
570     *               true otherwise
571     * @hide
572     */
573    public boolean getReport(BluetoothDevice device, byte reportType, byte reportId, int bufferSize) {
574        if (VDBG) log("getReport(" + device + "), reportType=" + reportType + " reportId=" + reportId + "bufferSize=" + bufferSize);
575        if (mService != null && isEnabled() && isValidDevice(device)) {
576            try {
577                return mService.getReport(device, reportType, reportId, bufferSize);
578            } catch (RemoteException e) {
579                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
580                return false;
581            }
582        }
583        if (mService == null) Log.w(TAG, "Proxy not attached to service");
584        return false;
585    }
586
587    /**
588     * Send Set_Report command to the connected HID input device.
589     *
590     * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
591     *
592     * @param device Remote Bluetooth Device
593     * @param reportType Report type
594     * @param report Report receiving buffer size
595     * @return false on immediate error,
596     *               true otherwise
597     * @hide
598     */
599    public boolean setReport(BluetoothDevice device, byte reportType, String report) {
600        if (DBG) log("setReport(" + device + "), reportType=" + reportType + " report=" + report);
601        if (mService != null && isEnabled() && isValidDevice(device)) {
602            try {
603                return mService.setReport(device, reportType, report);
604            } catch (RemoteException e) {
605                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
606                return false;
607            }
608        }
609        if (mService == null) Log.w(TAG, "Proxy not attached to service");
610        return false;
611    }
612
613    /**
614     * Send Send_Data command to the connected HID input device.
615     *
616     * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
617     *
618     * @param device Remote Bluetooth Device
619     * @param data Data to send
620     * @return false on immediate error,
621     *               true otherwise
622     * @hide
623     */
624    public boolean sendData(BluetoothDevice device, String report) {
625        if (DBG) log("sendData(" + device + "), report=" + report);
626        if (mService != null && isEnabled() && isValidDevice(device)) {
627            try {
628                return mService.sendData(device, report);
629            } catch (RemoteException e) {
630                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
631                return false;
632            }
633        }
634        if (mService == null) Log.w(TAG, "Proxy not attached to service");
635        return false;
636    }
637    private static void log(String msg) {
638      Log.d(TAG, msg);
639    }
640}
641