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                                    doBind();
210                                }
211                            } catch (Exception re) {
212                                Log.e(TAG,"",re);
213                            }
214                        }
215                    }
216                }
217        };
218
219    /**
220     * Create a BluetoothInputDevice proxy object for interacting with the local
221     * Bluetooth Service which handles the InputDevice profile
222     *
223     */
224    /*package*/ BluetoothInputDevice(Context context, ServiceListener l) {
225        mContext = context;
226        mServiceListener = l;
227        mAdapter = BluetoothAdapter.getDefaultAdapter();
228
229        IBluetoothManager mgr = mAdapter.getBluetoothManager();
230        if (mgr != null) {
231            try {
232                mgr.registerStateChangeCallback(mBluetoothStateChangeCallback);
233            } catch (RemoteException e) {
234                Log.e(TAG,"",e);
235            }
236        }
237
238        doBind();
239    }
240
241    boolean doBind() {
242        Intent intent = new Intent(IBluetoothInputDevice.class.getName());
243        ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0);
244        intent.setComponent(comp);
245        if (comp == null || !mContext.bindService(intent, mConnection, 0)) {
246            Log.e(TAG, "Could not bind to Bluetooth HID Service with " + intent);
247            return false;
248        }
249        return true;
250    }
251
252    /*package*/ void close() {
253        if (VDBG) log("close()");
254        IBluetoothManager mgr = mAdapter.getBluetoothManager();
255        if (mgr != null) {
256            try {
257                mgr.unregisterStateChangeCallback(mBluetoothStateChangeCallback);
258            } catch (Exception e) {
259                Log.e(TAG,"",e);
260            }
261        }
262
263        synchronized (mConnection) {
264            if (mService != null) {
265                try {
266                    mService = null;
267                    mContext.unbindService(mConnection);
268                } catch (Exception re) {
269                    Log.e(TAG,"",re);
270                }
271           }
272        }
273        mServiceListener = null;
274    }
275
276    /**
277     * Initiate connection to a profile of the remote bluetooth device.
278     *
279     * <p> The system supports connection to multiple input devices.
280     *
281     * <p> This API returns false in scenarios like the profile on the
282     * device is already connected or Bluetooth is not turned on.
283     * When this API returns true, it is guaranteed that
284     * connection state intent for the profile will be broadcasted with
285     * the state. Users can get the connection state of the profile
286     * from this intent.
287     *
288     * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
289     * permission.
290     *
291     * @param device Remote Bluetooth Device
292     * @return false on immediate error,
293     *               true otherwise
294     * @hide
295     */
296    public boolean connect(BluetoothDevice device) {
297        if (DBG) log("connect(" + device + ")");
298        if (mService != null && isEnabled() && isValidDevice(device)) {
299            try {
300                return mService.connect(device);
301            } catch (RemoteException e) {
302                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
303                return false;
304            }
305        }
306        if (mService == null) Log.w(TAG, "Proxy not attached to service");
307        return false;
308    }
309
310    /**
311     * Initiate disconnection from a profile
312     *
313     * <p> This API will return false in scenarios like the profile on the
314     * Bluetooth device is not in connected state etc. When this API returns,
315     * true, it is guaranteed that the connection state change
316     * intent will be broadcasted with the state. Users can get the
317     * disconnection state of the profile from this intent.
318     *
319     * <p> If the disconnection is initiated by a remote device, the state
320     * will transition from {@link #STATE_CONNECTED} to
321     * {@link #STATE_DISCONNECTED}. If the disconnect is initiated by the
322     * host (local) device the state will transition from
323     * {@link #STATE_CONNECTED} to state {@link #STATE_DISCONNECTING} to
324     * state {@link #STATE_DISCONNECTED}. The transition to
325     * {@link #STATE_DISCONNECTING} can be used to distinguish between the
326     * two scenarios.
327     *
328     * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
329     * permission.
330     *
331     * @param device Remote Bluetooth Device
332     * @return false on immediate error,
333     *               true otherwise
334     * @hide
335     */
336    public boolean disconnect(BluetoothDevice device) {
337        if (DBG) log("disconnect(" + device + ")");
338        if (mService != null && isEnabled() && isValidDevice(device)) {
339            try {
340                return mService.disconnect(device);
341            } catch (RemoteException e) {
342                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
343                return false;
344            }
345        }
346        if (mService == null) Log.w(TAG, "Proxy not attached to service");
347        return false;
348    }
349
350    /**
351     * {@inheritDoc}
352     */
353    public List<BluetoothDevice> getConnectedDevices() {
354        if (VDBG) log("getConnectedDevices()");
355        if (mService != null && isEnabled()) {
356            try {
357                return mService.getConnectedDevices();
358            } catch (RemoteException e) {
359                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
360                return new ArrayList<BluetoothDevice>();
361            }
362        }
363        if (mService == null) Log.w(TAG, "Proxy not attached to service");
364        return new ArrayList<BluetoothDevice>();
365    }
366
367    /**
368     * {@inheritDoc}
369     */
370    public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
371        if (VDBG) log("getDevicesMatchingStates()");
372        if (mService != null && isEnabled()) {
373            try {
374                return mService.getDevicesMatchingConnectionStates(states);
375            } catch (RemoteException e) {
376                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
377                return new ArrayList<BluetoothDevice>();
378            }
379        }
380        if (mService == null) Log.w(TAG, "Proxy not attached to service");
381        return new ArrayList<BluetoothDevice>();
382    }
383
384    /**
385     * {@inheritDoc}
386     */
387    public int getConnectionState(BluetoothDevice device) {
388        if (VDBG) log("getState(" + device + ")");
389        if (mService != null && isEnabled() && isValidDevice(device)) {
390            try {
391                return mService.getConnectionState(device);
392            } catch (RemoteException e) {
393                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
394                return BluetoothProfile.STATE_DISCONNECTED;
395            }
396        }
397        if (mService == null) Log.w(TAG, "Proxy not attached to service");
398        return BluetoothProfile.STATE_DISCONNECTED;
399    }
400
401    /**
402     * Set priority of the profile
403     *
404     * <p> The device should already be paired.
405     *  Priority can be one of {@link #PRIORITY_ON} or
406     * {@link #PRIORITY_OFF},
407     *
408     * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
409     * permission.
410     *
411     * @param device Paired bluetooth device
412     * @param priority
413     * @return true if priority is set, false on error
414     * @hide
415     */
416    public boolean setPriority(BluetoothDevice device, int priority) {
417        if (DBG) log("setPriority(" + device + ", " + priority + ")");
418        if (mService != null && isEnabled() && isValidDevice(device)) {
419            if (priority != BluetoothProfile.PRIORITY_OFF &&
420                priority != BluetoothProfile.PRIORITY_ON) {
421              return false;
422            }
423            try {
424                return mService.setPriority(device, priority);
425            } catch (RemoteException e) {
426                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
427                return false;
428            }
429        }
430        if (mService == null) Log.w(TAG, "Proxy not attached to service");
431        return false;
432    }
433
434    /**
435     * Get the priority of the profile.
436     *
437     * <p> The priority can be any of:
438     * {@link #PRIORITY_AUTO_CONNECT}, {@link #PRIORITY_OFF},
439     * {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED}
440     *
441     * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
442     *
443     * @param device Bluetooth device
444     * @return priority of the device
445     * @hide
446     */
447    public int getPriority(BluetoothDevice device) {
448        if (VDBG) log("getPriority(" + device + ")");
449        if (mService != null && isEnabled() && isValidDevice(device)) {
450            try {
451                return mService.getPriority(device);
452            } catch (RemoteException e) {
453                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
454                return BluetoothProfile.PRIORITY_OFF;
455            }
456        }
457        if (mService == null) Log.w(TAG, "Proxy not attached to service");
458        return BluetoothProfile.PRIORITY_OFF;
459    }
460
461    private final ServiceConnection mConnection = new ServiceConnection() {
462        public void onServiceConnected(ComponentName className, IBinder service) {
463            if (DBG) Log.d(TAG, "Proxy object connected");
464            mService = IBluetoothInputDevice.Stub.asInterface(service);
465
466            if (mServiceListener != null) {
467                mServiceListener.onServiceConnected(BluetoothProfile.INPUT_DEVICE, BluetoothInputDevice.this);
468            }
469        }
470        public void onServiceDisconnected(ComponentName className) {
471            if (DBG) Log.d(TAG, "Proxy object disconnected");
472            mService = null;
473            if (mServiceListener != null) {
474                mServiceListener.onServiceDisconnected(BluetoothProfile.INPUT_DEVICE);
475            }
476        }
477    };
478
479    private boolean isEnabled() {
480       if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true;
481       return false;
482    }
483
484    private boolean isValidDevice(BluetoothDevice device) {
485       if (device == null) return false;
486
487       if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true;
488       return false;
489    }
490
491
492    /**
493     * Initiate virtual unplug for a HID input device.
494     *
495     * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
496     *
497     * @param device Remote Bluetooth Device
498     * @return false on immediate error,
499     *               true otherwise
500     * @hide
501     */
502    public boolean virtualUnplug(BluetoothDevice device) {
503        if (DBG) log("virtualUnplug(" + device + ")");
504        if (mService != null && isEnabled() && isValidDevice(device)) {
505            try {
506                return mService.virtualUnplug(device);
507            } catch (RemoteException e) {
508                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
509                return false;
510            }
511        }
512
513        if (mService == null) Log.w(TAG, "Proxy not attached to service");
514        return false;
515
516    }
517
518    /**
519    * Send Get_Protocol_Mode command to the connected HID input device.
520    *
521    * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
522    *
523    * @param device Remote Bluetooth Device
524    * @return false on immediate error,
525    *true otherwise
526    * @hide
527    */
528    public boolean getProtocolMode(BluetoothDevice device) {
529        if (VDBG) log("getProtocolMode(" + device + ")");
530        if (mService != null && isEnabled() && isValidDevice(device)) {
531            try {
532                return mService.getProtocolMode(device);
533            } catch (RemoteException e) {
534                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
535                return false;
536            }
537        }
538        if (mService == null) Log.w(TAG, "Proxy not attached to service");
539            return false;
540    }
541
542    /**
543     * Send Set_Protocol_Mode command to the connected HID input device.
544     *
545     * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
546     *
547     * @param device Remote Bluetooth Device
548     * @return false on immediate error,
549     *               true otherwise
550     * @hide
551     */
552    public boolean setProtocolMode(BluetoothDevice device, int protocolMode) {
553        if (DBG) log("setProtocolMode(" + device + ")");
554        if (mService != null && isEnabled() && isValidDevice(device)) {
555            try {
556                return mService.setProtocolMode(device, protocolMode);
557            } catch (RemoteException e) {
558                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
559                return false;
560            }
561        }
562        if (mService == null) Log.w(TAG, "Proxy not attached to service");
563        return false;
564    }
565
566    /**
567     * Send Get_Report command to the connected HID input device.
568     *
569     * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
570     *
571     * @param device Remote Bluetooth Device
572     * @param reportType Report type
573     * @param reportId Report ID
574     * @param bufferSize Report receiving buffer size
575     * @return false on immediate error,
576     *               true otherwise
577     * @hide
578     */
579    public boolean getReport(BluetoothDevice device, byte reportType, byte reportId, int bufferSize) {
580        if (VDBG) log("getReport(" + device + "), reportType=" + reportType + " reportId=" + reportId + "bufferSize=" + bufferSize);
581        if (mService != null && isEnabled() && isValidDevice(device)) {
582            try {
583                return mService.getReport(device, reportType, reportId, bufferSize);
584            } catch (RemoteException e) {
585                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
586                return false;
587            }
588        }
589        if (mService == null) Log.w(TAG, "Proxy not attached to service");
590        return false;
591    }
592
593    /**
594     * Send Set_Report command to the connected HID input device.
595     *
596     * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
597     *
598     * @param device Remote Bluetooth Device
599     * @param reportType Report type
600     * @param report Report receiving buffer size
601     * @return false on immediate error,
602     *               true otherwise
603     * @hide
604     */
605    public boolean setReport(BluetoothDevice device, byte reportType, String report) {
606        if (DBG) log("setReport(" + device + "), reportType=" + reportType + " report=" + report);
607        if (mService != null && isEnabled() && isValidDevice(device)) {
608            try {
609                return mService.setReport(device, reportType, report);
610            } catch (RemoteException e) {
611                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
612                return false;
613            }
614        }
615        if (mService == null) Log.w(TAG, "Proxy not attached to service");
616        return false;
617    }
618
619    /**
620     * Send Send_Data command to the connected HID input device.
621     *
622     * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
623     *
624     * @param device Remote Bluetooth Device
625     * @param data Data to send
626     * @return false on immediate error,
627     *               true otherwise
628     * @hide
629     */
630    public boolean sendData(BluetoothDevice device, String report) {
631        if (DBG) log("sendData(" + device + "), report=" + report);
632        if (mService != null && isEnabled() && isValidDevice(device)) {
633            try {
634                return mService.sendData(device, report);
635            } catch (RemoteException e) {
636                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
637                return false;
638            }
639        }
640        if (mService == null) Log.w(TAG, "Proxy not attached to service");
641        return false;
642    }
643    private static void log(String msg) {
644      Log.d(TAG, msg);
645    }
646}
647