blob: 84f6bdc45cf671c0e26bc67b09de285cb076589b [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.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 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 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
* 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 are no connected devices (e.g., during startup, or after all
* devices have been disconnected, the active device per profile
* (either A2DP or HFP) is selected as follows:
* 5.1) The first connected device (for either A2DP or HFP) is immediately
* selected as active for that profile. Assume the first connected device
* is for A2DP.
* 5.2) A timer is started: if the same device is connected for the other
* profile as well (HFP in this example) while the timer is running,
* and there is no active HFP device yet, that device is selected as
* active for HFP as well. The purpose is to select by default the same
* device as active for both profiles.
* 5.3) While the timer is running, all other HFP connected devices are
* listed locally, but none of those devices is selected as active.
* 5.4) While the timer is running, if ACTION_ACTIVE_DEVICE_CHANGED broadcast
* is received for HFP, the device contained in the broadcast is
* marked as active.
* 5.5) If the timer expires and no HFP device has been selected as active,
* the first HFP connected device is selected as active.
* 6) 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.
* 7) 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.
*/
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_SELECT_ACTICE_DEVICE_TIMEOUT = 2;
private static final int MESSAGE_A2DP_ACTION_CONNECTION_STATE_CHANGED = 3;
private static final int MESSAGE_A2DP_ACTION_ACTIVE_DEVICE_CHANGED = 4;
private static final int MESSAGE_HFP_ACTION_CONNECTION_STATE_CHANGED = 5;
private static final int MESSAGE_HFP_ACTION_ACTIVE_DEVICE_CHANGED = 6;
private static final int MESSAGE_HEARING_AID_ACTION_ACTIVE_DEVICE_CHANGED = 7;
// Timeouts
private static final int SELECT_ACTIVE_DEVICE_TIMEOUT_MS = 6000; // 6s
private final AdapterService mAdapterService;
private final ServiceFactory mFactory;
private HandlerThread mHandlerThread = null;
private Handler mHandler = null;
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 ActivePoliceManagerHandler extends Handler {
ActivePoliceManagerHandler(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_SELECT_ACTICE_DEVICE_TIMEOUT: {
if (DBG) {
Log.d(TAG, "handleMessage(MESSAGE_SELECT_ACTICE_DEVICE_TIMEOUT)");
}
// Set the first connected device as active
if ((mA2dpActiveDevice == null) && !mA2dpConnectedDevices.isEmpty()
&& mHearingAidActiveDevice == null) {
setA2dpActiveDevice(mA2dpConnectedDevices.get(0));
}
if ((mHfpActiveDevice == null) && !mHfpConnectedDevices.isEmpty()
&& mHearingAidActiveDevice == null) {
setHfpActiveDevice(mHfpConnectedDevices.get(0));
}
}
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 (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;
}
if (!hasConnectedClassicDevices() && mHearingAidActiveDevice == null) {
// First connected device: select it as active and start the timer
mA2dpConnectedDevices.add(device);
Message m = obtainMessage(MESSAGE_SELECT_ACTICE_DEVICE_TIMEOUT);
sendMessageDelayed(m, SELECT_ACTIVE_DEVICE_TIMEOUT_MS);
setA2dpActiveDevice(device);
break;
}
mA2dpConnectedDevices.add(device);
// Check whether the active device for the other profile is same
if ((mA2dpActiveDevice == null) && matchesActiveDevice(device)
&& mHearingAidActiveDevice == null) {
setA2dpActiveDevice(device);
break;
}
// Check whether the active device selection timer is not running
if ((mA2dpActiveDevice == null)
&& !hasMessages(MESSAGE_SELECT_ACTICE_DEVICE_TIMEOUT)
&& mHearingAidActiveDevice == null) {
setA2dpActiveDevice(mA2dpConnectedDevices.get(0));
break;
}
break;
}
if ((prevState == BluetoothProfile.STATE_CONNECTED)
&& (nextState != prevState)) {
// 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);
}
removeMessages(MESSAGE_SELECT_ACTICE_DEVICE_TIMEOUT);
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);
// TODO: Copy the corresponding logic from the processing of
// message MESSAGE_A2DP_ACTION_CONNECTION_STATE_CHANGED
}
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);
}
removeMessages(MESSAGE_SELECT_ACTICE_DEVICE_TIMEOUT);
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);
}
removeMessages(MESSAGE_SELECT_ACTICE_DEVICE_TIMEOUT);
// Just assign locally the new value
mHearingAidActiveDevice = device;
if (device != null) {
setA2dpActiveDevice(null);
setHfpActiveDevice(null);
}
}
break;
}
}
}
ActiveDeviceManager(AdapterService service, ServiceFactory factory) {
mAdapterService = service;
mFactory = factory;
}
void start() {
if (DBG) {
Log.d(TAG, "start()");
}
mHandlerThread = new HandlerThread("BluetoothActiveDeviceManager");
mHandlerThread.start();
mHandler = new ActivePoliceManagerHandler(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);
}
void cleanup() {
if (DBG) {
Log.d(TAG, "cleanup()");
}
mAdapterService.unregisterReceiver(mReceiver);
if (mHandlerThread != null) {
mHandlerThread.quit();
mHandlerThread = null;
}
resetState();
}
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 boolean hasConnectedClassicDevices() {
return (!mA2dpConnectedDevices.isEmpty() || !mHfpConnectedDevices.isEmpty());
}
private boolean matchesActiveDevice(BluetoothDevice device) {
return (Objects.equals(mA2dpActiveDevice, device)
|| Objects.equals(mHfpActiveDevice, device));
}
private void resetState() {
if (mHandler != null) {
mHandler.removeMessages(MESSAGE_SELECT_ACTICE_DEVICE_TIMEOUT);
}
mA2dpConnectedDevices.clear();
mA2dpActiveDevice = null;
mHfpConnectedDevices.clear();
mHfpActiveDevice = null;
}
}