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 BluetootHeadset 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    /**
77     * TODO(API release): Consider incorporating as new state in
78     * HEADSET_STATE_CHANGED
79     */
80    private IBluetoothHeadset mService;
81    private final Context mContext;
82    private final ServiceListener mServiceListener;
83
84    /** There was an error trying to obtain the state */
85    public static final int STATE_ERROR        = -1;
86    /** No headset currently connected */
87    public static final int STATE_DISCONNECTED = 0;
88    /** Connection attempt in progress */
89    public static final int STATE_CONNECTING   = 1;
90    /** A headset is currently connected */
91    public static final int STATE_CONNECTED    = 2;
92
93    /** A SCO audio channel is not established */
94    public static final int AUDIO_STATE_DISCONNECTED = 0;
95    /** A SCO audio channel is established */
96    public static final int AUDIO_STATE_CONNECTED = 1;
97
98    public static final int RESULT_FAILURE = 0;
99    public static final int RESULT_SUCCESS = 1;
100    /** Connection canceled before completetion. */
101    public static final int RESULT_CANCELED = 2;
102
103    /** Default priority for headsets that  for which we will accept
104     * inconing connections and auto-connect */
105    public static final int PRIORITY_AUTO_CONNECT = 1000;
106    /** Default priority for headsets that  for which we will accept
107     * inconing connections but not auto-connect */
108    public static final int PRIORITY_ON = 100;
109    /** Default priority for headsets that should not be auto-connected
110     * and not allow incoming connections. */
111    public static final int PRIORITY_OFF = 0;
112    /** Default priority when not set or when the device is unpaired */
113    public static final int PRIORITY_UNDEFINED = -1;
114
115    /** The voice dialer 'works' but the user experience is poor. The voice
116     *  recognizer has trouble dealing with the 8kHz SCO signal, and it still
117     *  requires visual confirmation. Disable for cupcake.
118     */
119    public static final boolean DISABLE_BT_VOICE_DIALING = true;
120
121    /**
122     * An interface for notifying BluetoothHeadset IPC clients when they have
123     * been connected to the BluetoothHeadset service.
124     */
125    public interface ServiceListener {
126        /**
127         * Called to notify the client when this proxy object has been
128         * connected to the BluetoothHeadset service. Clients must wait for
129         * this callback before making IPC calls on the BluetoothHeadset
130         * service.
131         */
132        public void onServiceConnected();
133
134        /**
135         * Called to notify the client that this proxy object has been
136         * disconnected from the BluetoothHeadset service. Clients must not
137         * make IPC calls on the BluetoothHeadset service after this callback.
138         * This callback will currently only occur if the application hosting
139         * the BluetoothHeadset service, but may be called more often in future.
140         */
141        public void onServiceDisconnected();
142    }
143
144    /**
145     * Create a BluetoothHeadset proxy object.
146     */
147    public BluetoothHeadset(Context context, ServiceListener l) {
148        mContext = context;
149        mServiceListener = l;
150        if (!context.bindService(new Intent(IBluetoothHeadset.class.getName()), mConnection, 0)) {
151            Log.e(TAG, "Could not bind to Bluetooth Headset Service");
152        }
153    }
154
155    protected void finalize() throws Throwable {
156        try {
157            close();
158        } finally {
159            super.finalize();
160        }
161    }
162
163    /**
164     * Close the connection to the backing service.
165     * Other public functions of BluetoothHeadset will return default error
166     * results once close() has been called. Multiple invocations of close()
167     * are ok.
168     */
169    public synchronized void close() {
170        if (DBG) log("close()");
171        if (mConnection != null) {
172            mContext.unbindService(mConnection);
173            mConnection = null;
174        }
175    }
176
177    /**
178     * Get the current state of the Bluetooth Headset service.
179     * @return One of the STATE_ return codes, or STATE_ERROR if this proxy
180     *         object is currently not connected to the Headset service.
181     */
182    public int getState() {
183        if (DBG) log("getState()");
184        if (mService != null) {
185            try {
186                return mService.getState();
187            } catch (RemoteException e) {Log.e(TAG, e.toString());}
188        } else {
189            Log.w(TAG, "Proxy not attached to service");
190            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
191        }
192        return BluetoothHeadset.STATE_ERROR;
193    }
194
195    /**
196     * Get the BluetoothDevice for the current headset.
197     * @return current headset, or null if not in connected or connecting
198     *         state, or if this proxy object is not connected to the Headset
199     *         service.
200     */
201    public BluetoothDevice getCurrentHeadset() {
202        if (DBG) log("getCurrentHeadset()");
203        if (mService != null) {
204            try {
205                return mService.getCurrentHeadset();
206            } catch (RemoteException e) {Log.e(TAG, e.toString());}
207        } else {
208            Log.w(TAG, "Proxy not attached to service");
209            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
210        }
211        return null;
212    }
213
214    /**
215     * Request to initiate a connection to a headset.
216     * This call does not block. Fails if a headset is already connecting
217     * or connected.
218     * Initiates auto-connection if device is null. Tries to connect to all
219     * devices with priority greater than PRIORITY_AUTO in descending order.
220     * @param device device to connect to, or null to auto-connect last connected
221     *               headset
222     * @return       false if there was a problem initiating the connection
223     *               procedure, and no further HEADSET_STATE_CHANGED intents
224     *               will be expected.
225     */
226    public boolean connectHeadset(BluetoothDevice device) {
227        if (DBG) log("connectHeadset(" + device + ")");
228        if (mService != null) {
229            try {
230                if (mService.connectHeadset(device)) {
231                    return true;
232                }
233            } catch (RemoteException e) {Log.e(TAG, e.toString());}
234        } else {
235            Log.w(TAG, "Proxy not attached to service");
236            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
237        }
238        return false;
239    }
240
241    /**
242     * Returns true if the specified headset is connected (does not include
243     * connecting). Returns false if not connected, or if this proxy object
244     * if not currently connected to the headset service.
245     */
246    public boolean isConnected(BluetoothDevice device) {
247        if (DBG) log("isConnected(" + device + ")");
248        if (mService != null) {
249            try {
250                return mService.isConnected(device);
251            } catch (RemoteException e) {Log.e(TAG, e.toString());}
252        } else {
253            Log.w(TAG, "Proxy not attached to service");
254            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
255        }
256        return false;
257    }
258
259    /**
260     * Disconnects the current headset. Currently this call blocks, it may soon
261     * be made asynchornous. Returns false if this proxy object is
262     * not currently connected to the Headset service.
263     */
264    public boolean disconnectHeadset() {
265        if (DBG) log("disconnectHeadset()");
266        if (mService != null) {
267            try {
268                mService.disconnectHeadset();
269                return true;
270            } catch (RemoteException e) {Log.e(TAG, e.toString());}
271        } else {
272            Log.w(TAG, "Proxy not attached to service");
273            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
274        }
275        return false;
276    }
277
278    /**
279     * Start BT Voice Recognition mode, and set up Bluetooth audio path.
280     * Returns false if there is no headset connected, or if the
281     * connected headset does not support voice recognition, or on
282     * error.
283     */
284    public boolean startVoiceRecognition() {
285        if (DBG) log("startVoiceRecognition()");
286        if (mService != null) {
287            try {
288                return mService.startVoiceRecognition();
289            } catch (RemoteException e) {Log.e(TAG, e.toString());}
290        } else {
291            Log.w(TAG, "Proxy not attached to service");
292            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
293        }
294        return false;
295    }
296
297    /**
298     * Stop BT Voice Recognition mode, and shut down Bluetooth audio path.
299     * Returns false if there is no headset connected, or the connected
300     * headset is not in voice recognition mode, or on error.
301     */
302    public boolean stopVoiceRecognition() {
303        if (DBG) log("stopVoiceRecognition()");
304        if (mService != null) {
305            try {
306                return mService.stopVoiceRecognition();
307            } catch (RemoteException e) {Log.e(TAG, e.toString());}
308        } else {
309            Log.w(TAG, "Proxy not attached to service");
310            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
311        }
312        return false;
313    }
314
315    /**
316     * Set priority of headset.
317     * Priority is a non-negative integer. By default paired headsets will have
318     * a priority of PRIORITY_AUTO, and unpaired headset PRIORITY_NONE (0).
319     * Headsets with priority greater than zero will be auto-connected, and
320     * incoming connections will be accepted (if no other headset is
321     * connected).
322     * Auto-connection occurs at the following events: boot, incoming phone
323     * call, outgoing phone call.
324     * Headsets with priority equal to zero, or that are unpaired, are not
325     * auto-connected.
326     * Incoming connections are ignored regardless of priority if there is
327     * already a headset connected.
328     * @param device paired headset
329     * @param priority Integer priority, for example PRIORITY_AUTO or
330     *                 PRIORITY_NONE
331     * @return true if successful, false if there was some error
332     */
333    public boolean setPriority(BluetoothDevice device, int priority) {
334        if (DBG) log("setPriority(" + device + ", " + priority + ")");
335        if (mService != null) {
336            try {
337                return mService.setPriority(device, priority);
338            } catch (RemoteException e) {Log.e(TAG, e.toString());}
339        } else {
340            Log.w(TAG, "Proxy not attached to service");
341            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
342        }
343        return false;
344    }
345
346    /**
347     * Get priority of headset.
348     * @param device headset
349     * @return non-negative priority, or negative error code on error
350     */
351    public int getPriority(BluetoothDevice device) {
352        if (DBG) log("getPriority(" + device + ")");
353        if (mService != null) {
354            try {
355                return mService.getPriority(device);
356            } catch (RemoteException e) {Log.e(TAG, e.toString());}
357        } else {
358            Log.w(TAG, "Proxy not attached to service");
359            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
360        }
361        return -1;
362    }
363
364    /**
365     * Get battery usage hint for Bluetooth Headset service.
366     * This is a monotonically increasing integer. Wraps to 0 at
367     * Integer.MAX_INT, and at boot.
368     * Current implementation returns the number of AT commands handled since
369     * boot. This is a good indicator for spammy headset/handsfree units that
370     * can keep the device awake by polling for cellular status updates. As a
371     * rule of thumb, each AT command prevents the CPU from sleeping for 500 ms
372     * @return monotonically increasing battery usage hint, or a negative error
373     *         code on error
374     * @hide
375     */
376    public int getBatteryUsageHint() {
377        if (DBG) log("getBatteryUsageHint()");
378        if (mService != null) {
379            try {
380                return mService.getBatteryUsageHint();
381            } catch (RemoteException e) {Log.e(TAG, e.toString());}
382        } else {
383            Log.w(TAG, "Proxy not attached to service");
384            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
385        }
386        return -1;
387    }
388
389    private ServiceConnection mConnection = new ServiceConnection() {
390        public void onServiceConnected(ComponentName className, IBinder service) {
391            if (DBG) Log.d(TAG, "Proxy object connected");
392            mService = IBluetoothHeadset.Stub.asInterface(service);
393            if (mServiceListener != null) {
394                mServiceListener.onServiceConnected();
395            }
396        }
397        public void onServiceDisconnected(ComponentName className) {
398            if (DBG) Log.d(TAG, "Proxy object disconnected");
399            mService = null;
400            if (mServiceListener != null) {
401                mServiceListener.onServiceDisconnected();
402            }
403        }
404    };
405
406    private static void log(String msg) {
407        Log.d(TAG, msg);
408    }
409}
410