blob: 47bd853361ee278d90679fe523f8c02d8638a570 [file] [log] [blame]
/*
* Copyright (C) 2008 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.settingslib.bluetooth;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Objects;
/**
* CachedBluetoothDeviceManager manages the set of remote Bluetooth devices.
*/
public class CachedBluetoothDeviceManager {
private static final String TAG = "CachedBluetoothDeviceManager";
private static final boolean DEBUG = BluetoothUtils.D;
private Context mContext;
private final LocalBluetoothManager mBtManager;
@VisibleForTesting
final List<CachedBluetoothDevice> mCachedDevices =
new ArrayList<CachedBluetoothDevice>();
// Contains the list of hearing aid devices that should not be shown in the UI.
@VisibleForTesting
final List<CachedBluetoothDevice> mHearingAidDevicesNotAddedInCache
= new ArrayList<CachedBluetoothDevice>();
// Maintains a list of devices which are added in mCachedDevices and have hiSyncIds.
@VisibleForTesting
final Map<Long, CachedBluetoothDevice> mCachedDevicesMapForHearingAids
= new HashMap<Long, CachedBluetoothDevice>();
CachedBluetoothDeviceManager(Context context, LocalBluetoothManager localBtManager) {
mContext = context;
mBtManager = localBtManager;
}
public synchronized Collection<CachedBluetoothDevice> getCachedDevicesCopy() {
return new ArrayList<CachedBluetoothDevice>(mCachedDevices);
}
public static boolean onDeviceDisappeared(CachedBluetoothDevice cachedDevice) {
cachedDevice.setJustDiscovered(false);
return cachedDevice.getBondState() == BluetoothDevice.BOND_NONE;
}
public void onDeviceNameUpdated(BluetoothDevice device) {
CachedBluetoothDevice cachedDevice = findDevice(device);
if (cachedDevice != null) {
cachedDevice.refreshName();
}
}
/**
* Search for existing {@link CachedBluetoothDevice} or return null
* if this device isn't in the cache. Use {@link #addDevice}
* to create and return a new {@link CachedBluetoothDevice} for
* a newly discovered {@link BluetoothDevice}.
*
* @param device the address of the Bluetooth device
* @return the cached device object for this device, or null if it has
* not been previously seen
*/
public synchronized CachedBluetoothDevice findDevice(BluetoothDevice device) {
for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
if (cachedDevice.getDevice().equals(device)) {
return cachedDevice;
}
}
for (CachedBluetoothDevice notCachedDevice : mHearingAidDevicesNotAddedInCache) {
if (notCachedDevice.getDevice().equals(device)) {
return notCachedDevice;
}
}
return null;
}
/**
* Create and return a new {@link CachedBluetoothDevice}. This assumes
* that {@link #findDevice} has already been called and returned null.
* @param device the address of the new Bluetooth device
* @return the newly created CachedBluetoothDevice object
*/
public CachedBluetoothDevice addDevice(BluetoothDevice device) {
LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
CachedBluetoothDevice newDevice = new CachedBluetoothDevice(mContext, profileManager,
device);
if (profileManager.getHearingAidProfile() != null
&& profileManager.getHearingAidProfile().getHiSyncId(newDevice.getDevice())
!= BluetoothHearingAid.HI_SYNC_ID_INVALID) {
newDevice.setHiSyncId(profileManager.getHearingAidProfile()
.getHiSyncId(newDevice.getDevice()));
}
// Just add one of the hearing aids from a pair in the list that is shown in the UI.
if (isPairAddedInCache(newDevice.getHiSyncId())) {
synchronized (this) {
mHearingAidDevicesNotAddedInCache.add(newDevice);
}
} else {
synchronized (this) {
mCachedDevices.add(newDevice);
if (newDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID
&& !mCachedDevicesMapForHearingAids.containsKey(newDevice.getHiSyncId())) {
mCachedDevicesMapForHearingAids.put(newDevice.getHiSyncId(), newDevice);
}
mBtManager.getEventManager().dispatchDeviceAdded(newDevice);
}
}
return newDevice;
}
/**
* Returns true if the one of the two hearing aid devices is already cached for UI.
*
* @param long hiSyncId
* @return {@code True} if one of the two hearing aid devices is is already cached for UI.
*/
private synchronized boolean isPairAddedInCache(long hiSyncId) {
if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID) {
return false;
}
if(mCachedDevicesMapForHearingAids.containsKey(hiSyncId)) {
return true;
}
return false;
}
/**
* Returns device summary of the pair of the hearing aid passed as the parameter.
*
* @param CachedBluetoothDevice device
* @return Device summary, or if the pair does not exist or if its not a hearing aid,
* then {@code null}.
*/
public synchronized String getHearingAidPairDeviceSummary(CachedBluetoothDevice device) {
String pairDeviceSummary = null;
if (device.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
for (CachedBluetoothDevice hearingAidDevice : mHearingAidDevicesNotAddedInCache) {
if (hearingAidDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID
&& hearingAidDevice.getHiSyncId() == device.getHiSyncId()) {
pairDeviceSummary = hearingAidDevice.getConnectionSummary();
}
}
}
return pairDeviceSummary;
}
/**
* Adds the 2nd hearing aid in a pair in a list that maintains the hearing aids that are
* not dispalyed in the UI.
*
* @param CachedBluetoothDevice device
*/
public synchronized void addDeviceNotaddedInMap(CachedBluetoothDevice device) {
mHearingAidDevicesNotAddedInCache.add(device);
}
/**
* Updates the Hearing Aid devices; specifically the HiSyncId's. This routine is called when the
* Hearing Aid Service is connected and the HiSyncId's are now available.
* @param LocalBluetoothProfileManager profileManager
*/
public synchronized void updateHearingAidsDevices(LocalBluetoothProfileManager profileManager) {
HearingAidProfile profileProxy = profileManager.getHearingAidProfile();
if (profileProxy == null) {
log("updateHearingAidsDevices: getHearingAidProfile() is null");
return;
}
final Set<Long> syncIdChangedSet = new HashSet<Long>();
for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
if (cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
continue;
}
long newHiSyncId = profileProxy.getHiSyncId(cachedDevice.getDevice());
if (newHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
cachedDevice.setHiSyncId(newHiSyncId);
syncIdChangedSet.add(newHiSyncId);
}
}
for (Long syncId : syncIdChangedSet) {
onHiSyncIdChanged(syncId);
}
}
/**
* Attempts to get the name of a remote device, otherwise returns the address.
*
* @param device The remote device.
* @return The name, or if unavailable, the address.
*/
public String getName(BluetoothDevice device) {
CachedBluetoothDevice cachedDevice = findDevice(device);
if (cachedDevice != null && cachedDevice.getName() != null) {
return cachedDevice.getName();
}
String name = device.getAliasName();
if (name != null) {
return name;
}
return device.getAddress();
}
public synchronized void clearNonBondedDevices() {
mCachedDevicesMapForHearingAids.entrySet().removeIf(entries
-> entries.getValue().getBondState() == BluetoothDevice.BOND_NONE);
mCachedDevices.removeIf(cachedDevice
-> cachedDevice.getBondState() == BluetoothDevice.BOND_NONE);
mHearingAidDevicesNotAddedInCache.removeIf(hearingAidDevice
-> hearingAidDevice.getBondState() == BluetoothDevice.BOND_NONE);
}
public synchronized void onScanningStateChanged(boolean started) {
if (!started) return;
// If starting a new scan, clear old visibility
// Iterate in reverse order since devices may be removed.
for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
cachedDevice.setJustDiscovered(false);
}
for (int i = mHearingAidDevicesNotAddedInCache.size() - 1; i >= 0; i--) {
CachedBluetoothDevice notCachedDevice = mHearingAidDevicesNotAddedInCache.get(i);
notCachedDevice.setJustDiscovered(false);
}
}
public synchronized void onBtClassChanged(BluetoothDevice device) {
CachedBluetoothDevice cachedDevice = findDevice(device);
if (cachedDevice != null) {
cachedDevice.dispatchAttributesChanged();
}
}
public synchronized void onUuidChanged(BluetoothDevice device) {
CachedBluetoothDevice cachedDevice = findDevice(device);
if (cachedDevice != null) {
cachedDevice.onUuidChanged();
}
}
public synchronized void onBluetoothStateChanged(int bluetoothState) {
// When Bluetooth is turning off, we need to clear the non-bonded devices
// Otherwise, they end up showing up on the next BT enable
if (bluetoothState == BluetoothAdapter.STATE_TURNING_OFF) {
for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
if (cachedDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
cachedDevice.setJustDiscovered(false);
mCachedDevices.remove(i);
if (cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID
&& mCachedDevicesMapForHearingAids.containsKey(cachedDevice.getHiSyncId()))
{
mCachedDevicesMapForHearingAids.remove(cachedDevice.getHiSyncId());
}
} else {
// For bonded devices, we need to clear the connection status so that
// when BT is enabled next time, device connection status shall be retrieved
// by making a binder call.
cachedDevice.clearProfileConnectionState();
}
}
for (int i = mHearingAidDevicesNotAddedInCache.size() - 1; i >= 0; i--) {
CachedBluetoothDevice notCachedDevice = mHearingAidDevicesNotAddedInCache.get(i);
if (notCachedDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
notCachedDevice.setJustDiscovered(false);
mHearingAidDevicesNotAddedInCache.remove(i);
} else {
// For bonded devices, we need to clear the connection status so that
// when BT is enabled next time, device connection status shall be retrieved
// by making a binder call.
notCachedDevice.clearProfileConnectionState();
}
}
}
}
public synchronized void onActiveDeviceChanged(CachedBluetoothDevice activeDevice,
int bluetoothProfile) {
for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
boolean isActive = Objects.equals(cachedDevice, activeDevice);
cachedDevice.onActiveDeviceChanged(isActive, bluetoothProfile);
}
}
public synchronized void onHiSyncIdChanged(long hiSyncId) {
int firstMatchedIndex = -1;
for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
if (cachedDevice.getHiSyncId() == hiSyncId) {
if (firstMatchedIndex != -1) {
/* Found the second one */
int indexToRemoveFromUi;
CachedBluetoothDevice deviceToRemoveFromUi;
// Since the hiSyncIds have been updated for a connected pair of hearing aids,
// we remove the entry of one the hearing aids from the UI. Unless the
// hiSyncId get updated, the system does not know it is a hearing aid, so we add
// both the hearing aids as separate entries in the UI first, then remove one
// of them after the hiSyncId is populated. We will choose the device that
// is not connected to be removed.
if (cachedDevice.isConnected()) {
indexToRemoveFromUi = firstMatchedIndex;
deviceToRemoveFromUi = mCachedDevices.get(firstMatchedIndex);
mCachedDevicesMapForHearingAids.put(hiSyncId, cachedDevice);
} else {
indexToRemoveFromUi = i;
deviceToRemoveFromUi = cachedDevice;
mCachedDevicesMapForHearingAids.put(hiSyncId,
mCachedDevices.get(firstMatchedIndex));
}
mCachedDevices.remove(indexToRemoveFromUi);
mHearingAidDevicesNotAddedInCache.add(deviceToRemoveFromUi);
log("onHiSyncIdChanged: removed from UI device=" + deviceToRemoveFromUi
+ ", with hiSyncId=" + hiSyncId);
mBtManager.getEventManager().dispatchDeviceRemoved(deviceToRemoveFromUi);
break;
} else {
mCachedDevicesMapForHearingAids.put(hiSyncId, cachedDevice);
firstMatchedIndex = i;
}
}
}
}
private CachedBluetoothDevice getHearingAidOtherDevice(CachedBluetoothDevice thisDevice,
long hiSyncId) {
if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID) {
return null;
}
// Searched the lists for the other side device with the matching hiSyncId.
for (CachedBluetoothDevice notCachedDevice : mHearingAidDevicesNotAddedInCache) {
if ((hiSyncId == notCachedDevice.getHiSyncId()) &&
(!Objects.equals(notCachedDevice, thisDevice))) {
return notCachedDevice;
}
}
CachedBluetoothDevice cachedDevice = mCachedDevicesMapForHearingAids.get(hiSyncId);
if (!Objects.equals(cachedDevice, thisDevice)) {
return cachedDevice;
}
return null;
}
private void hearingAidSwitchDisplayDevice(CachedBluetoothDevice toDisplayDevice,
CachedBluetoothDevice toHideDevice, long hiSyncId)
{
log("hearingAidSwitchDisplayDevice: toDisplayDevice=" + toDisplayDevice
+ ", toHideDevice=" + toHideDevice);
// Remove the "toHideDevice" device from the UI.
mHearingAidDevicesNotAddedInCache.add(toHideDevice);
mCachedDevices.remove(toHideDevice);
mBtManager.getEventManager().dispatchDeviceRemoved(toHideDevice);
// Add the "toDisplayDevice" device to the UI.
mHearingAidDevicesNotAddedInCache.remove(toDisplayDevice);
mCachedDevices.add(toDisplayDevice);
mCachedDevicesMapForHearingAids.put(hiSyncId, toDisplayDevice);
mBtManager.getEventManager().dispatchDeviceAdded(toDisplayDevice);
}
public synchronized void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice,
int state, int bluetoothProfile) {
if (bluetoothProfile == BluetoothProfile.HEARING_AID
&& cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID
&& cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED) {
long hiSyncId = cachedDevice.getHiSyncId();
CachedBluetoothDevice otherDevice = getHearingAidOtherDevice(cachedDevice, hiSyncId);
if (otherDevice == null) {
// no other side device. Nothing to do.
return;
}
if (state == BluetoothProfile.STATE_CONNECTED &&
mHearingAidDevicesNotAddedInCache.contains(cachedDevice)) {
hearingAidSwitchDisplayDevice(cachedDevice, otherDevice, hiSyncId);
} else if (state == BluetoothProfile.STATE_DISCONNECTED
&& otherDevice.isConnected()) {
CachedBluetoothDevice mapDevice = mCachedDevicesMapForHearingAids.get(hiSyncId);
if ((mapDevice != null) && (Objects.equals(cachedDevice, mapDevice))) {
hearingAidSwitchDisplayDevice(otherDevice, cachedDevice, hiSyncId);
}
}
}
}
public synchronized void onDeviceUnpaired(CachedBluetoothDevice device) {
final long hiSyncId = device.getHiSyncId();
if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID) return;
for (int i = mHearingAidDevicesNotAddedInCache.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mHearingAidDevicesNotAddedInCache.get(i);
if (cachedDevice.getHiSyncId() == hiSyncId) {
// TODO: Look for more cleanups on unpairing the device.
mHearingAidDevicesNotAddedInCache.remove(i);
if (device == cachedDevice) continue;
log("onDeviceUnpaired: Unpair device=" + cachedDevice);
cachedDevice.unpair();
}
}
CachedBluetoothDevice mappedDevice = mCachedDevicesMapForHearingAids.get(hiSyncId);
if ((mappedDevice != null) && (!Objects.equals(device, mappedDevice))) {
log("onDeviceUnpaired: Unpair mapped device=" + mappedDevice);
mappedDevice.unpair();
}
}
public synchronized void dispatchAudioModeChanged() {
for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
cachedDevice.onAudioModeChanged();
}
}
private void log(String msg) {
if (DEBUG) {
Log.d(TAG, msg);
}
}
}