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.bluetooth.BluetoothUuid; 22 import android.bluetooth.le.ScanFilter; 23 import android.content.ContentResolver; 24 import android.content.Context; 25 import android.media.AudioDeviceAttributes; 26 import android.media.audiopolicy.AudioProductStrategy; 27 import android.os.ParcelUuid; 28 import android.provider.Settings; 29 import android.util.Log; 30 31 import com.android.internal.annotations.VisibleForTesting; 32 33 import java.util.HashSet; 34 import java.util.List; 35 import java.util.Set; 36 37 /** 38 * HearingAidDeviceManager manages the set of remote HearingAid(ASHA) Bluetooth devices. 39 */ 40 public class HearingAidDeviceManager { 41 private static final String TAG = "HearingAidDeviceManager"; 42 private static final boolean DEBUG = BluetoothUtils.D; 43 44 private final ContentResolver mContentResolver; 45 private final LocalBluetoothManager mBtManager; 46 private final List<CachedBluetoothDevice> mCachedDevices; 47 private final HearingAidAudioRoutingHelper mRoutingHelper; HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager, List<CachedBluetoothDevice> CachedDevices)48 HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager, 49 List<CachedBluetoothDevice> CachedDevices) { 50 mContentResolver = context.getContentResolver(); 51 mBtManager = localBtManager; 52 mCachedDevices = CachedDevices; 53 mRoutingHelper = new HearingAidAudioRoutingHelper(context); 54 } 55 56 @VisibleForTesting HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager, List<CachedBluetoothDevice> cachedDevices, HearingAidAudioRoutingHelper routingHelper)57 HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager, 58 List<CachedBluetoothDevice> cachedDevices, HearingAidAudioRoutingHelper routingHelper) { 59 mContentResolver = context.getContentResolver(); 60 mBtManager = localBtManager; 61 mCachedDevices = cachedDevices; 62 mRoutingHelper = routingHelper; 63 } 64 initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice, List<ScanFilter> leScanFilters)65 void initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice, 66 List<ScanFilter> leScanFilters) { 67 long hiSyncId = getHiSyncId(newDevice.getDevice()); 68 if (isValidHiSyncId(hiSyncId)) { 69 // Once hiSyncId is valid, assign hearing aid info 70 final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder() 71 .setAshaDeviceSide(getDeviceSide(newDevice.getDevice())) 72 .setAshaDeviceMode(getDeviceMode(newDevice.getDevice())) 73 .setHiSyncId(hiSyncId); 74 newDevice.setHearingAidInfo(infoBuilder.build()); 75 } else if (leScanFilters != null && !newDevice.isHearingAidDevice()) { 76 // If the device is added with hearing aid scan filter during pairing, set an empty 77 // hearing aid info to indicate it's a hearing aid device. The info will be updated 78 // when corresponding profiles connected. 79 for (ScanFilter leScanFilter: leScanFilters) { 80 final ParcelUuid serviceUuid = leScanFilter.getServiceUuid(); 81 final ParcelUuid serviceDataUuid = leScanFilter.getServiceDataUuid(); 82 if (BluetoothUuid.HEARING_AID.equals(serviceUuid) 83 || BluetoothUuid.HAS.equals(serviceUuid) 84 || BluetoothUuid.HEARING_AID.equals(serviceDataUuid) 85 || BluetoothUuid.HAS.equals(serviceDataUuid)) { 86 newDevice.setHearingAidInfo(new HearingAidInfo.Builder().build()); 87 break; 88 } 89 } 90 } 91 } 92 getHiSyncId(BluetoothDevice device)93 private long getHiSyncId(BluetoothDevice device) { 94 final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); 95 final HearingAidProfile profileProxy = profileManager.getHearingAidProfile(); 96 if (profileProxy == null) { 97 return BluetoothHearingAid.HI_SYNC_ID_INVALID; 98 } 99 100 return profileProxy.getHiSyncId(device); 101 } 102 getDeviceSide(BluetoothDevice device)103 private int getDeviceSide(BluetoothDevice device) { 104 final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); 105 final HearingAidProfile profileProxy = profileManager.getHearingAidProfile(); 106 if (profileProxy == null) { 107 Log.w(TAG, "HearingAidProfile is not supported and not ready to fetch device side"); 108 return HearingAidProfile.DeviceSide.SIDE_INVALID; 109 } 110 111 return profileProxy.getDeviceSide(device); 112 } 113 getDeviceMode(BluetoothDevice device)114 private int getDeviceMode(BluetoothDevice device) { 115 final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); 116 final HearingAidProfile profileProxy = profileManager.getHearingAidProfile(); 117 if (profileProxy == null) { 118 Log.w(TAG, "HearingAidProfile is not supported and not ready to fetch device mode"); 119 return HearingAidProfile.DeviceMode.MODE_INVALID; 120 } 121 122 return profileProxy.getDeviceMode(device); 123 } 124 setSubDeviceIfNeeded(CachedBluetoothDevice newDevice)125 boolean setSubDeviceIfNeeded(CachedBluetoothDevice newDevice) { 126 final long hiSyncId = newDevice.getHiSyncId(); 127 if (isValidHiSyncId(hiSyncId)) { 128 final CachedBluetoothDevice hearingAidDevice = getCachedDevice(hiSyncId); 129 // Just add one of the hearing aids from a pair in the list that is shown in the UI. 130 // Once there is another device with the same hiSyncId, to add new device as sub 131 // device. 132 if (hearingAidDevice != null) { 133 hearingAidDevice.setSubDevice(newDevice); 134 return true; 135 } 136 } 137 return false; 138 } 139 isValidHiSyncId(long hiSyncId)140 private boolean isValidHiSyncId(long hiSyncId) { 141 return hiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID; 142 } 143 getCachedDevice(long hiSyncId)144 private CachedBluetoothDevice getCachedDevice(long hiSyncId) { 145 for (int i = mCachedDevices.size() - 1; i >= 0; i--) { 146 CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); 147 if (cachedDevice.getHiSyncId() == hiSyncId) { 148 return cachedDevice; 149 } 150 } 151 return null; 152 } 153 154 // To collect all HearingAid devices and call #onHiSyncIdChanged to group device by HiSyncId updateHearingAidsDevices()155 void updateHearingAidsDevices() { 156 final Set<Long> newSyncIdSet = new HashSet<Long>(); 157 for (CachedBluetoothDevice cachedDevice : mCachedDevices) { 158 // Do nothing if HiSyncId has been assigned 159 if (!isValidHiSyncId(cachedDevice.getHiSyncId())) { 160 final long newHiSyncId = getHiSyncId(cachedDevice.getDevice()); 161 // Do nothing if there is no HiSyncId on Bluetooth device 162 if (isValidHiSyncId(newHiSyncId)) { 163 // Once hiSyncId is valid, assign hearing aid info 164 final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder() 165 .setAshaDeviceSide(getDeviceSide(cachedDevice.getDevice())) 166 .setAshaDeviceMode(getDeviceMode(cachedDevice.getDevice())) 167 .setHiSyncId(newHiSyncId); 168 cachedDevice.setHearingAidInfo(infoBuilder.build()); 169 170 newSyncIdSet.add(newHiSyncId); 171 } 172 } 173 } 174 for (Long syncId : newSyncIdSet) { 175 onHiSyncIdChanged(syncId); 176 } 177 } 178 179 // Group devices by hiSyncId 180 @VisibleForTesting onHiSyncIdChanged(long hiSyncId)181 void onHiSyncIdChanged(long hiSyncId) { 182 int firstMatchedIndex = -1; 183 184 for (int i = mCachedDevices.size() - 1; i >= 0; i--) { 185 CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); 186 if (cachedDevice.getHiSyncId() != hiSyncId) { 187 continue; 188 } 189 190 // The remote device supports CSIP, the other ear should be processed as a member 191 // device. Ignore hiSyncId grouping from ASHA here. 192 if (cachedDevice.getProfiles().stream().anyMatch( 193 profile -> profile instanceof CsipSetCoordinatorProfile)) { 194 continue; 195 } 196 197 if (firstMatchedIndex == -1) { 198 // Found the first one 199 firstMatchedIndex = i; 200 continue; 201 } 202 // Found the second one 203 int indexToRemoveFromUi; 204 CachedBluetoothDevice subDevice; 205 CachedBluetoothDevice mainDevice; 206 // Since the hiSyncIds have been updated for a connected pair of hearing aids, 207 // we remove the entry of one the hearing aids from the UI. Unless the 208 // hiSyncId get updated, the system does not know it is a hearing aid, so we add 209 // both the hearing aids as separate entries in the UI first, then remove one 210 // of them after the hiSyncId is populated. We will choose the device that 211 // is not connected to be removed. 212 if (cachedDevice.isConnected()) { 213 mainDevice = cachedDevice; 214 indexToRemoveFromUi = firstMatchedIndex; 215 subDevice = mCachedDevices.get(firstMatchedIndex); 216 } else { 217 mainDevice = mCachedDevices.get(firstMatchedIndex); 218 indexToRemoveFromUi = i; 219 subDevice = cachedDevice; 220 } 221 222 mainDevice.setSubDevice(subDevice); 223 mCachedDevices.remove(indexToRemoveFromUi); 224 log("onHiSyncIdChanged: removed from UI device =" + subDevice 225 + ", with hiSyncId=" + hiSyncId); 226 mBtManager.getEventManager().dispatchDeviceRemoved(subDevice); 227 break; 228 } 229 } 230 231 // @return {@code true}, the event is processed inside the method. It is for updating 232 // hearing aid device on main-sub relationship when receiving connected or disconnected. 233 // @return {@code false}, it is not hearing aid device or to process it same as other profiles onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice, int state)234 boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice, 235 int state) { 236 switch (state) { 237 case BluetoothProfile.STATE_CONNECTED: 238 onHiSyncIdChanged(cachedDevice.getHiSyncId()); 239 CachedBluetoothDevice mainDevice = findMainDevice(cachedDevice); 240 if (mainDevice != null) { 241 if (mainDevice.isConnected()) { 242 // When main device exists and in connected state, receiving sub device 243 // connection. To refresh main device UI 244 mainDevice.refresh(); 245 } else { 246 // When both Hearing Aid devices are disconnected, receiving sub device 247 // connection. To switch content and dispatch to notify UI change 248 mBtManager.getEventManager().dispatchDeviceRemoved(mainDevice); 249 mainDevice.switchSubDeviceContent(); 250 mainDevice.refresh(); 251 // It is necessary to do remove and add for updating the mapping on 252 // preference and device 253 mBtManager.getEventManager().dispatchDeviceAdded(mainDevice); 254 } 255 return true; 256 } 257 break; 258 case BluetoothProfile.STATE_DISCONNECTED: 259 mainDevice = findMainDevice(cachedDevice); 260 if (cachedDevice.getUnpairing()) { 261 return true; 262 } 263 if (mainDevice != null) { 264 // When main device exists, receiving sub device disconnection 265 // To update main device UI 266 mainDevice.refresh(); 267 return true; 268 } 269 CachedBluetoothDevice subDevice = cachedDevice.getSubDevice(); 270 if (subDevice != null && subDevice.isConnected()) { 271 // Main device is disconnected and sub device is connected 272 // To copy data from sub device to main device 273 mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice); 274 cachedDevice.switchSubDeviceContent(); 275 cachedDevice.refresh(); 276 // It is necessary to do remove and add for updating the mapping on 277 // preference and device 278 mBtManager.getEventManager().dispatchDeviceAdded(cachedDevice); 279 280 return true; 281 } 282 break; 283 } 284 return false; 285 } 286 onActiveDeviceChanged(CachedBluetoothDevice device)287 void onActiveDeviceChanged(CachedBluetoothDevice device) { 288 if (device.isActiveDevice(BluetoothProfile.HEARING_AID) || device.isActiveDevice( 289 BluetoothProfile.LE_AUDIO)) { 290 setAudioRoutingConfig(device); 291 } else { 292 clearAudioRoutingConfig(); 293 } 294 } 295 setAudioRoutingConfig(CachedBluetoothDevice device)296 private void setAudioRoutingConfig(CachedBluetoothDevice device) { 297 AudioDeviceAttributes hearingDeviceAttributes = 298 mRoutingHelper.getMatchedHearingDeviceAttributes(device); 299 if (hearingDeviceAttributes == null) { 300 Log.w(TAG, "Can not find expected AudioDeviceAttributes for hearing device: " 301 + device.getDevice().getAnonymizedAddress()); 302 return; 303 } 304 305 final int callRoutingValue = Settings.Secure.getInt(mContentResolver, 306 Settings.Secure.HEARING_AID_CALL_ROUTING, 307 HearingAidAudioRoutingConstants.RoutingValue.AUTO); 308 final int mediaRoutingValue = Settings.Secure.getInt(mContentResolver, 309 Settings.Secure.HEARING_AID_MEDIA_ROUTING, 310 HearingAidAudioRoutingConstants.RoutingValue.AUTO); 311 final int ringtoneRoutingValue = Settings.Secure.getInt(mContentResolver, 312 Settings.Secure.HEARING_AID_RINGTONE_ROUTING, 313 HearingAidAudioRoutingConstants.RoutingValue.AUTO); 314 final int systemSoundsRoutingValue = Settings.Secure.getInt(mContentResolver, 315 Settings.Secure.HEARING_AID_SYSTEM_SOUNDS_ROUTING, 316 HearingAidAudioRoutingConstants.RoutingValue.AUTO); 317 318 setPreferredDeviceRoutingStrategies( 319 HearingAidAudioRoutingConstants.CALL_ROUTING_ATTRIBUTES, 320 hearingDeviceAttributes, callRoutingValue); 321 setPreferredDeviceRoutingStrategies( 322 HearingAidAudioRoutingConstants.MEDIA_ROUTING_ATTRIBUTES, 323 hearingDeviceAttributes, mediaRoutingValue); 324 setPreferredDeviceRoutingStrategies( 325 HearingAidAudioRoutingConstants.RINGTONE_ROUTING_ATTRIBUTE, 326 hearingDeviceAttributes, ringtoneRoutingValue); 327 setPreferredDeviceRoutingStrategies( 328 HearingAidAudioRoutingConstants.SYSTEM_SOUNDS_ROUTING_ATTRIBUTES, 329 hearingDeviceAttributes, systemSoundsRoutingValue); 330 } 331 clearAudioRoutingConfig()332 private void clearAudioRoutingConfig() { 333 // Don't need to pass hearingDevice when we want to reset it (set to AUTO). 334 setPreferredDeviceRoutingStrategies( 335 HearingAidAudioRoutingConstants.CALL_ROUTING_ATTRIBUTES, 336 /* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO); 337 setPreferredDeviceRoutingStrategies( 338 HearingAidAudioRoutingConstants.MEDIA_ROUTING_ATTRIBUTES, 339 /* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO); 340 setPreferredDeviceRoutingStrategies( 341 HearingAidAudioRoutingConstants.RINGTONE_ROUTING_ATTRIBUTE, 342 /* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO); 343 setPreferredDeviceRoutingStrategies( 344 HearingAidAudioRoutingConstants.SYSTEM_SOUNDS_ROUTING_ATTRIBUTES, 345 /* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO); 346 } 347 setPreferredDeviceRoutingStrategies(int[] attributeSdkUsageList, AudioDeviceAttributes hearingDevice, @HearingAidAudioRoutingConstants.RoutingValue int routingValue)348 private void setPreferredDeviceRoutingStrategies(int[] attributeSdkUsageList, 349 AudioDeviceAttributes hearingDevice, 350 @HearingAidAudioRoutingConstants.RoutingValue int routingValue) { 351 final List<AudioProductStrategy> supportedStrategies = 352 mRoutingHelper.getSupportedStrategies(attributeSdkUsageList); 353 354 final boolean status = mRoutingHelper.setPreferredDeviceRoutingStrategies( 355 supportedStrategies, hearingDevice, routingValue); 356 357 if (!status) { 358 Log.w(TAG, "routingStrategies: " + supportedStrategies.toString() + "routingValue: " 359 + routingValue + " fail to configure AudioProductStrategy"); 360 } 361 } 362 findMainDevice(CachedBluetoothDevice device)363 CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) { 364 for (CachedBluetoothDevice cachedDevice : mCachedDevices) { 365 if (isValidHiSyncId(cachedDevice.getHiSyncId())) { 366 CachedBluetoothDevice subDevice = cachedDevice.getSubDevice(); 367 if (subDevice != null && subDevice.equals(device)) { 368 return cachedDevice; 369 } 370 } 371 } 372 return null; 373 } 374 log(String msg)375 private void log(String msg) { 376 if (DEBUG) { 377 Log.d(TAG, msg); 378 } 379 } 380 }