| /* |
| * 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.support.annotation.VisibleForTesting; |
| 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/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. |
| */ |
| 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 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; |
| } |
| } |
| } |
| |
| 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 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); |
| } |
| |
| 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 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; |
| } |
| } |