/* * 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.annotation.RequiresPermission; import android.annotation.SuppressLint; 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 mA2dpConnectedDevices = new LinkedList<>(); private final List 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. */ @SuppressLint("AndroidFrameworkRequiresPermission") 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; } @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) 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 @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) void wiredAudioDeviceConnected() { if (DBG) { Log.d(TAG, "wiredAudioDeviceConnected"); } setA2dpActiveDevice(null); setHfpActiveDevice(null); setHearingAidActiveDevice(null); } }