blob: 69381c53db857ad24ddaf6dc7bfdf7c3ca17108d [file] [log] [blame]
/*
* Copyright (C) 2018 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.bluetooth.btservice;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import com.android.bluetooth.a2dp.A2dpService;
import com.android.bluetooth.hearingaid.HearingAidService;
import com.android.bluetooth.hfp.HeadsetService;
import com.android.internal.annotations.VisibleForTesting;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
/**
* The active device manager is responsible for keeping track of the
* connected A2DP/HFP/AVRCP/HearingAid devices and select which device is
* active (for each profile).
*
* Current policy (subject to change):
* 1) If the maximum number of connected devices is one, the manager doesn't
* do anything. Each profile is responsible for automatically selecting
* the connected device as active. Only if the maximum number of connected
* devices is more than one, the rules below will apply.
* 2) The selected A2DP active device is the one used for AVRCP as well.
* 3) The HFP active device might be different from the A2DP active device.
* 4) The Active Device Manager always listens for ACTION_ACTIVE_DEVICE_CHANGED
* broadcasts for each profile:
* - BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED for A2DP
* - BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED for HFP
* - BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED for HearingAid
* If such broadcast is received (e.g., triggered indirectly by user
* action on the UI), the device in the received broacast is marked
* as the current active device for that profile.
* 5) If there is a HearingAid active device, then A2DP and HFP active devices
* must be set to null (i.e., A2DP and HFP cannot have active devices).
* The reason is because A2DP or HFP cannot be used together with HearingAid.
* 6) If there are no connected devices (e.g., during startup, or after all
* devices have been disconnected, the active device per profile
* (A2DP/HFP/HearingAid) is selected as follows:
* 6.1) The last connected HearingAid device is selected as active.
* If there is an active A2DP or HFP device, those must be set to null.
* 6.2) The last connected A2DP or HFP device is selected as active.
* However, if there is an active HearingAid device, then the
* A2DP or HFP active device is not set (must remain null).
* 7) If the currently active device (per profile) is disconnected, the
* Active Device Manager just marks that the profile has no active device,
* but does not attempt to select a new one. Currently, the expectation is
* that the user will explicitly select the new active device.
* 8) If there is already an active device, and the corresponding
* ACTION_ACTIVE_DEVICE_CHANGED broadcast is received, the device
* contained in the broadcast is marked as active. However, if
* the contained device is null, the corresponding profile is marked
* as having no active device.
* 9) If a wired audio device is connected, the audio output is switched
* by the Audio Framework itself to that device. We detect this here,
* and the active device for each profile (A2DP/HFP/HearingAid) is set
* to null to reflect the output device state change. However, if the
* wired audio device is disconnected, we don't do anything explicit
* and apply the default behavior instead:
* 9.1) If the wired headset is still the selected output device (i.e. the
* active device is set to null), the Phone itself will become the output
* device (i.e., the active device will remain null). If music was
* playing, it will stop.
* 9.2) If one of the Bluetooth devices is the selected active device
* (e.g., by the user in the UI), disconnecting the wired audio device
* will have no impact. E.g., music will continue streaming over the
* active Bluetooth device.
*/
class ActiveDeviceManager {
private static final boolean DBG = true;
private static final String TAG = "BluetoothActiveDeviceManager";
// Message types for the handler
private static final int MESSAGE_ADAPTER_ACTION_STATE_CHANGED = 1;
private static final int MESSAGE_A2DP_ACTION_CONNECTION_STATE_CHANGED = 2;
private static final int MESSAGE_A2DP_ACTION_ACTIVE_DEVICE_CHANGED = 3;
private static final int MESSAGE_HFP_ACTION_CONNECTION_STATE_CHANGED = 4;
private static final int MESSAGE_HFP_ACTION_ACTIVE_DEVICE_CHANGED = 5;
private static final int MESSAGE_HEARING_AID_ACTION_ACTIVE_DEVICE_CHANGED = 6;
private final AdapterService mAdapterService;
private final ServiceFactory mFactory;
private HandlerThread mHandlerThread = null;
private Handler mHandler = null;
private final AudioManager mAudioManager;
private final AudioManagerAudioDeviceCallback mAudioManagerAudioDeviceCallback;
private final List<BluetoothDevice> mA2dpConnectedDevices = new LinkedList<>();
private final List<BluetoothDevice> mHfpConnectedDevices = new LinkedList<>();
private BluetoothDevice mA2dpActiveDevice = null;
private BluetoothDevice mHfpActiveDevice = null;
private BluetoothDevice mHearingAidActiveDevice = null;
// Broadcast receiver for all changes
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action == null) {
Log.e(TAG, "Received intent with null action");
return;
}
switch (action) {
case BluetoothAdapter.ACTION_STATE_CHANGED:
mHandler.obtainMessage(MESSAGE_ADAPTER_ACTION_STATE_CHANGED,
intent).sendToTarget();
break;
case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED:
mHandler.obtainMessage(MESSAGE_A2DP_ACTION_CONNECTION_STATE_CHANGED,
intent).sendToTarget();
break;
case BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED:
mHandler.obtainMessage(MESSAGE_A2DP_ACTION_ACTIVE_DEVICE_CHANGED,
intent).sendToTarget();
break;
case BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED:
mHandler.obtainMessage(MESSAGE_HFP_ACTION_CONNECTION_STATE_CHANGED,
intent).sendToTarget();
break;
case BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED:
mHandler.obtainMessage(MESSAGE_HFP_ACTION_ACTIVE_DEVICE_CHANGED,
intent).sendToTarget();
break;
case BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED:
mHandler.obtainMessage(MESSAGE_HEARING_AID_ACTION_ACTIVE_DEVICE_CHANGED,
intent).sendToTarget();
break;
default:
Log.e(TAG, "Received unexpected intent, action=" + action);
break;
}
}
};
class ActiveDeviceManagerHandler extends Handler {
ActiveDeviceManagerHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_ADAPTER_ACTION_STATE_CHANGED: {
Intent intent = (Intent) msg.obj;
int newState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
if (DBG) {
Log.d(TAG, "handleMessage(MESSAGE_ADAPTER_ACTION_STATE_CHANGED): newState="
+ newState);
}
if (newState == BluetoothAdapter.STATE_ON) {
resetState();
}
}
break;
case MESSAGE_A2DP_ACTION_CONNECTION_STATE_CHANGED: {
Intent intent = (Intent) msg.obj;
BluetoothDevice device =
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
int prevState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
int nextState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
if (prevState == nextState) {
// Nothing has changed
break;
}
if (nextState == BluetoothProfile.STATE_CONNECTED) {
// Device connected
if (DBG) {
Log.d(TAG,
"handleMessage(MESSAGE_A2DP_ACTION_CONNECTION_STATE_CHANGED): "
+ "device " + device + " connected");
}
if (mA2dpConnectedDevices.contains(device)) {
break; // The device is already connected
}
mA2dpConnectedDevices.add(device);
if (mHearingAidActiveDevice == null) {
// New connected device: select it as active
setA2dpActiveDevice(device);
break;
}
break;
}
if (prevState == BluetoothProfile.STATE_CONNECTED) {
// Device disconnected
if (DBG) {
Log.d(TAG,
"handleMessage(MESSAGE_A2DP_ACTION_CONNECTION_STATE_CHANGED): "
+ "device " + device + " disconnected");
}
mA2dpConnectedDevices.remove(device);
if (Objects.equals(mA2dpActiveDevice, device)) {
setA2dpActiveDevice(null);
}
}
}
break;
case MESSAGE_A2DP_ACTION_ACTIVE_DEVICE_CHANGED: {
Intent intent = (Intent) msg.obj;
BluetoothDevice device =
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (DBG) {
Log.d(TAG, "handleMessage(MESSAGE_A2DP_ACTION_ACTIVE_DEVICE_CHANGED): "
+ "device= " + device);
}
if (device != null && !Objects.equals(mA2dpActiveDevice, device)) {
setHearingAidActiveDevice(null);
}
// Just assign locally the new value
mA2dpActiveDevice = device;
}
break;
case MESSAGE_HFP_ACTION_CONNECTION_STATE_CHANGED: {
Intent intent = (Intent) msg.obj;
BluetoothDevice device =
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
int prevState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
int nextState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
if (prevState == nextState) {
// Nothing has changed
break;
}
if (nextState == BluetoothProfile.STATE_CONNECTED) {
// Device connected
if (DBG) {
Log.d(TAG,
"handleMessage(MESSAGE_HFP_ACTION_CONNECTION_STATE_CHANGED): "
+ "device " + device + " connected");
}
if (mHfpConnectedDevices.contains(device)) {
break; // The device is already connected
}
mHfpConnectedDevices.add(device);
if (mHearingAidActiveDevice == null) {
// New connected device: select it as active
setHfpActiveDevice(device);
break;
}
break;
}
if (prevState == BluetoothProfile.STATE_CONNECTED) {
// Device disconnected
if (DBG) {
Log.d(TAG,
"handleMessage(MESSAGE_HFP_ACTION_CONNECTION_STATE_CHANGED): "
+ "device " + device + " disconnected");
}
mHfpConnectedDevices.remove(device);
if (Objects.equals(mHfpActiveDevice, device)) {
setHfpActiveDevice(null);
}
}
}
break;
case MESSAGE_HFP_ACTION_ACTIVE_DEVICE_CHANGED: {
Intent intent = (Intent) msg.obj;
BluetoothDevice device =
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (DBG) {
Log.d(TAG, "handleMessage(MESSAGE_HFP_ACTION_ACTIVE_DEVICE_CHANGED): "
+ "device= " + device);
}
if (device != null && !Objects.equals(mHfpActiveDevice, device)) {
setHearingAidActiveDevice(null);
}
// Just assign locally the new value
mHfpActiveDevice = device;
}
break;
case MESSAGE_HEARING_AID_ACTION_ACTIVE_DEVICE_CHANGED: {
Intent intent = (Intent) msg.obj;
BluetoothDevice device =
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (DBG) {
Log.d(TAG, "handleMessage(MESSAGE_HA_ACTION_ACTIVE_DEVICE_CHANGED): "
+ "device= " + device);
}
// Just assign locally the new value
mHearingAidActiveDevice = device;
if (device != null) {
setA2dpActiveDevice(null);
setHfpActiveDevice(null);
}
}
break;
}
}
}
/** Notifications of audio device connection and disconnection events. */
private class AudioManagerAudioDeviceCallback extends AudioDeviceCallback {
private boolean isWiredAudioHeadset(AudioDeviceInfo deviceInfo) {
switch (deviceInfo.getType()) {
case AudioDeviceInfo.TYPE_WIRED_HEADSET:
case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
case AudioDeviceInfo.TYPE_USB_HEADSET:
return true;
default:
break;
}
return false;
}
@Override
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
if (DBG) {
Log.d(TAG, "onAudioDevicesAdded");
}
boolean hasAddedWiredDevice = false;
for (AudioDeviceInfo deviceInfo : addedDevices) {
if (DBG) {
Log.d(TAG, "Audio device added: " + deviceInfo.getProductName() + " type: "
+ deviceInfo.getType());
}
if (isWiredAudioHeadset(deviceInfo)) {
hasAddedWiredDevice = true;
break;
}
}
if (hasAddedWiredDevice) {
wiredAudioDeviceConnected();
}
}
@Override
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
}
}
ActiveDeviceManager(AdapterService service, ServiceFactory factory) {
mAdapterService = service;
mFactory = factory;
mAudioManager = (AudioManager) service.getSystemService(Context.AUDIO_SERVICE);
mAudioManagerAudioDeviceCallback = new AudioManagerAudioDeviceCallback();
}
void start() {
if (DBG) {
Log.d(TAG, "start()");
}
mHandlerThread = new HandlerThread("BluetoothActiveDeviceManager");
mHandlerThread.start();
mHandler = new ActiveDeviceManagerHandler(mHandlerThread.getLooper());
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
filter.addAction(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED);
filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
filter.addAction(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED);
filter.addAction(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED);
mAdapterService.registerReceiver(mReceiver, filter);
mAudioManager.registerAudioDeviceCallback(mAudioManagerAudioDeviceCallback, mHandler);
}
void cleanup() {
if (DBG) {
Log.d(TAG, "cleanup()");
}
mAudioManager.unregisterAudioDeviceCallback(mAudioManagerAudioDeviceCallback);
mAdapterService.unregisterReceiver(mReceiver);
if (mHandlerThread != null) {
mHandlerThread.quit();
mHandlerThread = null;
}
resetState();
}
/**
* Get the {@link Looper} for the handler thread. This is used in testing and helper
* objects
*
* @return {@link Looper} for the handler thread
*/
@VisibleForTesting
public Looper getHandlerLooper() {
if (mHandlerThread == null) {
return null;
}
return mHandlerThread.getLooper();
}
private void setA2dpActiveDevice(BluetoothDevice device) {
if (DBG) {
Log.d(TAG, "setA2dpActiveDevice(" + device + ")");
}
final A2dpService a2dpService = mFactory.getA2dpService();
if (a2dpService == null) {
return;
}
if (!a2dpService.setActiveDevice(device)) {
return;
}
mA2dpActiveDevice = device;
}
private void setHfpActiveDevice(BluetoothDevice device) {
if (DBG) {
Log.d(TAG, "setHfpActiveDevice(" + device + ")");
}
final HeadsetService headsetService = mFactory.getHeadsetService();
if (headsetService == null) {
return;
}
if (!headsetService.setActiveDevice(device)) {
return;
}
mHfpActiveDevice = device;
}
private void setHearingAidActiveDevice(BluetoothDevice device) {
if (DBG) {
Log.d(TAG, "setHearingAidActiveDevice(" + device + ")");
}
final HearingAidService hearingAidService = mFactory.getHearingAidService();
if (hearingAidService == null) {
return;
}
if (!hearingAidService.setActiveDevice(device)) {
return;
}
mHearingAidActiveDevice = device;
}
private void resetState() {
mA2dpConnectedDevices.clear();
mA2dpActiveDevice = null;
mHfpConnectedDevices.clear();
mHfpActiveDevice = null;
mHearingAidActiveDevice = null;
}
@VisibleForTesting
BroadcastReceiver getBroadcastReceiver() {
return mReceiver;
}
@VisibleForTesting
BluetoothDevice getA2dpActiveDevice() {
return mA2dpActiveDevice;
}
@VisibleForTesting
BluetoothDevice getHfpActiveDevice() {
return mHfpActiveDevice;
}
@VisibleForTesting
BluetoothDevice getHearingAidActiveDevice() {
return mHearingAidActiveDevice;
}
/**
* Called when a wired audio device is connected.
* It might be called multiple times each time a wired audio device is connected.
*/
@VisibleForTesting
void wiredAudioDeviceConnected() {
if (DBG) {
Log.d(TAG, "wiredAudioDeviceConnected");
}
setA2dpActiveDevice(null);
setHfpActiveDevice(null);
setHearingAidActiveDevice(null);
}
}