• 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 static android.bluetooth.BluetoothDevice.BOND_BONDED;
19 
20 import android.annotation.CallbackExecutor;
21 import android.bluetooth.BluetoothCsipSetCoordinator;
22 import android.bluetooth.BluetoothDevice;
23 import android.bluetooth.BluetoothHapClient;
24 import android.bluetooth.BluetoothHearingAid;
25 import android.bluetooth.BluetoothProfile;
26 import android.bluetooth.BluetoothUuid;
27 import android.bluetooth.le.ScanFilter;
28 import android.content.ContentResolver;
29 import android.content.Context;
30 import android.media.AudioDeviceAttributes;
31 import android.media.audiopolicy.AudioProductStrategy;
32 import android.os.ParcelUuid;
33 import android.provider.Settings;
34 import android.util.FeatureFlagUtils;
35 import android.util.Log;
36 
37 import androidx.annotation.IntDef;
38 import androidx.annotation.NonNull;
39 import androidx.collection.ArraySet;
40 
41 import com.android.internal.annotations.VisibleForTesting;
42 import com.android.settingslib.bluetooth.HearingAidAudioRoutingConstants.RoutingValue;
43 
44 import java.lang.annotation.Retention;
45 import java.lang.annotation.RetentionPolicy;
46 import java.util.HashSet;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.Set;
50 import java.util.concurrent.ConcurrentHashMap;
51 import java.util.concurrent.Executor;
52 import java.util.stream.Collectors;
53 
54 /**
55  * HearingAidDeviceManager manages the set of remote bluetooth hearing devices.
56  */
57 public class HearingAidDeviceManager {
58     private static final String TAG = "HearingAidDeviceManager";
59     private static final boolean DEBUG = BluetoothUtils.D;
60 
61     private final ContentResolver mContentResolver;
62     private final Context mContext;
63     private final LocalBluetoothManager mBtManager;
64     private final List<CachedBluetoothDevice> mCachedDevices;
65     private final HearingAidAudioRoutingHelper mRoutingHelper;
66     private static final Map<ConnectionStatusListener, Executor>
67             mConnectionStatusListeners = new ConcurrentHashMap<>();
68     @ConnectionStatus
69     private int mDevicesConnectionStatus = ConnectionStatus.NO_DEVICE_BONDED;
70     private boolean mInitialDevicesConnectionStatusUpdate = false;
71 
HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager, List<CachedBluetoothDevice> CachedDevices)72     HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager,
73             List<CachedBluetoothDevice> CachedDevices) {
74         mContext = context;
75         mContentResolver = context.getContentResolver();
76         mBtManager = localBtManager;
77         mCachedDevices = CachedDevices;
78         mRoutingHelper = new HearingAidAudioRoutingHelper(context);
79     }
80 
81     @VisibleForTesting
HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager, List<CachedBluetoothDevice> cachedDevices, HearingAidAudioRoutingHelper routingHelper)82     HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager,
83             List<CachedBluetoothDevice> cachedDevices, HearingAidAudioRoutingHelper routingHelper) {
84         mContext = context;
85         mContentResolver = context.getContentResolver();
86         mBtManager = localBtManager;
87         mCachedDevices = cachedDevices;
88         mRoutingHelper = routingHelper;
89     }
90 
91     /**
92      * Defines the connection status for hearing devices.
93      */
94     @Retention(RetentionPolicy.SOURCE)
95     @IntDef({
96             ConnectionStatus.NO_DEVICE_BONDED,
97             ConnectionStatus.DISCONNECTED,
98             ConnectionStatus.CONNECTED,
99             ConnectionStatus.CONNECTING_OR_DISCONNECTING,
100             ConnectionStatus.ACTIVE
101     })
102     public @interface ConnectionStatus {
103         int NO_DEVICE_BONDED = -1;
104         int DISCONNECTED = 0;
105         int CONNECTED = 1;
106         int CONNECTING_OR_DISCONNECTING = 2;
107         int ACTIVE = 3;
108     }
109     /**
110      * Interface for connection status listener.
111      */
112     public interface ConnectionStatusListener {
113         /**
114          * Callback when hearing devices connection status change.
115          *
116          * <p>devices here means singular device or binaural device.
117          * E.g. One of hearing device is in CONNECTED status and another is in DISCONNECTED,
118          * it will callback CONNECTED status.
119          *
120          * @param status Updated {@link ConnectionStatus}
121          */
onDevicesConnectionStatusChanged(@onnectionStatus int status)122         void onDevicesConnectionStatusChanged(@ConnectionStatus int status);
123     }
124 
125     /**
126      * Registers a listener to be notified of connection status changes.
127      *
128      * @param listener The listener to register.
129      * @param executor The executor on which the listener's callback will be run.
130      */
registerConnectionStatusListener( @onNull ConnectionStatusListener listener, @NonNull @CallbackExecutor Executor executor)131     public void registerConnectionStatusListener(
132             @NonNull ConnectionStatusListener listener,
133             @NonNull @CallbackExecutor Executor executor) {
134         mConnectionStatusListeners.put(listener, executor);
135     }
136 
137     /**
138      * Unregisters a listener previously registered with
139      * {@link #registerConnectionStatusListener(ConnectionStatusListener, Executor)}.
140      *
141      * @param listener The listener to unregister.
142      */
unregisterConnectionStatusListener( @onNull ConnectionStatusListener listener)143     public void unregisterConnectionStatusListener(
144             @NonNull ConnectionStatusListener listener) {
145         mConnectionStatusListeners.remove(listener);
146     }
147 
notifyDevicesConnectionStatusChanged(int status)148     private void notifyDevicesConnectionStatusChanged(int status) {
149         mConnectionStatusListeners.forEach((listener, executor) ->
150                 executor.execute(() -> listener.onDevicesConnectionStatusChanged(status)));
151     }
152 
153     /**
154      * Updates the connection status of the hearing devices based on the currently bonded
155      * hearing aid devices.
156      */
notifyDevicesConnectionStatusChanged()157     synchronized void notifyDevicesConnectionStatusChanged() {
158         final int prevVal = mDevicesConnectionStatus;
159         updateDevicesConnectionStatus();
160         if (mDevicesConnectionStatus != prevVal) {
161             notifyDevicesConnectionStatusChanged(mDevicesConnectionStatus);
162         }
163     }
164 
updateDevicesConnectionStatus()165     private void updateDevicesConnectionStatus() {
166         mInitialDevicesConnectionStatusUpdate = true;
167         // Add all hearing devices including sub and member into a set.
168         Set<CachedBluetoothDevice> allHearingDevices = mCachedDevices.stream()
169                 .filter(d -> d.getBondState() == BluetoothDevice.BOND_BONDED
170                         && d.isHearingDevice())
171                 .flatMap(d -> getAssociatedCachedDevice(d).stream())
172                 .collect(Collectors.toSet());
173 
174         // Status sequence matters here. If one of the hearing devices is in previous
175         // ConnectionStatus, we will treat whole hearing devices is in this status.
176         // E.g. One of hearing device is in CONNECTED status and another is in DISCONNECTED
177         // status, the hearing devices connection status will notify CONNECTED status.
178         if (isConnectingOrDisconnectingConnectionStatus(allHearingDevices)) {
179             mDevicesConnectionStatus = ConnectionStatus.CONNECTING_OR_DISCONNECTING;
180         } else if (isActiveConnectionStatus(allHearingDevices)) {
181             mDevicesConnectionStatus = ConnectionStatus.ACTIVE;
182         } else if (isConnectedStatus(allHearingDevices)) {
183             mDevicesConnectionStatus = ConnectionStatus.CONNECTED;
184         } else if (isDisconnectedStatus(allHearingDevices)) {
185             mDevicesConnectionStatus = ConnectionStatus.DISCONNECTED;
186         } else {
187             mDevicesConnectionStatus = ConnectionStatus.NO_DEVICE_BONDED;
188         }
189 
190         if (DEBUG) {
191             Log.d(TAG, "updateDevicesConnectionStatus: " + mDevicesConnectionStatus);
192         }
193     }
194 
195     /**
196      * @return all the related CachedBluetoothDevices for this device.
197      */
198     @NonNull
getAssociatedCachedDevice( @onNull CachedBluetoothDevice device)199     public Set<CachedBluetoothDevice> getAssociatedCachedDevice(
200             @NonNull CachedBluetoothDevice device) {
201         ArraySet<CachedBluetoothDevice> cachedDeviceSet = new ArraySet<>();
202         cachedDeviceSet.add(device);
203         // Associated device should be added into memberDevice if it support CSIP profile.
204         Set<CachedBluetoothDevice> memberDevices = device.getMemberDevice();
205         if (!memberDevices.isEmpty()) {
206             cachedDeviceSet.addAll(memberDevices);
207             return cachedDeviceSet;
208         }
209         // If not support CSIP profile, it should be ASHA hearing device and added into subDevice.
210         CachedBluetoothDevice subDevice = device.getSubDevice();
211         if (subDevice != null) {
212             cachedDeviceSet.add(subDevice);
213             return cachedDeviceSet;
214         }
215 
216         return cachedDeviceSet;
217     }
218 
isConnectingOrDisconnectingConnectionStatus( Set<CachedBluetoothDevice> devices)219     private boolean isConnectingOrDisconnectingConnectionStatus(
220             Set<CachedBluetoothDevice> devices) {
221         HearingAidProfile hearingAidProfile = mBtManager.getProfileManager().getHearingAidProfile();
222         HapClientProfile hapClientProfile = mBtManager.getProfileManager().getHapClientProfile();
223 
224         for (CachedBluetoothDevice device : devices) {
225             if (hearingAidProfile != null) {
226                 int status = device.getProfileConnectionState(hearingAidProfile);
227                 if (status == BluetoothProfile.STATE_DISCONNECTING
228                         || status == BluetoothProfile.STATE_CONNECTING) {
229                     return true;
230                 }
231             }
232             if (hapClientProfile != null) {
233                 int status = device.getProfileConnectionState(hapClientProfile);
234                 if (status == BluetoothProfile.STATE_DISCONNECTING
235                         || status == BluetoothProfile.STATE_CONNECTING) {
236                     return true;
237                 }
238             }
239         }
240         return false;
241     }
242 
isActiveConnectionStatus(Set<CachedBluetoothDevice> devices)243     private boolean isActiveConnectionStatus(Set<CachedBluetoothDevice> devices) {
244         for (CachedBluetoothDevice device : devices) {
245             if ((device.isActiveDevice(BluetoothProfile.HEARING_AID)
246                     && device.isConnectedProfile(BluetoothProfile.HEARING_AID))
247                     || (device.isActiveDevice(BluetoothProfile.LE_AUDIO)
248                     && device.isConnectedProfile(BluetoothProfile.LE_AUDIO))) {
249                 return true;
250             }
251         }
252         return false;
253     }
254 
isConnectedStatus(Set<CachedBluetoothDevice> devices)255     private boolean isConnectedStatus(Set<CachedBluetoothDevice> devices) {
256         return devices.stream().anyMatch(CachedBluetoothDevice::isConnected);
257     }
258 
isDisconnectedStatus(Set<CachedBluetoothDevice> devices)259     private boolean isDisconnectedStatus(Set<CachedBluetoothDevice> devices) {
260         return devices.stream().anyMatch(
261                 d -> (!d.isConnected() && d.getBondState() == BOND_BONDED));
262     }
263 
264     /**
265      * Gets the connection status for hearing device set. Will update connection status first if
266      * never updated.
267      */
268     @ConnectionStatus
getDevicesConnectionStatus()269     public int getDevicesConnectionStatus() {
270         if (!mInitialDevicesConnectionStatusUpdate) {
271             updateDevicesConnectionStatus();
272         }
273         return mDevicesConnectionStatus;
274     }
275 
initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice, List<ScanFilter> leScanFilters)276     void initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice,
277             List<ScanFilter> leScanFilters) {
278         HearingAidInfo info = generateHearingAidInfo(newDevice);
279         if (info != null) {
280             newDevice.setHearingAidInfo(info);
281         } else if (leScanFilters != null && !newDevice.isHearingAidDevice()) {
282             // If the device is added with hearing aid scan filter during pairing, set an empty
283             // hearing aid info to indicate it's a hearing aid device. The info will be updated
284             // when corresponding profiles connected.
285             for (ScanFilter leScanFilter: leScanFilters) {
286                 final ParcelUuid serviceUuid = leScanFilter.getServiceUuid();
287                 final ParcelUuid serviceDataUuid = leScanFilter.getServiceDataUuid();
288                 if (BluetoothUuid.HEARING_AID.equals(serviceUuid)
289                         || BluetoothUuid.HAS.equals(serviceUuid)
290                         || BluetoothUuid.HEARING_AID.equals(serviceDataUuid)
291                         || BluetoothUuid.HAS.equals(serviceDataUuid)) {
292                     newDevice.setHearingAidInfo(new HearingAidInfo.Builder().build());
293                     break;
294                 }
295             }
296         }
297     }
298 
setSubDeviceIfNeeded(CachedBluetoothDevice newDevice)299     boolean setSubDeviceIfNeeded(CachedBluetoothDevice newDevice) {
300         final long hiSyncId = newDevice.getHiSyncId();
301         if (isValidHiSyncId(hiSyncId)) {
302             // The remote device supports CSIP, the other ear should be processed as a member
303             // device. Ignore hiSyncId grouping from ASHA here.
304             if (newDevice.getProfiles().stream().anyMatch(
305                     profile -> profile instanceof CsipSetCoordinatorProfile)) {
306                 Log.w(TAG, "Skip ASHA grouping since this device supports CSIP");
307                 return false;
308             }
309 
310             final CachedBluetoothDevice hearingAidDevice = getCachedDevice(hiSyncId);
311             // Just add one of the hearing aids from a pair in the list that is shown in the UI.
312             // Once there is another device with the same hiSyncId, to add new device as sub
313             // device.
314             if (hearingAidDevice != null) {
315                 hearingAidDevice.setSubDevice(newDevice);
316                 newDevice.setName(hearingAidDevice.getName());
317                 return true;
318             }
319         }
320         return false;
321     }
322 
isValidHiSyncId(long hiSyncId)323     private boolean isValidHiSyncId(long hiSyncId) {
324         return hiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID;
325     }
326 
isValidGroupId(int groupId)327     private boolean isValidGroupId(int groupId) {
328         return groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
329     }
330 
getCachedDevice(long hiSyncId)331     private CachedBluetoothDevice getCachedDevice(long hiSyncId) {
332         for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
333             CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
334             if (cachedDevice.getHiSyncId() == hiSyncId) {
335                 return cachedDevice;
336             }
337         }
338         return null;
339     }
340 
341     // To collect all HearingAid devices and call #onHiSyncIdChanged to group device by HiSyncId
updateHearingAidsDevices()342     void updateHearingAidsDevices() {
343         final Set<Long> newSyncIdSet = new HashSet<>();
344         for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
345             // Do nothing if HiSyncId has been assigned
346             if (isValidHiSyncId(cachedDevice.getHiSyncId())) {
347                 continue;
348             }
349             HearingAidInfo info = generateHearingAidInfo(cachedDevice);
350             if (info != null) {
351                 cachedDevice.setHearingAidInfo(info);
352                 if (isValidHiSyncId(info.getHiSyncId())) {
353                     newSyncIdSet.add(info.getHiSyncId());
354                 }
355             }
356         }
357         for (Long syncId : newSyncIdSet) {
358             onHiSyncIdChanged(syncId);
359         }
360     }
361 
362     // Group devices by hiSyncId
363     @VisibleForTesting
onHiSyncIdChanged(long hiSyncId)364     void onHiSyncIdChanged(long hiSyncId) {
365         int firstMatchedIndex = -1;
366 
367         for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
368             CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
369             if (cachedDevice.getHiSyncId() != hiSyncId) {
370                 continue;
371             }
372 
373             // The remote device supports CSIP, the other ear should be processed as a member
374             // device. Ignore hiSyncId grouping from ASHA here.
375             if (cachedDevice.getProfiles().stream().anyMatch(
376                     profile -> profile instanceof CsipSetCoordinatorProfile)) {
377                 Log.w(TAG, "Skip ASHA grouping since this device supports CSIP");
378                 continue;
379             }
380 
381             if (firstMatchedIndex == -1) {
382                 // Found the first one
383                 firstMatchedIndex = i;
384                 continue;
385             }
386             // Found the second one
387             int indexToRemoveFromUi;
388             CachedBluetoothDevice subDevice;
389             CachedBluetoothDevice mainDevice;
390             // Since the hiSyncIds have been updated for a connected pair of hearing aids,
391             // we remove the entry of one the hearing aids from the UI. Unless the
392             // hiSyncId get updated, the system does not know it is a hearing aid, so we add
393             // both the hearing aids as separate entries in the UI first, then remove one
394             // of them after the hiSyncId is populated. We will choose the device that
395             // is not connected to be removed.
396             if (cachedDevice.isConnected()) {
397                 mainDevice = cachedDevice;
398                 indexToRemoveFromUi = firstMatchedIndex;
399                 subDevice = mCachedDevices.get(firstMatchedIndex);
400             } else {
401                 mainDevice = mCachedDevices.get(firstMatchedIndex);
402                 indexToRemoveFromUi = i;
403                 subDevice = cachedDevice;
404             }
405 
406             mainDevice.setSubDevice(subDevice);
407             mCachedDevices.remove(indexToRemoveFromUi);
408             log("onHiSyncIdChanged: removed from UI device =" + subDevice
409                     + ", with hiSyncId=" + hiSyncId);
410             mBtManager.getEventManager().dispatchDeviceRemoved(subDevice);
411             break;
412         }
413     }
414 
415     // @return {@code true}, the event is processed inside the method. It is for updating
416     // hearing aid device on main-sub relationship when receiving connected or disconnected.
417     // @return {@code false}, it is not hearing aid device or to process it same as other profiles
onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice, int state)418     boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice,
419             int state) {
420         switch (state) {
421             case BluetoothProfile.STATE_CONNECTED:
422                 onHiSyncIdChanged(cachedDevice.getHiSyncId());
423                 CachedBluetoothDevice mainDevice = findMainDevice(cachedDevice);
424                 if (mainDevice != null) {
425                     if (mainDevice.isConnected()) {
426                         // Sub/member device is connected and main device is connected
427                         // To refresh main device UI
428                         mainDevice.refresh();
429                     } else {
430                         // Sub/member device is connected and main device is disconnected
431                         // To switch content and dispatch to notify UI change
432                         switchDeviceContent(mainDevice, cachedDevice);
433                     }
434                     return true;
435                 }
436                 break;
437             case BluetoothProfile.STATE_DISCONNECTED:
438                 if (cachedDevice.getUnpairing()) {
439                     return true;
440                 }
441                 mainDevice = findMainDevice(cachedDevice);
442                 if (mainDevice != null) {
443                     // Sub/member device is disconnected and main device exists
444                     // To update main device UI
445                     mainDevice.refresh();
446                     return true;
447                 }
448                 CachedBluetoothDevice connectedSecondaryDevice = getConnectedSecondaryDevice(
449                         cachedDevice);
450                 if (connectedSecondaryDevice != null) {
451                     // Main device is disconnected and sub/member device is connected
452                     // To switch content and dispatch to notify UI change
453                     switchDeviceContent(cachedDevice, connectedSecondaryDevice);
454                     return true;
455                 }
456                 break;
457         }
458         return false;
459     }
460 
switchDeviceContent(CachedBluetoothDevice mainDevice, CachedBluetoothDevice secondaryDevice)461     private void switchDeviceContent(CachedBluetoothDevice mainDevice,
462             CachedBluetoothDevice secondaryDevice) {
463         mBtManager.getEventManager().dispatchDeviceRemoved(mainDevice);
464         if (mainDevice.getSubDevice() != null
465                 && mainDevice.getSubDevice().equals(secondaryDevice)) {
466             mainDevice.switchSubDeviceContent();
467         } else {
468             mainDevice.switchMemberDeviceContent(secondaryDevice);
469         }
470         mainDevice.refresh();
471         // It is necessary to do remove and add for updating the mapping on
472         // preference and device
473         mBtManager.getEventManager().dispatchDeviceAdded(mainDevice);
474     }
475 
getConnectedSecondaryDevice(CachedBluetoothDevice cachedDevice)476     private CachedBluetoothDevice getConnectedSecondaryDevice(CachedBluetoothDevice cachedDevice) {
477         if (cachedDevice.getSubDevice() != null && cachedDevice.getSubDevice().isConnected()) {
478             return cachedDevice.getSubDevice();
479         }
480         return cachedDevice.getMemberDevice().stream().filter(
481                 CachedBluetoothDevice::isConnected).findAny().orElse(null);
482     }
483 
onActiveDeviceChanged(CachedBluetoothDevice device)484     void onActiveDeviceChanged(CachedBluetoothDevice device) {
485         if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_AUDIO_ROUTING)) {
486             if (device.isConnectedHearingAidDevice()
487                     && (device.isActiveDevice(BluetoothProfile.HEARING_AID)
488                     || device.isActiveDevice(BluetoothProfile.LE_AUDIO))) {
489                 setAudioRoutingConfig(device);
490             } else {
491                 clearAudioRoutingConfig();
492             }
493         }
494         if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) {
495             if (device.isConnectedHearingAidDevice()
496                     && (device.isActiveDevice(BluetoothProfile.HEARING_AID)
497                     || device.isActiveDevice(BluetoothProfile.LE_AUDIO))) {
498                 setMicrophoneForCalls(device);
499             } else {
500                 clearMicrophoneForCalls();
501             }
502         }
503     }
504 
syncDeviceIfNeeded(CachedBluetoothDevice device)505     void syncDeviceIfNeeded(CachedBluetoothDevice device) {
506         final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
507         final HapClientProfile hap = profileManager.getHapClientProfile();
508         // Sync preset if device doesn't support synchronization on the remote side
509         if (hap != null && !hap.supportsSynchronizedPresets(device.getDevice())) {
510             final CachedBluetoothDevice mainDevice = findMainDevice(device);
511             if (mainDevice != null) {
512                 int mainPresetIndex = hap.getActivePresetIndex(mainDevice.getDevice());
513                 int presetIndex = hap.getActivePresetIndex(device.getDevice());
514                 if (mainPresetIndex != BluetoothHapClient.PRESET_INDEX_UNAVAILABLE
515                         && mainPresetIndex != presetIndex) {
516                     if (DEBUG) {
517                         Log.d(TAG, "syncing preset from " + presetIndex + "->"
518                                 + mainPresetIndex + ", device=" + device);
519                     }
520                     hap.selectPreset(device.getDevice(), mainPresetIndex);
521                 }
522             }
523         }
524     }
525 
clearLocalDataIfNeeded(CachedBluetoothDevice device)526     void clearLocalDataIfNeeded(CachedBluetoothDevice device) {
527         HearingDeviceLocalDataManager.clear(mContext, device.getDevice());
528     }
529 
setMicrophoneForCalls(CachedBluetoothDevice device)530     private void setMicrophoneForCalls(CachedBluetoothDevice device) {
531         boolean useRemoteMicrophone = device.getDevice().isMicrophonePreferredForCalls();
532         boolean status = mRoutingHelper.setPreferredInputDeviceForCalls(device,
533                 useRemoteMicrophone ? RoutingValue.AUTO : RoutingValue.BUILTIN_DEVICE);
534         if (!status) {
535             Log.d(TAG, "Fail to configure setPreferredInputDeviceForCalls");
536         }
537     }
538 
clearMicrophoneForCalls()539     private void clearMicrophoneForCalls() {
540         boolean status = mRoutingHelper.clearPreferredInputDeviceForCalls();
541         if (!status) {
542             Log.d(TAG, "Fail to configure clearMicrophoneForCalls");
543         }
544     }
545 
setAudioRoutingConfig(CachedBluetoothDevice device)546     private void setAudioRoutingConfig(CachedBluetoothDevice device) {
547         AudioDeviceAttributes hearingDeviceAttributes =
548                 mRoutingHelper.getMatchedHearingDeviceAttributesForOutput(device);
549         if (hearingDeviceAttributes == null) {
550             Log.w(TAG, "Can not find expected AudioDeviceAttributes for hearing device: "
551                     + device.getDevice().getAnonymizedAddress());
552             return;
553         }
554 
555         final int callRoutingValue = Settings.Secure.getInt(mContentResolver,
556                 Settings.Secure.HEARING_AID_CALL_ROUTING, RoutingValue.AUTO);
557         final int mediaRoutingValue = Settings.Secure.getInt(mContentResolver,
558                 Settings.Secure.HEARING_AID_MEDIA_ROUTING, RoutingValue.AUTO);
559         final int ringtoneRoutingValue = Settings.Secure.getInt(mContentResolver,
560                 Settings.Secure.HEARING_AID_RINGTONE_ROUTING, RoutingValue.AUTO);
561         final int systemSoundsRoutingValue = Settings.Secure.getInt(mContentResolver,
562                 Settings.Secure.HEARING_AID_NOTIFICATION_ROUTING, RoutingValue.AUTO);
563 
564         setPreferredDeviceRoutingStrategies(
565                 HearingAidAudioRoutingConstants.CALL_ROUTING_ATTRIBUTES,
566                 hearingDeviceAttributes, callRoutingValue);
567         setPreferredDeviceRoutingStrategies(
568                 HearingAidAudioRoutingConstants.MEDIA_ROUTING_ATTRIBUTES,
569                 hearingDeviceAttributes, mediaRoutingValue);
570         setPreferredDeviceRoutingStrategies(
571                 HearingAidAudioRoutingConstants.RINGTONE_ROUTING_ATTRIBUTES,
572                 hearingDeviceAttributes, ringtoneRoutingValue);
573         setPreferredDeviceRoutingStrategies(
574                 HearingAidAudioRoutingConstants.NOTIFICATION_ROUTING_ATTRIBUTES,
575                 hearingDeviceAttributes, systemSoundsRoutingValue);
576     }
577 
clearAudioRoutingConfig()578     private void clearAudioRoutingConfig() {
579         // Don't need to pass hearingDevice when we want to reset it (set to AUTO).
580         setPreferredDeviceRoutingStrategies(
581                 HearingAidAudioRoutingConstants.CALL_ROUTING_ATTRIBUTES,
582                 /* hearingDevice = */ null, RoutingValue.AUTO);
583         setPreferredDeviceRoutingStrategies(
584                 HearingAidAudioRoutingConstants.MEDIA_ROUTING_ATTRIBUTES,
585                 /* hearingDevice = */ null, RoutingValue.AUTO);
586         setPreferredDeviceRoutingStrategies(
587                 HearingAidAudioRoutingConstants.RINGTONE_ROUTING_ATTRIBUTES,
588                 /* hearingDevice = */ null, RoutingValue.AUTO);
589         setPreferredDeviceRoutingStrategies(
590                 HearingAidAudioRoutingConstants.NOTIFICATION_ROUTING_ATTRIBUTES,
591                 /* hearingDevice = */ null, RoutingValue.AUTO);
592     }
593 
setPreferredDeviceRoutingStrategies(int[] attributeSdkUsageList, AudioDeviceAttributes hearingDevice, @RoutingValue int routingValue)594     private void setPreferredDeviceRoutingStrategies(int[] attributeSdkUsageList,
595             AudioDeviceAttributes hearingDevice,
596             @RoutingValue int routingValue) {
597         final List<AudioProductStrategy> supportedStrategies =
598                 mRoutingHelper.getSupportedStrategies(attributeSdkUsageList);
599 
600         final boolean status = mRoutingHelper.setPreferredDeviceRoutingStrategies(
601                 supportedStrategies, hearingDevice, routingValue);
602 
603         if (!status) {
604             Log.w(TAG, "routingStrategies: " + supportedStrategies.toString() + "routingValue: "
605                     + routingValue + " fail to configure AudioProductStrategy");
606         }
607     }
608 
findMainDevice(CachedBluetoothDevice device)609     CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) {
610         if (device == null || mCachedDevices == null) {
611             return null;
612         }
613 
614         for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
615             if (isValidGroupId(cachedDevice.getGroupId())) {
616                 Set<CachedBluetoothDevice> memberSet = cachedDevice.getMemberDevice();
617                 for (CachedBluetoothDevice memberDevice : memberSet) {
618                     if (memberDevice != null && memberDevice.equals(device)) {
619                         return cachedDevice;
620                     }
621                 }
622             }
623             if (isValidHiSyncId(cachedDevice.getHiSyncId())) {
624                 CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
625                 if (subDevice != null && subDevice.equals(device)) {
626                     return cachedDevice;
627                 }
628             }
629         }
630         return null;
631     }
632 
generateHearingAidInfo(CachedBluetoothDevice cachedDevice)633     private HearingAidInfo generateHearingAidInfo(CachedBluetoothDevice cachedDevice) {
634         final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
635 
636         final HearingAidProfile asha = profileManager.getHearingAidProfile();
637         if (asha == null) {
638             Log.w(TAG, "HearingAidProfile is not supported on this device");
639         } else {
640             long hiSyncId = asha.getHiSyncId(cachedDevice.getDevice());
641             if (isValidHiSyncId(hiSyncId)) {
642                 final HearingAidInfo info = new HearingAidInfo.Builder()
643                         .setAshaDeviceSide(asha.getDeviceSide(cachedDevice.getDevice()))
644                         .setAshaDeviceMode(asha.getDeviceMode(cachedDevice.getDevice()))
645                         .setHiSyncId(hiSyncId)
646                         .build();
647                 if (DEBUG) {
648                     Log.d(TAG, "generateHearingAidInfo, " + cachedDevice + ", info=" + info);
649                 }
650                 return info;
651             }
652         }
653 
654         final HapClientProfile hapClientProfile = profileManager.getHapClientProfile();
655         final LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile();
656         if (hapClientProfile == null || leAudioProfile == null) {
657             Log.w(TAG, "HapClientProfile or LeAudioProfile is not supported on this device");
658         } else if (cachedDevice.getProfiles().stream().anyMatch(
659                 p -> p instanceof HapClientProfile)) {
660             int audioLocation = leAudioProfile.getAudioLocation(cachedDevice.getDevice());
661             int hearingAidType = hapClientProfile.getHearingAidType(cachedDevice.getDevice());
662             if (hearingAidType != HapClientProfile.HearingAidType.TYPE_INVALID) {
663                 final HearingAidInfo info = new HearingAidInfo.Builder()
664                         .setLeAudioLocation(audioLocation)
665                         .setHapDeviceType(hearingAidType)
666                         .build();
667                 if (DEBUG) {
668                     Log.d(TAG, "generateHearingAidInfo, " + cachedDevice + ", info=" + info);
669                 }
670                 return info;
671             }
672         }
673 
674         return null;
675     }
676 
log(String msg)677     private void log(String msg) {
678         if (DEBUG) {
679             Log.d(TAG, msg);
680         }
681     }
682 }