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