blob: d31c69dc32e71638597a00736249e01b1bf2e97e [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.telecom;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import java.util.List;
/**
* Listens to and caches bluetooth headset state. Used By the CallAudioManager for maintaining
* overall audio state. Also provides method for connecting the bluetooth headset to the phone call.
*/
public class BluetoothManager {
public static final int BLUETOOTH_UNINITIALIZED = 0;
public static final int BLUETOOTH_DISCONNECTED = 1;
public static final int BLUETOOTH_DEVICE_CONNECTED = 2;
public static final int BLUETOOTH_AUDIO_PENDING = 3;
public static final int BLUETOOTH_AUDIO_CONNECTED = 4;
public interface BluetoothStateListener {
void onBluetoothStateChange(int oldState, int newState);
}
private final BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
new BluetoothProfile.ServiceListener() {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
Log.startSession("BMSL.oSC");
try {
if (profile == BluetoothProfile.HEADSET) {
mBluetoothHeadset = new BluetoothHeadsetProxy((BluetoothHeadset) proxy);
Log.v(this, "- Got BluetoothHeadset: " + mBluetoothHeadset);
} else {
Log.w(this, "Connected to non-headset bluetooth service. Not changing" +
" bluetooth headset.");
}
updateListenerOfBluetoothState(true);
} finally {
Log.endSession();
}
}
@Override
public void onServiceDisconnected(int profile) {
Log.startSession("BMSL.oSD");
try {
mBluetoothHeadset = null;
Log.v(this, "Lost BluetoothHeadset: " + mBluetoothHeadset);
updateListenerOfBluetoothState(false);
} finally {
Log.endSession();
}
}
};
/**
* Receiver for misc intent broadcasts the BluetoothManager cares about.
*/
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.startSession("BM.oR");
try {
String action = intent.getAction();
if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
int bluetoothHeadsetState = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
BluetoothHeadset.STATE_DISCONNECTED);
Log.i(this, "mReceiver: HEADSET_STATE_CHANGED_ACTION");
Log.i(this, "==> new state: %s ", bluetoothHeadsetState);
updateListenerOfBluetoothState(
bluetoothHeadsetState == BluetoothHeadset.STATE_CONNECTING);
} else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
int bluetoothHeadsetAudioState =
intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
Log.i(this, "mReceiver: HEADSET_AUDIO_STATE_CHANGED_ACTION");
Log.i(this, "==> new state: %s", bluetoothHeadsetAudioState);
updateListenerOfBluetoothState(
bluetoothHeadsetAudioState ==
BluetoothHeadset.STATE_AUDIO_CONNECTING
|| bluetoothHeadsetAudioState ==
BluetoothHeadset.STATE_AUDIO_CONNECTED);
}
} finally {
Log.endSession();
}
}
};
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final BluetoothAdapterProxy mBluetoothAdapter;
private BluetoothStateListener mBluetoothStateListener;
private BluetoothHeadsetProxy mBluetoothHeadset;
private long mBluetoothConnectionRequestTime;
private final Runnable mBluetoothConnectionTimeout = new Runnable("BM.cBA", null /*lock*/) {
@Override
public void loggedRun() {
if (!isBluetoothAudioConnected()) {
Log.v(this, "Bluetooth audio inexplicably disconnected within 5 seconds of " +
"connection. Updating UI.");
}
updateListenerOfBluetoothState(false);
}
};
private final Runnable mRetryConnectAudio = new Runnable("BM.rCA", null /*lock*/) {
@Override
public void loggedRun() {
Log.i(this, "Retrying connecting to bluetooth audio.");
if (!mBluetoothHeadset.connectAudio()) {
Log.w(this, "Retry of bluetooth audio connection failed. Giving up.");
} else {
setBluetoothStatePending();
}
}
};
private final Context mContext;
private int mBluetoothState = BLUETOOTH_UNINITIALIZED;
public BluetoothManager(Context context, BluetoothAdapterProxy bluetoothAdapterProxy) {
mBluetoothAdapter = bluetoothAdapterProxy;
mContext = context;
if (mBluetoothAdapter != null) {
mBluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
BluetoothProfile.HEADSET);
}
// Register for misc other intent broadcasts.
IntentFilter intentFilter =
new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
intentFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
context.registerReceiver(mReceiver, intentFilter);
}
public void setBluetoothStateListener(BluetoothStateListener bluetoothStateListener) {
mBluetoothStateListener = bluetoothStateListener;
}
//
// Bluetooth helper methods.
//
// - BluetoothAdapter is the Bluetooth system service. If
// getDefaultAdapter() returns null
// then the device is not BT capable. Use BluetoothDevice.isEnabled()
// to see if BT is enabled on the device.
//
// - BluetoothHeadset is the API for the control connection to a
// Bluetooth Headset. This lets you completely connect/disconnect a
// headset (which we don't do from the Phone UI!) but also lets you
// get the address of the currently active headset and see whether
// it's currently connected.
/**
* @return true if the Bluetooth on/off switch in the UI should be
* available to the user (i.e. if the device is BT-capable
* and a headset is connected.)
*/
@VisibleForTesting
public boolean isBluetoothAvailable() {
Log.v(this, "isBluetoothAvailable()...");
// There's no need to ask the Bluetooth system service if BT is enabled:
//
// BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
// if ((adapter == null) || !adapter.isEnabled()) {
// Log.d(this, " ==> FALSE (BT not enabled)");
// return false;
// }
// Log.d(this, " - BT enabled! device name " + adapter.getName()
// + ", address " + adapter.getAddress());
//
// ...since we already have a BluetoothHeadset instance. We can just
// call isConnected() on that, and assume it'll be false if BT isn't
// enabled at all.
// Check if there's a connected headset, using the BluetoothHeadset API.
boolean isConnected = false;
if (mBluetoothHeadset != null) {
List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
if (deviceList.size() > 0) {
isConnected = true;
for (int i = 0; i < deviceList.size(); i++) {
BluetoothDevice device = deviceList.get(i);
Log.v(this, "state = " + mBluetoothHeadset.getConnectionState(device)
+ "for headset: " + device);
}
}
}
Log.v(this, " ==> " + isConnected);
return isConnected;
}
/**
* @return true if a BT Headset is available, and its audio is currently connected.
*/
@VisibleForTesting
public boolean isBluetoothAudioConnected() {
if (mBluetoothHeadset == null) {
Log.v(this, "isBluetoothAudioConnected: ==> FALSE (null mBluetoothHeadset)");
return false;
}
List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
if (deviceList.isEmpty()) {
return false;
}
for (int i = 0; i < deviceList.size(); i++) {
BluetoothDevice device = deviceList.get(i);
boolean isAudioOn = mBluetoothHeadset.isAudioConnected(device);
Log.v(this, "isBluetoothAudioConnected: ==> isAudioOn = " + isAudioOn
+ "for headset: " + device);
if (isAudioOn) {
return true;
}
}
return false;
}
/**
* Helper method used to control the onscreen "Bluetooth" indication;
*
* @return true if a BT device is available and its audio is currently connected,
* <b>or</b> if we issued a BluetoothHeadset.connectAudio()
* call within the last 5 seconds (which presumably means
* that the BT audio connection is currently being set
* up, and will be connected soon.)
*/
@VisibleForTesting
public boolean isBluetoothAudioConnectedOrPending() {
if (isBluetoothAudioConnected()) {
Log.v(this, "isBluetoothAudioConnectedOrPending: ==> TRUE (really connected)");
return true;
}
// If we issued a connectAudio() call "recently enough", even
// if BT isn't actually connected yet, let's still pretend BT is
// on. This makes the onscreen indication more responsive.
if (isBluetoothAudioPending()) {
long timeSinceRequest =
SystemClock.elapsedRealtime() - mBluetoothConnectionRequestTime;
Log.v(this, "isBluetoothAudioConnectedOrPending: ==> TRUE (requested "
+ timeSinceRequest + " msec ago)");
return true;
}
Log.v(this, "isBluetoothAudioConnectedOrPending: ==> FALSE");
return false;
}
private boolean isBluetoothAudioPending() {
return mBluetoothState == BLUETOOTH_AUDIO_PENDING;
}
/**
* Notified audio manager of a change to the bluetooth state.
*/
private void updateListenerOfBluetoothState(boolean canBePending) {
int newState;
if (isBluetoothAudioConnected()) {
newState = BLUETOOTH_AUDIO_CONNECTED;
} else if (canBePending && isBluetoothAudioPending()) {
newState = BLUETOOTH_AUDIO_PENDING;
} else if (isBluetoothAvailable()) {
newState = BLUETOOTH_DEVICE_CONNECTED;
} else {
newState = BLUETOOTH_DISCONNECTED;
}
if (mBluetoothState != newState) {
mBluetoothStateListener.onBluetoothStateChange(mBluetoothState, newState);
mBluetoothState = newState;
}
}
@VisibleForTesting
public void connectBluetoothAudio() {
Log.v(this, "connectBluetoothAudio()...");
if (mBluetoothHeadset != null) {
if (!mBluetoothHeadset.connectAudio()) {
mHandler.postDelayed(mRetryConnectAudio.prepare(),
Timeouts.getRetryBluetoothConnectAudioBackoffMillis(
mContext.getContentResolver()));
}
}
// The call to connectAudio is asynchronous and may take some time to complete. However,
// if connectAudio() returns false, we know that it has failed and therefore will
// schedule a retry to happen some time later. We set bluetooth state to pending now and
// show bluetooth as connected in the UI, but confirmation that we are connected will
// arrive through mReceiver.
setBluetoothStatePending();
}
private void setBluetoothStatePending() {
mBluetoothState = BLUETOOTH_AUDIO_PENDING;
mBluetoothConnectionRequestTime = SystemClock.elapsedRealtime();
mHandler.removeCallbacks(mBluetoothConnectionTimeout.getRunnableToCancel());
mBluetoothConnectionTimeout.cancel();
// If the mBluetoothConnectionTimeout runnable has run, the session had been cleared...
// Create a new Session before putting it back in the queue to possibly run again.
mHandler.postDelayed(mBluetoothConnectionTimeout.prepare(),
Timeouts.getBluetoothPendingTimeoutMillis(mContext.getContentResolver()));
}
@VisibleForTesting
public void disconnectBluetoothAudio() {
Log.v(this, "disconnectBluetoothAudio()...");
if (mBluetoothHeadset != null) {
mBluetoothState = BLUETOOTH_DEVICE_CONNECTED;
mBluetoothHeadset.disconnectAudio();
} else {
mBluetoothState = BLUETOOTH_DISCONNECTED;
}
mHandler.removeCallbacks(mBluetoothConnectionTimeout.getRunnableToCancel());
mBluetoothConnectionTimeout.cancel();
}
/**
* Dumps the state of the {@link BluetoothManager}.
*
* @param pw The {@code IndentingPrintWriter} to write the state to.
*/
public void dump(IndentingPrintWriter pw) {
pw.println("isBluetoothAvailable: " + isBluetoothAvailable());
pw.println("isBluetoothAudioConnected: " + isBluetoothAudioConnected());
pw.println("isBluetoothAudioConnectedOrPending: " + isBluetoothAudioConnectedOrPending());
if (mBluetoothAdapter != null) {
if (mBluetoothHeadset != null) {
List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
if (deviceList.size() > 0) {
BluetoothDevice device = deviceList.get(0);
pw.println("BluetoothHeadset.getCurrentDevice: " + device);
pw.println("BluetoothHeadset.State: "
+ mBluetoothHeadset.getConnectionState(device));
pw.println("BluetoothHeadset audio connected: " +
mBluetoothHeadset.isAudioConnected(device));
}
} else {
pw.println("mBluetoothHeadset is null");
}
} else {
pw.println("mBluetoothAdapter is null; device is not BT capable");
}
}
/**
* Set the bluetooth headset proxy for testing purposes.
* @param bluetoothHeadsetProxy
*/
@VisibleForTesting
public void setBluetoothHeadsetForTesting(BluetoothHeadsetProxy bluetoothHeadsetProxy) {
mBluetoothHeadset = bluetoothHeadsetProxy;
}
/**
* Set mBluetoothState for testing.
* @param state
*/
@VisibleForTesting
public void setInternalBluetoothState(int state) {
mBluetoothState = state;
}
}