Home | History | Annotate | Download | only in bluetooth
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.settingslib.bluetooth;
     18 
     19 import android.bluetooth.BluetoothAdapter;
     20 import android.bluetooth.BluetoothDevice;
     21 import android.bluetooth.BluetoothHearingAid;
     22 import android.bluetooth.BluetoothProfile;
     23 import android.content.Context;
     24 import android.util.Log;
     25 
     26 import com.android.internal.annotations.VisibleForTesting;
     27 
     28 import java.util.ArrayList;
     29 import java.util.Collection;
     30 import java.util.HashMap;
     31 import java.util.HashSet;
     32 import java.util.List;
     33 import java.util.Map;
     34 import java.util.Set;
     35 import java.util.Objects;
     36 
     37 /**
     38  * CachedBluetoothDeviceManager manages the set of remote Bluetooth devices.
     39  */
     40 public class CachedBluetoothDeviceManager {
     41     private static final String TAG = "CachedBluetoothDeviceManager";
     42     private static final boolean DEBUG = Utils.D;
     43 
     44     private Context mContext;
     45     private final LocalBluetoothManager mBtManager;
     46 
     47     @VisibleForTesting
     48     final List<CachedBluetoothDevice> mCachedDevices =
     49         new ArrayList<CachedBluetoothDevice>();
     50     // Contains the list of hearing aid devices that should not be shown in the UI.
     51     @VisibleForTesting
     52     final List<CachedBluetoothDevice> mHearingAidDevicesNotAddedInCache
     53         = new ArrayList<CachedBluetoothDevice>();
     54     // Maintains a list of devices which are added in mCachedDevices and have hiSyncIds.
     55     @VisibleForTesting
     56     final Map<Long, CachedBluetoothDevice> mCachedDevicesMapForHearingAids
     57         = new HashMap<Long, CachedBluetoothDevice>();
     58 
     59     CachedBluetoothDeviceManager(Context context, LocalBluetoothManager localBtManager) {
     60         mContext = context;
     61         mBtManager = localBtManager;
     62     }
     63 
     64     public synchronized Collection<CachedBluetoothDevice> getCachedDevicesCopy() {
     65         return new ArrayList<CachedBluetoothDevice>(mCachedDevices);
     66     }
     67 
     68     public static boolean onDeviceDisappeared(CachedBluetoothDevice cachedDevice) {
     69         cachedDevice.setJustDiscovered(false);
     70         return cachedDevice.getBondState() == BluetoothDevice.BOND_NONE;
     71     }
     72 
     73     public void onDeviceNameUpdated(BluetoothDevice device) {
     74         CachedBluetoothDevice cachedDevice = findDevice(device);
     75         if (cachedDevice != null) {
     76             cachedDevice.refreshName();
     77         }
     78     }
     79 
     80     /**
     81      * Search for existing {@link CachedBluetoothDevice} or return null
     82      * if this device isn't in the cache. Use {@link #addDevice}
     83      * to create and return a new {@link CachedBluetoothDevice} for
     84      * a newly discovered {@link BluetoothDevice}.
     85      *
     86      * @param device the address of the Bluetooth device
     87      * @return the cached device object for this device, or null if it has
     88      *   not been previously seen
     89      */
     90     public synchronized CachedBluetoothDevice findDevice(BluetoothDevice device) {
     91         for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
     92             if (cachedDevice.getDevice().equals(device)) {
     93                 return cachedDevice;
     94             }
     95         }
     96         for (CachedBluetoothDevice notCachedDevice : mHearingAidDevicesNotAddedInCache) {
     97             if (notCachedDevice.getDevice().equals(device)) {
     98                 return notCachedDevice;
     99             }
    100         }
    101         return null;
    102     }
    103 
    104     /**
    105      * Create and return a new {@link CachedBluetoothDevice}. This assumes
    106      * that {@link #findDevice} has already been called and returned null.
    107      * @param device the address of the new Bluetooth device
    108      * @return the newly created CachedBluetoothDevice object
    109      */
    110     public CachedBluetoothDevice addDevice(LocalBluetoothAdapter adapter,
    111             LocalBluetoothProfileManager profileManager,
    112             BluetoothDevice device) {
    113         CachedBluetoothDevice newDevice = new CachedBluetoothDevice(mContext, adapter,
    114             profileManager, device);
    115         if (profileManager.getHearingAidProfile() != null
    116             && profileManager.getHearingAidProfile().getHiSyncId(newDevice.getDevice())
    117                 != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
    118             newDevice.setHiSyncId(profileManager.getHearingAidProfile()
    119                 .getHiSyncId(newDevice.getDevice()));
    120         }
    121         // Just add one of the hearing aids from a pair in the list that is shown in the UI.
    122         if (isPairAddedInCache(newDevice.getHiSyncId())) {
    123             synchronized (this) {
    124                 mHearingAidDevicesNotAddedInCache.add(newDevice);
    125             }
    126         } else {
    127             synchronized (this) {
    128                 mCachedDevices.add(newDevice);
    129                 if (newDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID
    130                     && !mCachedDevicesMapForHearingAids.containsKey(newDevice.getHiSyncId())) {
    131                     mCachedDevicesMapForHearingAids.put(newDevice.getHiSyncId(), newDevice);
    132                 }
    133                 mBtManager.getEventManager().dispatchDeviceAdded(newDevice);
    134             }
    135         }
    136 
    137         return newDevice;
    138     }
    139 
    140     /**
    141      * Returns true if the one of the two hearing aid devices is already cached for UI.
    142      *
    143      * @param long hiSyncId
    144      * @return {@code True} if one of the two hearing aid devices is is already cached for UI.
    145      */
    146     private synchronized boolean isPairAddedInCache(long hiSyncId) {
    147         if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID) {
    148             return false;
    149         }
    150         if(mCachedDevicesMapForHearingAids.containsKey(hiSyncId)) {
    151             return true;
    152         }
    153         return false;
    154     }
    155 
    156     /**
    157      * Returns device summary of the pair of the hearing aid passed as the parameter.
    158      *
    159      * @param CachedBluetoothDevice device
    160      * @return Device summary, or if the pair does not exist or if its not a hearing aid,
    161      * then {@code null}.
    162      */
    163     public synchronized String getHearingAidPairDeviceSummary(CachedBluetoothDevice device) {
    164         String pairDeviceSummary = null;
    165         if (device.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
    166             for (CachedBluetoothDevice hearingAidDevice : mHearingAidDevicesNotAddedInCache) {
    167                 if (hearingAidDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID
    168                     && hearingAidDevice.getHiSyncId() == device.getHiSyncId()) {
    169                     pairDeviceSummary = hearingAidDevice.getConnectionSummary();
    170                 }
    171             }
    172         }
    173         return pairDeviceSummary;
    174     }
    175 
    176     /**
    177      * Adds the 2nd hearing aid in a pair in a list that maintains the hearing aids that are
    178      * not dispalyed in the UI.
    179      *
    180      * @param CachedBluetoothDevice device
    181      */
    182     public synchronized void addDeviceNotaddedInMap(CachedBluetoothDevice device) {
    183         mHearingAidDevicesNotAddedInCache.add(device);
    184     }
    185 
    186     /**
    187      * Updates the Hearing Aid devices; specifically the HiSyncId's. This routine is called when the
    188      * Hearing Aid Service is connected and the HiSyncId's are now available.
    189      * @param LocalBluetoothProfileManager profileManager
    190      */
    191     public synchronized void updateHearingAidsDevices(LocalBluetoothProfileManager profileManager) {
    192         HearingAidProfile profileProxy = profileManager.getHearingAidProfile();
    193         if (profileProxy == null) {
    194             log("updateHearingAidsDevices: getHearingAidProfile() is null");
    195             return;
    196         }
    197         final Set<Long> syncIdChangedSet = new HashSet<Long>();
    198         for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
    199             if (cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
    200                 continue;
    201             }
    202 
    203             long newHiSyncId = profileProxy.getHiSyncId(cachedDevice.getDevice());
    204 
    205             if (newHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
    206                 cachedDevice.setHiSyncId(newHiSyncId);
    207                 syncIdChangedSet.add(newHiSyncId);
    208             }
    209         }
    210         for (Long syncId : syncIdChangedSet) {
    211             onHiSyncIdChanged(syncId);
    212         }
    213     }
    214 
    215     /**
    216      * Attempts to get the name of a remote device, otherwise returns the address.
    217      *
    218      * @param device The remote device.
    219      * @return The name, or if unavailable, the address.
    220      */
    221     public String getName(BluetoothDevice device) {
    222         CachedBluetoothDevice cachedDevice = findDevice(device);
    223         if (cachedDevice != null && cachedDevice.getName() != null) {
    224             return cachedDevice.getName();
    225         }
    226 
    227         String name = device.getAliasName();
    228         if (name != null) {
    229             return name;
    230         }
    231 
    232         return device.getAddress();
    233     }
    234 
    235     public synchronized void clearNonBondedDevices() {
    236 
    237         mCachedDevicesMapForHearingAids.entrySet().removeIf(entries
    238             -> entries.getValue().getBondState() == BluetoothDevice.BOND_NONE);
    239 
    240         mCachedDevices.removeIf(cachedDevice
    241             -> cachedDevice.getBondState() == BluetoothDevice.BOND_NONE);
    242 
    243         mHearingAidDevicesNotAddedInCache.removeIf(hearingAidDevice
    244             -> hearingAidDevice.getBondState() == BluetoothDevice.BOND_NONE);
    245     }
    246 
    247     public synchronized void onScanningStateChanged(boolean started) {
    248         if (!started) return;
    249         // If starting a new scan, clear old visibility
    250         // Iterate in reverse order since devices may be removed.
    251         for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
    252             CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
    253             cachedDevice.setJustDiscovered(false);
    254         }
    255         for (int i = mHearingAidDevicesNotAddedInCache.size() - 1; i >= 0; i--) {
    256             CachedBluetoothDevice notCachedDevice = mHearingAidDevicesNotAddedInCache.get(i);
    257             notCachedDevice.setJustDiscovered(false);
    258         }
    259     }
    260 
    261     public synchronized void onBtClassChanged(BluetoothDevice device) {
    262         CachedBluetoothDevice cachedDevice = findDevice(device);
    263         if (cachedDevice != null) {
    264             cachedDevice.refreshBtClass();
    265         }
    266     }
    267 
    268     public synchronized void onUuidChanged(BluetoothDevice device) {
    269         CachedBluetoothDevice cachedDevice = findDevice(device);
    270         if (cachedDevice != null) {
    271             cachedDevice.onUuidChanged();
    272         }
    273     }
    274 
    275     public synchronized void onBluetoothStateChanged(int bluetoothState) {
    276         // When Bluetooth is turning off, we need to clear the non-bonded devices
    277         // Otherwise, they end up showing up on the next BT enable
    278         if (bluetoothState == BluetoothAdapter.STATE_TURNING_OFF) {
    279             for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
    280                 CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
    281                 if (cachedDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
    282                     cachedDevice.setJustDiscovered(false);
    283                     mCachedDevices.remove(i);
    284                     if (cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID
    285                         && mCachedDevicesMapForHearingAids.containsKey(cachedDevice.getHiSyncId()))
    286                     {
    287                         mCachedDevicesMapForHearingAids.remove(cachedDevice.getHiSyncId());
    288                     }
    289                 } else {
    290                     // For bonded devices, we need to clear the connection status so that
    291                     // when BT is enabled next time, device connection status shall be retrieved
    292                     // by making a binder call.
    293                     cachedDevice.clearProfileConnectionState();
    294                 }
    295             }
    296             for (int i = mHearingAidDevicesNotAddedInCache.size() - 1; i >= 0; i--) {
    297                 CachedBluetoothDevice notCachedDevice = mHearingAidDevicesNotAddedInCache.get(i);
    298                 if (notCachedDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
    299                     notCachedDevice.setJustDiscovered(false);
    300                     mHearingAidDevicesNotAddedInCache.remove(i);
    301                 } else {
    302                     // For bonded devices, we need to clear the connection status so that
    303                     // when BT is enabled next time, device connection status shall be retrieved
    304                     // by making a binder call.
    305                     notCachedDevice.clearProfileConnectionState();
    306                 }
    307             }
    308         }
    309     }
    310 
    311     public synchronized void onActiveDeviceChanged(CachedBluetoothDevice activeDevice,
    312                                                    int bluetoothProfile) {
    313         for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
    314             boolean isActive = Objects.equals(cachedDevice, activeDevice);
    315             cachedDevice.onActiveDeviceChanged(isActive, bluetoothProfile);
    316         }
    317     }
    318 
    319     public synchronized void onHiSyncIdChanged(long hiSyncId) {
    320         int firstMatchedIndex = -1;
    321 
    322         for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
    323             CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
    324             if (cachedDevice.getHiSyncId() == hiSyncId) {
    325                 if (firstMatchedIndex != -1) {
    326                     /* Found the second one */
    327                     int indexToRemoveFromUi;
    328                     CachedBluetoothDevice deviceToRemoveFromUi;
    329 
    330                     // Since the hiSyncIds have been updated for a connected pair of hearing aids,
    331                     // we remove the entry of one the hearing aids from the UI. Unless the
    332                     // hiSyncId get updated, the system does not know it is a hearing aid, so we add
    333                     // both the hearing aids as separate entries in the UI first, then remove one
    334                     // of them after the hiSyncId is populated. We will choose the device that
    335                     // is not connected to be removed.
    336                     if (cachedDevice.isConnected()) {
    337                         indexToRemoveFromUi = firstMatchedIndex;
    338                         deviceToRemoveFromUi = mCachedDevices.get(firstMatchedIndex);
    339                         mCachedDevicesMapForHearingAids.put(hiSyncId, cachedDevice);
    340                     } else {
    341                         indexToRemoveFromUi = i;
    342                         deviceToRemoveFromUi = cachedDevice;
    343                         mCachedDevicesMapForHearingAids.put(hiSyncId,
    344                                                             mCachedDevices.get(firstMatchedIndex));
    345                     }
    346 
    347                     mCachedDevices.remove(indexToRemoveFromUi);
    348                     mHearingAidDevicesNotAddedInCache.add(deviceToRemoveFromUi);
    349                     log("onHiSyncIdChanged: removed from UI device=" + deviceToRemoveFromUi
    350                         + ", with hiSyncId=" + hiSyncId);
    351                     mBtManager.getEventManager().dispatchDeviceRemoved(deviceToRemoveFromUi);
    352                     break;
    353                 } else {
    354                     mCachedDevicesMapForHearingAids.put(hiSyncId, cachedDevice);
    355                     firstMatchedIndex = i;
    356                 }
    357             }
    358         }
    359     }
    360 
    361     private CachedBluetoothDevice getHearingAidOtherDevice(CachedBluetoothDevice thisDevice,
    362                                                            long hiSyncId) {
    363         if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID) {
    364             return null;
    365         }
    366 
    367         // Searched the lists for the other side device with the matching hiSyncId.
    368         for (CachedBluetoothDevice notCachedDevice : mHearingAidDevicesNotAddedInCache) {
    369             if ((hiSyncId == notCachedDevice.getHiSyncId()) &&
    370                 (!Objects.equals(notCachedDevice, thisDevice))) {
    371                 return notCachedDevice;
    372             }
    373         }
    374 
    375         CachedBluetoothDevice cachedDevice = mCachedDevicesMapForHearingAids.get(hiSyncId);
    376         if (!Objects.equals(cachedDevice, thisDevice)) {
    377             return cachedDevice;
    378         }
    379         return null;
    380     }
    381 
    382     private void hearingAidSwitchDisplayDevice(CachedBluetoothDevice toDisplayDevice,
    383                                            CachedBluetoothDevice toHideDevice, long hiSyncId)
    384     {
    385         log("hearingAidSwitchDisplayDevice: toDisplayDevice=" + toDisplayDevice
    386             + ", toHideDevice=" + toHideDevice);
    387 
    388         // Remove the "toHideDevice" device from the UI.
    389         mHearingAidDevicesNotAddedInCache.add(toHideDevice);
    390         mCachedDevices.remove(toHideDevice);
    391         mBtManager.getEventManager().dispatchDeviceRemoved(toHideDevice);
    392 
    393         // Add the "toDisplayDevice" device to the UI.
    394         mHearingAidDevicesNotAddedInCache.remove(toDisplayDevice);
    395         mCachedDevices.add(toDisplayDevice);
    396         mCachedDevicesMapForHearingAids.put(hiSyncId, toDisplayDevice);
    397         mBtManager.getEventManager().dispatchDeviceAdded(toDisplayDevice);
    398     }
    399 
    400     public synchronized void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice,
    401                                                              int state, int bluetoothProfile) {
    402         if (bluetoothProfile == BluetoothProfile.HEARING_AID
    403             && cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID
    404             && cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED) {
    405 
    406             long hiSyncId = cachedDevice.getHiSyncId();
    407 
    408             CachedBluetoothDevice otherDevice = getHearingAidOtherDevice(cachedDevice, hiSyncId);
    409             if (otherDevice == null) {
    410                 // no other side device. Nothing to do.
    411                 return;
    412             }
    413 
    414             if (state == BluetoothProfile.STATE_CONNECTED &&
    415                 mHearingAidDevicesNotAddedInCache.contains(cachedDevice)) {
    416                 hearingAidSwitchDisplayDevice(cachedDevice, otherDevice, hiSyncId);
    417             } else if (state == BluetoothProfile.STATE_DISCONNECTED
    418                        && otherDevice.isConnected()) {
    419                 CachedBluetoothDevice mapDevice = mCachedDevicesMapForHearingAids.get(hiSyncId);
    420                 if ((mapDevice != null) && (Objects.equals(cachedDevice, mapDevice))) {
    421                     hearingAidSwitchDisplayDevice(otherDevice, cachedDevice, hiSyncId);
    422                 }
    423             }
    424         }
    425     }
    426 
    427     public synchronized void onDeviceUnpaired(CachedBluetoothDevice device) {
    428         final long hiSyncId = device.getHiSyncId();
    429 
    430         if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID) return;
    431 
    432         for (int i = mHearingAidDevicesNotAddedInCache.size() - 1; i >= 0; i--) {
    433             CachedBluetoothDevice cachedDevice = mHearingAidDevicesNotAddedInCache.get(i);
    434             if (cachedDevice.getHiSyncId() == hiSyncId) {
    435                 // TODO: Look for more cleanups on unpairing the device.
    436                 mHearingAidDevicesNotAddedInCache.remove(i);
    437                 if (device == cachedDevice) continue;
    438                 log("onDeviceUnpaired: Unpair device=" + cachedDevice);
    439                 cachedDevice.unpair();
    440             }
    441         }
    442 
    443         CachedBluetoothDevice mappedDevice = mCachedDevicesMapForHearingAids.get(hiSyncId);
    444         if ((mappedDevice != null) && (!Objects.equals(device, mappedDevice))) {
    445             log("onDeviceUnpaired: Unpair mapped device=" + mappedDevice);
    446             mappedDevice.unpair();
    447         }
    448     }
    449 
    450     public synchronized void dispatchAudioModeChanged() {
    451         for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
    452             cachedDevice.onAudioModeChanged();
    453         }
    454     }
    455 
    456     private void log(String msg) {
    457         if (DEBUG) {
    458             Log.d(TAG, msg);
    459         }
    460     }
    461 }
    462