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