1 /* 2 * Copyright (C) 2018 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 package com.android.settingslib.bluetooth; 17 18 import android.bluetooth.BluetoothDevice; 19 import android.bluetooth.BluetoothHearingAid; 20 import android.bluetooth.BluetoothProfile; 21 import android.util.Log; 22 23 import com.android.internal.annotations.VisibleForTesting; 24 25 import java.util.HashSet; 26 import java.util.List; 27 import java.util.Set; 28 29 /** 30 * HearingAidDeviceManager manages the set of remote HearingAid Bluetooth devices. 31 */ 32 public class HearingAidDeviceManager { 33 private static final String TAG = "HearingAidDeviceManager"; 34 private static final boolean DEBUG = BluetoothUtils.D; 35 36 private final LocalBluetoothManager mBtManager; 37 private final List<CachedBluetoothDevice> mCachedDevices; HearingAidDeviceManager(LocalBluetoothManager localBtManager, List<CachedBluetoothDevice> CachedDevices)38 HearingAidDeviceManager(LocalBluetoothManager localBtManager, 39 List<CachedBluetoothDevice> CachedDevices) { 40 mBtManager = localBtManager; 41 mCachedDevices = CachedDevices; 42 } 43 initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice)44 void initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice) { 45 long hiSyncId = getHiSyncId(newDevice.getDevice()); 46 if (isValidHiSyncId(hiSyncId)) { 47 // Once hiSyncId is valid, assign hiSyncId 48 newDevice.setHiSyncId(hiSyncId); 49 final int side = getDeviceSide(newDevice.getDevice()); 50 final int mode = getDeviceMode(newDevice.getDevice()); 51 newDevice.setDeviceSide(side); 52 newDevice.setDeviceMode(mode); 53 } 54 } 55 getHiSyncId(BluetoothDevice device)56 private long getHiSyncId(BluetoothDevice device) { 57 final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); 58 final HearingAidProfile profileProxy = profileManager.getHearingAidProfile(); 59 if (profileProxy == null) { 60 return BluetoothHearingAid.HI_SYNC_ID_INVALID; 61 } 62 63 return profileProxy.getHiSyncId(device); 64 } 65 getDeviceSide(BluetoothDevice device)66 private int getDeviceSide(BluetoothDevice device) { 67 final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); 68 final HearingAidProfile profileProxy = profileManager.getHearingAidProfile(); 69 if (profileProxy == null) { 70 Log.w(TAG, "HearingAidProfile is not supported and not ready to fetch device side"); 71 return HearingAidProfile.DeviceSide.SIDE_INVALID; 72 } 73 74 return profileProxy.getDeviceSide(device); 75 } 76 getDeviceMode(BluetoothDevice device)77 private int getDeviceMode(BluetoothDevice device) { 78 final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); 79 final HearingAidProfile profileProxy = profileManager.getHearingAidProfile(); 80 if (profileProxy == null) { 81 Log.w(TAG, "HearingAidProfile is not supported and not ready to fetch device mode"); 82 return HearingAidProfile.DeviceMode.MODE_INVALID; 83 } 84 85 return profileProxy.getDeviceMode(device); 86 } 87 setSubDeviceIfNeeded(CachedBluetoothDevice newDevice)88 boolean setSubDeviceIfNeeded(CachedBluetoothDevice newDevice) { 89 final long hiSyncId = newDevice.getHiSyncId(); 90 if (isValidHiSyncId(hiSyncId)) { 91 final CachedBluetoothDevice hearingAidDevice = getCachedDevice(hiSyncId); 92 // Just add one of the hearing aids from a pair in the list that is shown in the UI. 93 // Once there is another device with the same hiSyncId, to add new device as sub 94 // device. 95 if (hearingAidDevice != null) { 96 hearingAidDevice.setSubDevice(newDevice); 97 return true; 98 } 99 } 100 return false; 101 } 102 isValidHiSyncId(long hiSyncId)103 private boolean isValidHiSyncId(long hiSyncId) { 104 return hiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID; 105 } 106 getCachedDevice(long hiSyncId)107 private CachedBluetoothDevice getCachedDevice(long hiSyncId) { 108 for (int i = mCachedDevices.size() - 1; i >= 0; i--) { 109 CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); 110 if (cachedDevice.getHiSyncId() == hiSyncId) { 111 return cachedDevice; 112 } 113 } 114 return null; 115 } 116 117 // To collect all HearingAid devices and call #onHiSyncIdChanged to group device by HiSyncId updateHearingAidsDevices()118 void updateHearingAidsDevices() { 119 final Set<Long> newSyncIdSet = new HashSet<Long>(); 120 for (CachedBluetoothDevice cachedDevice : mCachedDevices) { 121 // Do nothing if HiSyncId has been assigned 122 if (!isValidHiSyncId(cachedDevice.getHiSyncId())) { 123 final long newHiSyncId = getHiSyncId(cachedDevice.getDevice()); 124 // Do nothing if there is no HiSyncId on Bluetooth device 125 if (isValidHiSyncId(newHiSyncId)) { 126 cachedDevice.setHiSyncId(newHiSyncId); 127 newSyncIdSet.add(newHiSyncId); 128 final int side = getDeviceSide(cachedDevice.getDevice()); 129 final int mode = getDeviceMode(cachedDevice.getDevice()); 130 cachedDevice.setDeviceSide(side); 131 cachedDevice.setDeviceMode(mode); 132 } 133 } 134 } 135 for (Long syncId : newSyncIdSet) { 136 onHiSyncIdChanged(syncId); 137 } 138 } 139 140 // Group devices by hiSyncId 141 @VisibleForTesting onHiSyncIdChanged(long hiSyncId)142 void onHiSyncIdChanged(long hiSyncId) { 143 int firstMatchedIndex = -1; 144 145 for (int i = mCachedDevices.size() - 1; i >= 0; i--) { 146 CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); 147 if (cachedDevice.getHiSyncId() != hiSyncId) { 148 continue; 149 } 150 if (firstMatchedIndex == -1) { 151 // Found the first one 152 firstMatchedIndex = i; 153 continue; 154 } 155 // Found the second one 156 int indexToRemoveFromUi; 157 CachedBluetoothDevice subDevice; 158 CachedBluetoothDevice mainDevice; 159 // Since the hiSyncIds have been updated for a connected pair of hearing aids, 160 // we remove the entry of one the hearing aids from the UI. Unless the 161 // hiSyncId get updated, the system does not know it is a hearing aid, so we add 162 // both the hearing aids as separate entries in the UI first, then remove one 163 // of them after the hiSyncId is populated. We will choose the device that 164 // is not connected to be removed. 165 if (cachedDevice.isConnected()) { 166 mainDevice = cachedDevice; 167 indexToRemoveFromUi = firstMatchedIndex; 168 subDevice = mCachedDevices.get(firstMatchedIndex); 169 } else { 170 mainDevice = mCachedDevices.get(firstMatchedIndex); 171 indexToRemoveFromUi = i; 172 subDevice = cachedDevice; 173 } 174 175 mainDevice.setSubDevice(subDevice); 176 mCachedDevices.remove(indexToRemoveFromUi); 177 log("onHiSyncIdChanged: removed from UI device =" + subDevice 178 + ", with hiSyncId=" + hiSyncId); 179 mBtManager.getEventManager().dispatchDeviceRemoved(subDevice); 180 break; 181 } 182 } 183 184 // @return {@code true}, the event is processed inside the method. It is for updating 185 // hearing aid device on main-sub relationship when receiving connected or disconnected. 186 // @return {@code false}, it is not hearing aid device or to process it same as other profiles onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice, int state)187 boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice, 188 int state) { 189 switch (state) { 190 case BluetoothProfile.STATE_CONNECTED: 191 onHiSyncIdChanged(cachedDevice.getHiSyncId()); 192 CachedBluetoothDevice mainDevice = findMainDevice(cachedDevice); 193 if (mainDevice != null){ 194 if (mainDevice.isConnected()) { 195 // When main device exists and in connected state, receiving sub device 196 // connection. To refresh main device UI 197 mainDevice.refresh(); 198 return true; 199 } else { 200 // When both Hearing Aid devices are disconnected, receiving sub device 201 // connection. To switch content and dispatch to notify UI change 202 mBtManager.getEventManager().dispatchDeviceRemoved(mainDevice); 203 mainDevice.switchSubDeviceContent(); 204 mainDevice.refresh(); 205 // It is necessary to do remove and add for updating the mapping on 206 // preference and device 207 mBtManager.getEventManager().dispatchDeviceAdded(mainDevice); 208 return true; 209 } 210 } 211 break; 212 case BluetoothProfile.STATE_DISCONNECTED: 213 mainDevice = findMainDevice(cachedDevice); 214 if (cachedDevice.getUnpairing()) { 215 return true; 216 } 217 if (mainDevice != null) { 218 // When main device exists, receiving sub device disconnection 219 // To update main device UI 220 mainDevice.refresh(); 221 return true; 222 } 223 CachedBluetoothDevice subDevice = cachedDevice.getSubDevice(); 224 if (subDevice != null && subDevice.isConnected()) { 225 // Main device is disconnected and sub device is connected 226 // To copy data from sub device to main device 227 mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice); 228 cachedDevice.switchSubDeviceContent(); 229 cachedDevice.refresh(); 230 // It is necessary to do remove and add for updating the mapping on 231 // preference and device 232 mBtManager.getEventManager().dispatchDeviceAdded(cachedDevice); 233 return true; 234 } 235 break; 236 } 237 return false; 238 } 239 findMainDevice(CachedBluetoothDevice device)240 CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) { 241 for (CachedBluetoothDevice cachedDevice : mCachedDevices) { 242 if (isValidHiSyncId(cachedDevice.getHiSyncId())) { 243 CachedBluetoothDevice subDevice = cachedDevice.getSubDevice(); 244 if (subDevice != null && subDevice.equals(device)) { 245 return cachedDevice; 246 } 247 } 248 } 249 return null; 250 } 251 log(String msg)252 private void log(String msg) { 253 if (DEBUG) { 254 Log.d(TAG, msg); 255 } 256 } 257 }