1 /*
2  * Copyright (C) 2023 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 
17 package com.android.settingslib.bluetooth;
18 
19 import android.content.Context;
20 import android.media.AudioAttributes;
21 import android.media.AudioDeviceAttributes;
22 import android.media.AudioDeviceInfo;
23 import android.media.AudioManager;
24 import android.media.audiopolicy.AudioProductStrategy;
25 
26 import androidx.annotation.Nullable;
27 import androidx.annotation.VisibleForTesting;
28 
29 import java.util.ArrayList;
30 import java.util.List;
31 import java.util.Set;
32 import java.util.stream.Collectors;
33 
34 /**
35  * A helper class to configure the routing strategy for hearing aids.
36  */
37 public class HearingAidAudioRoutingHelper {
38 
39     private final AudioManager mAudioManager;
40 
HearingAidAudioRoutingHelper(Context context)41     public HearingAidAudioRoutingHelper(Context context) {
42         mAudioManager = context.getSystemService(AudioManager.class);
43     }
44 
45     /**
46      * Gets the list of {@link AudioProductStrategy} referred by the given list of usage values
47      * defined in {@link AudioAttributes}
48      */
getSupportedStrategies(int[] attributeSdkUsageList)49     public List<AudioProductStrategy> getSupportedStrategies(int[] attributeSdkUsageList) {
50         final List<AudioAttributes> audioAttrList = new ArrayList<>(attributeSdkUsageList.length);
51         for (int attributeSdkUsage : attributeSdkUsageList) {
52             audioAttrList.add(new AudioAttributes.Builder().setUsage(attributeSdkUsage).build());
53         }
54 
55         final List<AudioProductStrategy> allStrategies = getAudioProductStrategies();
56         final List<AudioProductStrategy> supportedStrategies = new ArrayList<>();
57         for (AudioProductStrategy strategy : allStrategies) {
58             for (AudioAttributes audioAttr : audioAttrList) {
59                 if (strategy.supportsAudioAttributes(audioAttr)) {
60                     supportedStrategies.add(strategy);
61                 }
62             }
63         }
64 
65         return supportedStrategies.stream().distinct().collect(Collectors.toList());
66     }
67 
68     /**
69      * Sets the preferred device for the given strategies.
70      *
71      * @param supportedStrategies A list of {@link AudioProductStrategy} used to configure audio
72      *                            routing
73      * @param hearingDevice {@link AudioDeviceAttributes} of the device to be changed in audio
74      *                      routing
75      * @param routingValue one of value defined in
76      *                     {@link HearingAidAudioRoutingConstants.RoutingValue}, denotes routing
77      *                     destination.
78      * @return {code true} if the routing value successfully configure
79      */
setPreferredDeviceRoutingStrategies( List<AudioProductStrategy> supportedStrategies, AudioDeviceAttributes hearingDevice, @HearingAidAudioRoutingConstants.RoutingValue int routingValue)80     public boolean setPreferredDeviceRoutingStrategies(
81             List<AudioProductStrategy> supportedStrategies, AudioDeviceAttributes hearingDevice,
82             @HearingAidAudioRoutingConstants.RoutingValue int routingValue) {
83         boolean status;
84         switch (routingValue) {
85             case HearingAidAudioRoutingConstants.RoutingValue.AUTO:
86                 status = removePreferredDeviceForStrategies(supportedStrategies);
87                 return status;
88             case HearingAidAudioRoutingConstants.RoutingValue.HEARING_DEVICE:
89                 status = removePreferredDeviceForStrategies(supportedStrategies);
90                 status &= setPreferredDeviceForStrategies(supportedStrategies, hearingDevice);
91                 return status;
92             case HearingAidAudioRoutingConstants.RoutingValue.DEVICE_SPEAKER:
93                 status = removePreferredDeviceForStrategies(supportedStrategies);
94                 status &= setPreferredDeviceForStrategies(supportedStrategies,
95                         HearingAidAudioRoutingConstants.DEVICE_SPEAKER_OUT);
96                 return status;
97             default:
98                 throw new IllegalArgumentException("Unexpected routingValue: " + routingValue);
99         }
100     }
101 
102     /**
103      * Gets the matched hearing device {@link AudioDeviceAttributes} for {@code device}.
104      *
105      * <p>Will also try to match the {@link CachedBluetoothDevice#getSubDevice()} of {@code device}
106      *
107      * @param device the {@link CachedBluetoothDevice} need to be hearing aid device
108      * @return the requested AudioDeviceAttributes or {@code null} if not match
109      */
110     @Nullable
getMatchedHearingDeviceAttributes(CachedBluetoothDevice device)111     public AudioDeviceAttributes getMatchedHearingDeviceAttributes(CachedBluetoothDevice device) {
112         if (device == null || !device.isHearingAidDevice()) {
113             return null;
114         }
115 
116         AudioDeviceInfo[] audioDevices = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
117         for (AudioDeviceInfo audioDevice : audioDevices) {
118             // ASHA for TYPE_HEARING_AID, HAP for TYPE_BLE_HEADSET
119             if (audioDevice.getType() == AudioDeviceInfo.TYPE_HEARING_AID
120                     || audioDevice.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET) {
121                 if (matchAddress(device, audioDevice)) {
122                     return new AudioDeviceAttributes(audioDevice);
123                 }
124             }
125         }
126         return null;
127     }
128 
matchAddress(CachedBluetoothDevice device, AudioDeviceInfo audioDevice)129     private boolean matchAddress(CachedBluetoothDevice device, AudioDeviceInfo audioDevice) {
130         final String audioDeviceAddress = audioDevice.getAddress();
131         final CachedBluetoothDevice subDevice = device.getSubDevice();
132         final Set<CachedBluetoothDevice> memberDevices = device.getMemberDevice();
133 
134         return device.getAddress().equals(audioDeviceAddress)
135                 || (subDevice != null && subDevice.getAddress().equals(audioDeviceAddress))
136                 || (!memberDevices.isEmpty() && memberDevices.stream().anyMatch(
137                     m -> m.getAddress().equals(audioDeviceAddress)));
138     }
139 
setPreferredDeviceForStrategies(List<AudioProductStrategy> strategies, AudioDeviceAttributes audioDevice)140     private boolean setPreferredDeviceForStrategies(List<AudioProductStrategy> strategies,
141             AudioDeviceAttributes audioDevice) {
142         boolean status = true;
143         for (AudioProductStrategy strategy : strategies) {
144             status &= mAudioManager.setPreferredDeviceForStrategy(strategy, audioDevice);
145 
146         }
147 
148         return status;
149     }
150 
removePreferredDeviceForStrategies(List<AudioProductStrategy> strategies)151     private boolean removePreferredDeviceForStrategies(List<AudioProductStrategy> strategies) {
152         boolean status = true;
153         for (AudioProductStrategy strategy : strategies) {
154             if (mAudioManager.getPreferredDeviceForStrategy(strategy) != null) {
155                 status &= mAudioManager.removePreferredDeviceForStrategy(strategy);
156             }
157         }
158 
159         return status;
160     }
161 
162     @VisibleForTesting
getAudioProductStrategies()163     public List<AudioProductStrategy> getAudioProductStrategies() {
164         return AudioManager.getAudioProductStrategies();
165     }
166 }
167