1/*
2 * Copyright (C) 2008 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.RemoteException;
26import android.os.IBinder;
27import android.util.Log;
28
29/**
30 * The Android Bluetooth API is not finalized, and *will* change. Use at your
31 * own risk.
32 *
33 * Public API for controlling the Bluetooth Headset Service. This includes both
34 * Bluetooth Headset and Handsfree (v1.5) profiles. The Headset service will
35 * attempt a handsfree connection first, and fall back to headset.
36 *
37 * BluetoothHeadset is a proxy object for controlling the Bluetooth Headset
38 * Service via IPC.
39 *
40 * Creating a BluetoothHeadset object will create a binding with the
41 * BluetoothHeadset service. Users of this object should call close() when they
42 * are finished with the BluetoothHeadset, so that this proxy object can unbind
43 * from the service.
44 *
45 * This BluetoothHeadset object is not immediately bound to the
46 * BluetoothHeadset service. Use the ServiceListener interface to obtain a
47 * notification when it is bound, this is especially important if you wish to
48 * immediately call methods on BluetoothHeadset after construction.
49 *
50 * Android only supports one connected Bluetooth Headset at a time.
51 *
52 * @hide
53 */
54public final class BluetoothHeadset {
55
56    private static final String TAG = "BluetoothHeadset";
57    private static final boolean DBG = false;
58
59    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
60    public static final String ACTION_STATE_CHANGED =
61            "android.bluetooth.headset.action.STATE_CHANGED";
62    /**
63     * TODO(API release): Consider incorporating as new state in
64     * HEADSET_STATE_CHANGED
65     */
66    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
67    public static final String ACTION_AUDIO_STATE_CHANGED =
68            "android.bluetooth.headset.action.AUDIO_STATE_CHANGED";
69    public static final String EXTRA_STATE =
70            "android.bluetooth.headset.extra.STATE";
71    public static final String EXTRA_PREVIOUS_STATE =
72            "android.bluetooth.headset.extra.PREVIOUS_STATE";
73    public static final String EXTRA_AUDIO_STATE =
74            "android.bluetooth.headset.extra.AUDIO_STATE";
75
76    /** Extra to be used with the Headset State change intent.
77     * This will be used only when Headset state changes to
78     * {@link #STATE_DISCONNECTED} from any previous state.
79     * This extra field is optional and will be used when
80     * we have deterministic information regarding whether
81     * the disconnect was initiated by the remote device or
82     * by the local adapter.
83     */
84    public static final String EXTRA_DISCONNECT_INITIATOR =
85            "android.bluetooth.headset.extra.DISCONNECT_INITIATOR";
86
87    /**
88     * TODO(API release): Consider incorporating as new state in
89     * HEADSET_STATE_CHANGED
90     */
91    private IBluetoothHeadset mService;
92    private final Context mContext;
93    private final ServiceListener mServiceListener;
94
95    /** There was an error trying to obtain the state */
96    public static final int STATE_ERROR        = -1;
97    /** No headset currently connected */
98    public static final int STATE_DISCONNECTED = 0;
99    /** Connection attempt in progress */
100    public static final int STATE_CONNECTING   = 1;
101    /** A headset is currently connected */
102    public static final int STATE_CONNECTED    = 2;
103
104    /** A SCO audio channel is not established */
105    public static final int AUDIO_STATE_DISCONNECTED = 0;
106    /** A SCO audio channel is established */
107    public static final int AUDIO_STATE_CONNECTED = 1;
108
109    public static final int RESULT_FAILURE = 0;
110    public static final int RESULT_SUCCESS = 1;
111    /** Connection canceled before completion. */
112    public static final int RESULT_CANCELED = 2;
113
114    /** Values for {@link #EXTRA_DISCONNECT_INITIATOR} */
115    public static final int REMOTE_DISCONNECT = 0;
116    public static final int LOCAL_DISCONNECT = 1;
117
118
119    /** Default priority for headsets for which we will accept
120     * incoming connections and auto-connect. */
121    public static final int PRIORITY_AUTO_CONNECT = 1000;
122    /** Default priority for headsets for which we will accept
123     * incoming connections but not auto-connect. */
124    public static final int PRIORITY_ON = 100;
125    /** Default priority for headsets that should not be auto-connected
126     * and not allow incoming connections. */
127    public static final int PRIORITY_OFF = 0;
128    /** Default priority when not set or when the device is unpaired */
129    public static final int PRIORITY_UNDEFINED = -1;
130
131    /**
132     * An interface for notifying BluetoothHeadset IPC clients when they have
133     * been connected to the BluetoothHeadset service.
134     */
135    public interface ServiceListener {
136        /**
137         * Called to notify the client when this proxy object has been
138         * connected to the BluetoothHeadset service. Clients must wait for
139         * this callback before making IPC calls on the BluetoothHeadset
140         * service.
141         */
142        public void onServiceConnected();
143
144        /**
145         * Called to notify the client that this proxy object has been
146         * disconnected from the BluetoothHeadset service. Clients must not
147         * make IPC calls on the BluetoothHeadset service after this callback.
148         * This callback will currently only occur if the application hosting
149         * the BluetoothHeadset service, but may be called more often in future.
150         */
151        public void onServiceDisconnected();
152    }
153
154    /**
155     * Create a BluetoothHeadset proxy object.
156     */
157    public BluetoothHeadset(Context context, ServiceListener l) {
158        mContext = context;
159        mServiceListener = l;
160        if (!context.bindService(new Intent(IBluetoothHeadset.class.getName()), mConnection, 0)) {
161            Log.e(TAG, "Could not bind to Bluetooth Headset Service");
162        }
163    }
164
165    protected void finalize() throws Throwable {
166        try {
167            close();
168        } finally {
169            super.finalize();
170        }
171    }
172
173    /**
174     * Close the connection to the backing service.
175     * Other public functions of BluetoothHeadset will return default error
176     * results once close() has been called. Multiple invocations of close()
177     * are ok.
178     */
179    public synchronized void close() {
180        if (DBG) log("close()");
181        if (mConnection != null) {
182            mContext.unbindService(mConnection);
183            mConnection = null;
184        }
185    }
186
187    /**
188     * Get the current state of the Bluetooth Headset service.
189     * @return One of the STATE_ return codes, or STATE_ERROR if this proxy
190     *         object is currently not connected to the Headset service.
191     */
192    public int getState(BluetoothDevice device) {
193        if (DBG) log("getState()");
194        if (mService != null) {
195            try {
196                return mService.getState(device);
197            } catch (RemoteException e) {Log.e(TAG, e.toString());}
198        } else {
199            Log.w(TAG, "Proxy not attached to service");
200            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
201        }
202        return BluetoothHeadset.STATE_ERROR;
203    }
204
205    /**
206     * Get the BluetoothDevice for the current headset.
207     * @return current headset, or null if not in connected or connecting
208     *         state, or if this proxy object is not connected to the Headset
209     *         service.
210     */
211    public BluetoothDevice getCurrentHeadset() {
212        if (DBG) log("getCurrentHeadset()");
213        if (mService != null) {
214            try {
215                return mService.getCurrentHeadset();
216            } catch (RemoteException e) {Log.e(TAG, e.toString());}
217        } else {
218            Log.w(TAG, "Proxy not attached to service");
219            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
220        }
221        return null;
222    }
223
224    /**
225     * Request to initiate a connection to a headset.
226     * This call does not block. Fails if a headset is already connecting
227     * or connected.
228     * Initiates auto-connection if device is null. Tries to connect to all
229     * devices with priority greater than PRIORITY_AUTO in descending order.
230     * @param device device to connect to, or null to auto-connect last connected
231     *               headset
232     * @return       false if there was a problem initiating the connection
233     *               procedure, and no further HEADSET_STATE_CHANGED intents
234     *               will be expected.
235     */
236    public boolean connectHeadset(BluetoothDevice device) {
237        if (DBG) log("connectHeadset(" + device + ")");
238        if (mService != null) {
239            try {
240                if (mService.connectHeadset(device)) {
241                    return true;
242                }
243            } catch (RemoteException e) {Log.e(TAG, e.toString());}
244        } else {
245            Log.w(TAG, "Proxy not attached to service");
246            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
247        }
248        return false;
249    }
250
251    /**
252     * Returns true if the specified headset is connected (does not include
253     * connecting). Returns false if not connected, or if this proxy object
254     * if not currently connected to the headset service.
255     */
256    public boolean isConnected(BluetoothDevice device) {
257        if (DBG) log("isConnected(" + device + ")");
258        if (mService != null) {
259            try {
260                return mService.isConnected(device);
261            } catch (RemoteException e) {Log.e(TAG, e.toString());}
262        } else {
263            Log.w(TAG, "Proxy not attached to service");
264            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
265        }
266        return false;
267    }
268
269    /**
270     * Disconnects the current headset. Currently this call blocks, it may soon
271     * be made asynchronous. Returns false if this proxy object is
272     * not currently connected to the Headset service.
273     */
274    public boolean disconnectHeadset(BluetoothDevice device) {
275        if (DBG) log("disconnectHeadset()");
276        if (mService != null) {
277            try {
278                mService.disconnectHeadset(device);
279                return true;
280            } catch (RemoteException e) {Log.e(TAG, e.toString());}
281        } else {
282            Log.w(TAG, "Proxy not attached to service");
283            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
284        }
285        return false;
286    }
287
288    /**
289     * Start BT Voice Recognition mode, and set up Bluetooth audio path.
290     * Returns false if there is no headset connected, or if the
291     * connected headset does not support voice recognition, or on
292     * error.
293     */
294    public boolean startVoiceRecognition() {
295        if (DBG) log("startVoiceRecognition()");
296        if (mService != null) {
297            try {
298                return mService.startVoiceRecognition();
299            } catch (RemoteException e) {Log.e(TAG, e.toString());}
300        } else {
301            Log.w(TAG, "Proxy not attached to service");
302            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
303        }
304        return false;
305    }
306
307    /**
308     * Stop BT Voice Recognition mode, and shut down Bluetooth audio path.
309     * Returns false if there is no headset connected, or the connected
310     * headset is not in voice recognition mode, or on error.
311     */
312    public boolean stopVoiceRecognition() {
313        if (DBG) log("stopVoiceRecognition()");
314        if (mService != null) {
315            try {
316                return mService.stopVoiceRecognition();
317            } catch (RemoteException e) {Log.e(TAG, e.toString());}
318        } else {
319            Log.w(TAG, "Proxy not attached to service");
320            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
321        }
322        return false;
323    }
324
325    /**
326     * Set priority of headset.
327     * Priority is a non-negative integer. By default paired headsets will have
328     * a priority of PRIORITY_AUTO, and unpaired headset PRIORITY_NONE (0).
329     * Headsets with priority greater than zero will be auto-connected, and
330     * incoming connections will be accepted (if no other headset is
331     * connected).
332     * Auto-connection occurs at the following events: boot, incoming phone
333     * call, outgoing phone call.
334     * Headsets with priority equal to zero, or that are unpaired, are not
335     * auto-connected.
336     * Incoming connections are ignored regardless of priority if there is
337     * already a headset connected.
338     * @param device paired headset
339     * @param priority Integer priority, for example PRIORITY_AUTO or
340     *                 PRIORITY_NONE
341     * @return true if successful, false if there was some error
342     */
343    public boolean setPriority(BluetoothDevice device, int priority) {
344        if (DBG) log("setPriority(" + device + ", " + priority + ")");
345        if (mService != null) {
346            try {
347                return mService.setPriority(device, priority);
348            } catch (RemoteException e) {Log.e(TAG, e.toString());}
349        } else {
350            Log.w(TAG, "Proxy not attached to service");
351            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
352        }
353        return false;
354    }
355
356    /**
357     * Get priority of headset.
358     * @param device headset
359     * @return non-negative priority, or negative error code on error
360     */
361    public int getPriority(BluetoothDevice device) {
362        if (DBG) log("getPriority(" + device + ")");
363        if (mService != null) {
364            try {
365                return mService.getPriority(device);
366            } catch (RemoteException e) {Log.e(TAG, e.toString());}
367        } else {
368            Log.w(TAG, "Proxy not attached to service");
369            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
370        }
371        return -1;
372    }
373
374    /**
375     * Get battery usage hint for Bluetooth Headset service.
376     * This is a monotonically increasing integer. Wraps to 0 at
377     * Integer.MAX_INT, and at boot.
378     * Current implementation returns the number of AT commands handled since
379     * boot. This is a good indicator for spammy headset/handsfree units that
380     * can keep the device awake by polling for cellular status updates. As a
381     * rule of thumb, each AT command prevents the CPU from sleeping for 500 ms
382     * @return monotonically increasing battery usage hint, or a negative error
383     *         code on error
384     * @hide
385     */
386    public int getBatteryUsageHint() {
387        if (DBG) log("getBatteryUsageHint()");
388        if (mService != null) {
389            try {
390                return mService.getBatteryUsageHint();
391            } catch (RemoteException e) {Log.e(TAG, e.toString());}
392        } else {
393            Log.w(TAG, "Proxy not attached to service");
394            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
395        }
396        return -1;
397    }
398    /**
399     * Indicates if current platform supports voice dialing over bluetooth SCO.
400     * @return true if voice dialing over bluetooth is supported, false otherwise.
401     * @hide
402     */
403    public static boolean isBluetoothVoiceDialingEnabled(Context context) {
404        return context.getResources().getBoolean(
405                com.android.internal.R.bool.config_bluetooth_sco_off_call);
406    }
407
408    /**
409     * Cancel the outgoing connection.
410     * @hide
411     */
412    public boolean cancelConnectThread() {
413        if (DBG) log("cancelConnectThread");
414        if (mService != null) {
415            try {
416                return mService.cancelConnectThread();
417            } catch (RemoteException e) {Log.e(TAG, e.toString());}
418        } else {
419            Log.w(TAG, "Proxy not attached to service");
420            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
421        }
422        return false;
423    }
424
425    /**
426     * Accept the incoming connection.
427     * @hide
428     */
429    public boolean acceptIncomingConnect(BluetoothDevice device) {
430        if (DBG) log("acceptIncomingConnect");
431        if (mService != null) {
432            try {
433                return mService.acceptIncomingConnect(device);
434            } catch (RemoteException e) {Log.e(TAG, e.toString());}
435        } else {
436            Log.w(TAG, "Proxy not attached to service");
437            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
438        }
439        return false;
440    }
441
442    /**
443     * Create the connect thread the incoming connection.
444     * @hide
445     */
446    public boolean createIncomingConnect(BluetoothDevice device) {
447        if (DBG) log("createIncomingConnect");
448        if (mService != null) {
449            try {
450                return mService.createIncomingConnect(device);
451            } catch (RemoteException e) {Log.e(TAG, e.toString());}
452        } else {
453            Log.w(TAG, "Proxy not attached to service");
454            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
455        }
456        return false;
457    }
458
459    /**
460     * Reject the incoming connection.
461     * @hide
462     */
463    public boolean rejectIncomingConnect(BluetoothDevice device) {
464        if (DBG) log("rejectIncomingConnect");
465        if (mService != null) {
466            try {
467                return mService.rejectIncomingConnect(device);
468            } catch (RemoteException e) {Log.e(TAG, e.toString());}
469        } else {
470            Log.w(TAG, "Proxy not attached to service");
471            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
472        }
473        return false;
474    }
475
476    /**
477     * Connect to a Bluetooth Headset.
478     * Note: This is an internal function and shouldn't be exposed
479     * @hide
480     */
481    public boolean connectHeadsetInternal(BluetoothDevice device) {
482        if (DBG) log("connectHeadsetInternal");
483        if (mService != null) {
484            try {
485                return mService.connectHeadsetInternal(device);
486            } catch (RemoteException e) {Log.e(TAG, e.toString());}
487        } else {
488            Log.w(TAG, "Proxy not attached to service");
489            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
490        }
491        return false;
492    }
493
494    /**
495     * Disconnect a Bluetooth Headset.
496     * Note: This is an internal function and shouldn't be exposed
497     * @hide
498     */
499    public boolean disconnectHeadsetInternal(BluetoothDevice device) {
500        if (DBG) log("disconnectHeadsetInternal");
501        if (mService != null) {
502            try {
503                 return mService.disconnectHeadsetInternal(device);
504            } catch (RemoteException e) {Log.e(TAG, e.toString());}
505        } else {
506            Log.w(TAG, "Proxy not attached to service");
507            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
508        }
509        return false;
510    }
511    private ServiceConnection mConnection = new ServiceConnection() {
512        public void onServiceConnected(ComponentName className, IBinder service) {
513            if (DBG) Log.d(TAG, "Proxy object connected");
514            mService = IBluetoothHeadset.Stub.asInterface(service);
515            if (mServiceListener != null) {
516                mServiceListener.onServiceConnected();
517            }
518        }
519        public void onServiceDisconnected(ComponentName className) {
520            if (DBG) Log.d(TAG, "Proxy object disconnected");
521            mService = null;
522            if (mServiceListener != null) {
523                mServiceListener.onServiceDisconnected();
524            }
525        }
526    };
527
528    private static void log(String msg) {
529        Log.d(TAG, msg);
530    }
531}
532