/* * 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 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 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, * or 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 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; } }