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