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 }