blob: 6f26b959846f84d179833a37ba37731c29a3598b [file] [log] [blame]
Santos Cordon27a3c1f2013-08-06 07:49:27 -07001/*
2 * Copyright (C) 2013 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 com.android.phone;
18
19import com.google.android.collect.Lists;
20import com.google.common.base.Preconditions;
21
22import android.bluetooth.BluetoothAdapter;
23import android.bluetooth.BluetoothDevice;
24import android.bluetooth.BluetoothHeadset;
25import android.bluetooth.BluetoothProfile;
26import android.content.BroadcastReceiver;
27import android.content.Context;
28import android.content.Intent;
29import android.content.IntentFilter;
30import android.os.Handler;
31import android.os.Message;
32import android.os.SystemClock;
33import android.os.SystemProperties;
34import android.util.Log;
35
36import com.android.internal.telephony.CallManager;
37
38import java.util.List;
39
40/**
Santos Cordon593ab382013-08-06 21:58:23 -070041 * Listens to and caches bluetooth headset state. Used By the AudioRouter for maintaining
42 * overall audio state for use in the UI layer. Also provides method for connecting the bluetooth
43 * headset to the phone call.
Santos Cordon27a3c1f2013-08-06 07:49:27 -070044 */
45public class BluetoothManager {
Santos Cordon593ab382013-08-06 21:58:23 -070046 private static final String LOG_TAG = BluetoothManager.class.getSimpleName();
Santos Cordon27a3c1f2013-08-06 07:49:27 -070047 private static final boolean DBG =
48 (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
49 private static final boolean VDBG = (PhoneGlobals.DBG_LEVEL >= 2);
50
51 private final BluetoothAdapter mBluetoothAdapter;
52 private final CallManager mCallManager;
53 private final Context mContext;
54
55 private BluetoothHeadset mBluetoothHeadset;
56 private int mBluetoothHeadsetState = BluetoothProfile.STATE_DISCONNECTED;
57 private int mBluetoothHeadsetAudioState = BluetoothHeadset.STATE_AUDIO_DISCONNECTED;
58 private boolean mShowBluetoothIndication = false;
59 private boolean mBluetoothConnectionPending = false;
60 private long mBluetoothConnectionRequestTime;
61
62 // Broadcast receiver for various intent broadcasts (see onCreate())
63 private final BroadcastReceiver mReceiver = new BluetoothBroadcastReceiver();
64
65 private final List<BluetoothIndicatorListener> mListeners = Lists.newArrayList();
66
67 public BluetoothManager(Context context, CallManager callManager) {
68 mContext = context;
69 mCallManager = callManager;
70 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
71
72 init(mContext);
73 // TODO(klp): Listen for changes to the call list/state.
74 }
75
76 /* package */ boolean isBluetoothHeadsetAudioOn() {
77 return (mBluetoothHeadsetAudioState != BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
78 }
79
80 //
81 // Bluetooth helper methods.
82 //
83 // - BluetoothAdapter is the Bluetooth system service. If
84 // getDefaultAdapter() returns null
85 // then the device is not BT capable. Use BluetoothDevice.isEnabled()
86 // to see if BT is enabled on the device.
87 //
88 // - BluetoothHeadset is the API for the control connection to a
89 // Bluetooth Headset. This lets you completely connect/disconnect a
90 // headset (which we don't do from the Phone UI!) but also lets you
91 // get the address of the currently active headset and see whether
92 // it's currently connected.
93
94 /**
95 * @return true if the Bluetooth on/off switch in the UI should be
96 * available to the user (i.e. if the device is BT-capable
97 * and a headset is connected.)
98 */
99 /* package */ boolean isBluetoothAvailable() {
100 if (VDBG) log("isBluetoothAvailable()...");
101
102 // There's no need to ask the Bluetooth system service if BT is enabled:
103 //
104 // BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
105 // if ((adapter == null) || !adapter.isEnabled()) {
106 // if (DBG) log(" ==> FALSE (BT not enabled)");
107 // return false;
108 // }
109 // if (DBG) log(" - BT enabled! device name " + adapter.getName()
110 // + ", address " + adapter.getAddress());
111 //
112 // ...since we already have a BluetoothHeadset instance. We can just
113 // call isConnected() on that, and assume it'll be false if BT isn't
114 // enabled at all.
115
116 // Check if there's a connected headset, using the BluetoothHeadset API.
117 boolean isConnected = false;
118 if (mBluetoothHeadset != null) {
119 List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
120
121 if (deviceList.size() > 0) {
122 BluetoothDevice device = deviceList.get(0);
123 isConnected = true;
124
125 if (VDBG) log(" - headset state = " +
126 mBluetoothHeadset.getConnectionState(device));
127 if (VDBG) log(" - headset address: " + device);
128 if (VDBG) log(" - isConnected: " + isConnected);
129 }
130 }
131
132 if (VDBG) log(" ==> " + isConnected);
133 return isConnected;
134 }
135
136 /**
137 * @return true if a BT Headset is available, and its audio is currently connected.
138 */
139 /* package */ boolean isBluetoothAudioConnected() {
140 if (mBluetoothHeadset == null) {
141 if (VDBG) log("isBluetoothAudioConnected: ==> FALSE (null mBluetoothHeadset)");
142 return false;
143 }
144 List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
145
146 if (deviceList.isEmpty()) {
147 return false;
148 }
149 BluetoothDevice device = deviceList.get(0);
150 boolean isAudioOn = mBluetoothHeadset.isAudioConnected(device);
151 if (VDBG) log("isBluetoothAudioConnected: ==> isAudioOn = " + isAudioOn);
152 return isAudioOn;
153 }
154
155 /**
156 * Helper method used to control the onscreen "Bluetooth" indication;
157 * see InCallControlState.bluetoothIndicatorOn.
158 *
159 * @return true if a BT device is available and its audio is currently connected,
160 * <b>or</b> if we issued a BluetoothHeadset.connectAudio()
161 * call within the last 5 seconds (which presumably means
162 * that the BT audio connection is currently being set
163 * up, and will be connected soon.)
164 */
165 /* package */ boolean isBluetoothAudioConnectedOrPending() {
166 if (isBluetoothAudioConnected()) {
167 if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> TRUE (really connected)");
168 return true;
169 }
170
171 // If we issued a connectAudio() call "recently enough", even
172 // if BT isn't actually connected yet, let's still pretend BT is
173 // on. This makes the onscreen indication more responsive.
174 if (mBluetoothConnectionPending) {
175 long timeSinceRequest =
176 SystemClock.elapsedRealtime() - mBluetoothConnectionRequestTime;
177 if (timeSinceRequest < 5000 /* 5 seconds */) {
178 if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> TRUE (requested "
179 + timeSinceRequest + " msec ago)");
180 return true;
181 } else {
182 if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> FALSE (request too old: "
183 + timeSinceRequest + " msec ago)");
184 mBluetoothConnectionPending = false;
185 return false;
186 }
187 }
188
189 if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> FALSE");
190 return false;
191 }
192
193 /**
194 * @return true if the onscreen UI should currently be showing the
195 * special "bluetooth is active" indication in a couple of places (in
196 * which UI elements turn blue and/or show the bluetooth logo.)
197 *
198 * This depends on the BluetoothHeadset state *and* the current
199 * telephony state; see shouldShowBluetoothIndication().
200 *
201 * @see CallCard
202 * @see NotificationMgr.updateInCallNotification
203 */
204 /* package */ boolean showBluetoothIndication() {
205 return mShowBluetoothIndication;
206 }
207
208 /**
209 * Recomputes the mShowBluetoothIndication flag based on the current
210 * bluetooth state and current telephony state.
211 *
212 * This needs to be called any time the bluetooth headset state or the
213 * telephony state changes.
214 */
215 /* package */ void updateBluetoothIndication() {
216 mShowBluetoothIndication = shouldShowBluetoothIndication(mBluetoothHeadsetState,
217 mBluetoothHeadsetAudioState,
218 mCallManager);
219
220 notifyListeners(mShowBluetoothIndication);
221 }
222
Santos Cordon9b7bac72013-08-06 08:04:52 -0700223 public void addBluetoothIndicatorListener(BluetoothIndicatorListener listener) {
Santos Cordon27a3c1f2013-08-06 07:49:27 -0700224 if (!mListeners.contains(listener)) {
225 mListeners.add(listener);
226 }
227 }
228
Santos Cordon9b7bac72013-08-06 08:04:52 -0700229 public void removeBluetoothIndicatorListener(BluetoothIndicatorListener listener) {
230 if (mListeners.contains(listener)) {
231 mListeners.remove(listener);
232 }
233 }
234
Santos Cordon27a3c1f2013-08-06 07:49:27 -0700235 private void notifyListeners(boolean showBluetoothOn) {
236 for (int i = 0; i < mListeners.size(); i++) {
237 mListeners.get(i).onBluetoothIndicationChange(showBluetoothOn, this);
238 }
239 }
240
241 private void init(Context context) {
242 Preconditions.checkNotNull(context);
243
244 if (mBluetoothAdapter != null) {
245 mBluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
246 BluetoothProfile.HEADSET);
247 }
248
249 // Register for misc other intent broadcasts.
250 IntentFilter intentFilter =
251 new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
252 intentFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
253 context.registerReceiver(mReceiver, intentFilter);
254 }
255
256 private void tearDown() {
257 if (mBluetoothHeadset != null) {
258 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mBluetoothHeadset);
259 mBluetoothHeadset = null;
260 }
261 }
262
263 private BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
264 new BluetoothProfile.ServiceListener() {
265 @Override
266 public void onServiceConnected(int profile, BluetoothProfile proxy) {
267 mBluetoothHeadset = (BluetoothHeadset) proxy;
268 if (VDBG) log("- Got BluetoothHeadset: " + mBluetoothHeadset);
269 }
270
271 @Override
272 public void onServiceDisconnected(int profile) {
273 mBluetoothHeadset = null;
274 }
275 };
276
277 /**
278 * UI policy helper function for the couple of places in the UI that
279 * have some way of indicating that "bluetooth is in use."
280 *
281 * @return true if the onscreen UI should indicate that "bluetooth is in use",
282 * based on the specified bluetooth headset state, and the
283 * current state of the phone.
284 * @see showBluetoothIndication()
285 */
286 private static boolean shouldShowBluetoothIndication(int bluetoothState,
287 int bluetoothAudioState,
288 CallManager cm) {
289 // We want the UI to indicate that "bluetooth is in use" in two
290 // slightly different cases:
291 //
292 // (a) The obvious case: if a bluetooth headset is currently in
293 // use for an ongoing call.
294 //
295 // (b) The not-so-obvious case: if an incoming call is ringing,
296 // and we expect that audio *will* be routed to a bluetooth
297 // headset once the call is answered.
298
299 switch (cm.getState()) {
300 case OFFHOOK:
301 // This covers normal active calls, and also the case if
302 // the foreground call is DIALING or ALERTING. In this
303 // case, bluetooth is considered "active" if a headset
304 // is connected *and* audio is being routed to it.
305 return ((bluetoothState == BluetoothHeadset.STATE_CONNECTED)
306 && (bluetoothAudioState == BluetoothHeadset.STATE_AUDIO_CONNECTED));
307
308 case RINGING:
309 // If an incoming call is ringing, we're *not* yet routing
310 // audio to the headset (since there's no in-call audio
311 // yet!) In this case, if a bluetooth headset is
312 // connected at all, we assume that it'll become active
313 // once the user answers the phone.
314 return (bluetoothState == BluetoothHeadset.STATE_CONNECTED);
315
316 default: // Presumably IDLE
317 return false;
318 }
319 }
320
321 private void dumpBluetoothState() {
322 log("============== dumpBluetoothState() =============");
323 log("= isBluetoothAvailable: " + isBluetoothAvailable());
324 log("= isBluetoothAudioConnected: " + isBluetoothAudioConnected());
325 log("= isBluetoothAudioConnectedOrPending: " + isBluetoothAudioConnectedOrPending());
326 log("= PhoneApp.showBluetoothIndication: "
327 + showBluetoothIndication());
328 log("=");
329 if (mBluetoothAdapter != null) {
330 if (mBluetoothHeadset != null) {
331 List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
332
333 if (deviceList.size() > 0) {
334 BluetoothDevice device = deviceList.get(0);
335 log("= BluetoothHeadset.getCurrentDevice: " + device);
336 log("= BluetoothHeadset.State: "
337 + mBluetoothHeadset.getConnectionState(device));
338 log("= BluetoothHeadset audio connected: " +
339 mBluetoothHeadset.isAudioConnected(device));
340 }
341 } else {
342 log("= mBluetoothHeadset is null");
343 }
344 } else {
345 log("= mBluetoothAdapter is null; device is not BT capable");
346 }
347 }
348
349 /* package */ void connectBluetoothAudio() {
350 if (VDBG) log("connectBluetoothAudio()...");
351 if (mBluetoothHeadset != null) {
352 // TODO(BT) check return
353 mBluetoothHeadset.connectAudio();
354 }
355
356 // Watch out: The bluetooth connection doesn't happen instantly;
357 // the connectAudio() call returns instantly but does its real
358 // work in another thread. The mBluetoothConnectionPending flag
359 // is just a little trickery to ensure that the onscreen UI updates
360 // instantly. (See isBluetoothAudioConnectedOrPending() above.)
361 mBluetoothConnectionPending = true;
362 mBluetoothConnectionRequestTime = SystemClock.elapsedRealtime();
363 }
364
365 /* package */ void disconnectBluetoothAudio() {
366 if (VDBG) log("disconnectBluetoothAudio()...");
367 if (mBluetoothHeadset != null) {
368 mBluetoothHeadset.disconnectAudio();
369 }
370 mBluetoothConnectionPending = false;
371 }
372
373 /**
374 * Receiver for misc intent broadcasts the BluetoothManager cares about.
375 */
376 private class BluetoothBroadcastReceiver extends BroadcastReceiver {
377 @Override
378 public void onReceive(Context context, Intent intent) {
379 String action = intent.getAction();
380
381 if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
382 mBluetoothHeadsetState = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
383 BluetoothHeadset.STATE_DISCONNECTED);
384 if (VDBG) Log.d(LOG_TAG, "mReceiver: HEADSET_STATE_CHANGED_ACTION");
385 if (VDBG) Log.d(LOG_TAG, "==> new state: " + mBluetoothHeadsetState);
386 // Also update any visible UI if necessary
387 updateBluetoothIndication();
388 } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
389 mBluetoothHeadsetAudioState =
390 intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
391 BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
392 if (VDBG) Log.d(LOG_TAG, "mReceiver: HEADSET_AUDIO_STATE_CHANGED_ACTION");
393 if (VDBG) Log.d(LOG_TAG, "==> new state: " + mBluetoothHeadsetAudioState);
394 updateBluetoothIndication();
395 }
396 }
397 }
398
399 private void log(String msg) {
400 Log.d(LOG_TAG, msg);
401 }
402
403 /* package */ interface BluetoothIndicatorListener {
Santos Cordon9b7bac72013-08-06 08:04:52 -0700404 public void onBluetoothIndicationChange(boolean isConnected, BluetoothManager manager);
Santos Cordon27a3c1f2013-08-06 07:49:27 -0700405 }
406}