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