• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.settings.connecteddevice.audiosharing;
18 
19 import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.MetricKey.METRIC_KEY_CANDIDATE_DEVICE_COUNT;
20 import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_COUNT_IN_SHARING;
21 import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.MetricKey.METRIC_KEY_PAGE_ID;
22 import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID;
23 import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.MetricKey.METRIC_KEY_USER_TRIGGERED;
24 import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.BLUETOOTH_LE_BROADCAST_PRIMARY_DEVICE_GROUP_ID;
25 
26 import static java.util.stream.Collectors.toList;
27 
28 import android.bluetooth.BluetoothCsipSetCoordinator;
29 import android.bluetooth.BluetoothDevice;
30 import android.bluetooth.BluetoothLeBroadcastMetadata;
31 import android.content.Context;
32 import android.provider.Settings;
33 import android.util.Log;
34 import android.util.Pair;
35 import android.widget.Toast;
36 
37 import androidx.annotation.NonNull;
38 import androidx.annotation.Nullable;
39 
40 import com.android.settingslib.bluetooth.BluetoothUtils;
41 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
42 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
43 import com.android.settingslib.bluetooth.LeAudioProfile;
44 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
45 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
46 import com.android.settingslib.bluetooth.LocalBluetoothManager;
47 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
48 import com.android.settingslib.bluetooth.VolumeControlProfile;
49 
50 import java.util.ArrayList;
51 import java.util.Comparator;
52 import java.util.HashMap;
53 import java.util.List;
54 import java.util.Map;
55 import java.util.Objects;
56 
57 public class AudioSharingUtils {
58     private static final String TAG = "AudioSharingUtils";
59     private static final boolean DEBUG = BluetoothUtils.D;
60 
61     public enum MetricKey {
62         METRIC_KEY_SOURCE_PAGE_ID,
63         METRIC_KEY_PAGE_ID,
64         METRIC_KEY_USER_TRIGGERED,
65         METRIC_KEY_DEVICE_COUNT_IN_SHARING,
66         METRIC_KEY_CANDIDATE_DEVICE_COUNT
67     }
68 
69     /**
70      * Fetch {@link BluetoothDevice}s connected to the broadcast assistant. The devices are grouped
71      * by CSIP group id.
72      *
73      * @param localBtManager The BT manager to provide BT functions.
74      * @return A map of connected devices grouped by CSIP group id.
75      */
fetchConnectedDevicesByGroupId( @ullable LocalBluetoothManager localBtManager)76     public static Map<Integer, List<BluetoothDevice>> fetchConnectedDevicesByGroupId(
77             @Nullable LocalBluetoothManager localBtManager) {
78         Map<Integer, List<BluetoothDevice>> groupedDevices = new HashMap<>();
79         if (localBtManager == null) {
80             Log.d(TAG, "Skip fetchConnectedDevicesByGroupId due to bt manager is null");
81             return groupedDevices;
82         }
83         LocalBluetoothLeBroadcastAssistant assistant =
84                 localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
85         if (assistant == null) {
86             Log.d(TAG, "Skip fetchConnectedDevicesByGroupId due to assistant profile is null");
87             return groupedDevices;
88         }
89         List<BluetoothDevice> connectedDevices = assistant.getAllConnectedDevices();
90         CachedBluetoothDeviceManager cacheManager = localBtManager.getCachedDeviceManager();
91         for (BluetoothDevice device : connectedDevices) {
92             CachedBluetoothDevice cachedDevice = cacheManager.findDevice(device);
93             if (cachedDevice == null) {
94                 Log.d(TAG, "Skip device due to not being cached: " + device.getAnonymizedAddress());
95                 continue;
96             }
97             int groupId = BluetoothUtils.getGroupId(cachedDevice);
98             if (groupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
99                 Log.d(
100                         TAG,
101                         "Skip device due to no valid group id: " + device.getAnonymizedAddress());
102                 continue;
103             }
104             if (!groupedDevices.containsKey(groupId)) {
105                 groupedDevices.put(groupId, new ArrayList<>());
106             }
107             groupedDevices.get(groupId).add(device);
108         }
109         if (DEBUG) {
110             Log.d(TAG, "fetchConnectedDevicesByGroupId: " + groupedDevices);
111         }
112         return groupedDevices;
113     }
114 
115     /**
116      * Fetch a list of ordered connected lead {@link CachedBluetoothDevice}s eligible for audio
117      * sharing. The active device is placed in the first place if it exists. The devices can be
118      * filtered by whether it is already in the audio sharing session.
119      *
120      * @param localBtManager The BT manager to provide BT functions.
121      * @param groupedConnectedDevices devices connected to broadcast assistant grouped by CSIP group
122      *     id.
123      * @param filterByInSharing Whether to filter the device by if is already in the sharing
124      *     session.
125      * @return A list of ordered connected devices eligible for the audio sharing. The active device
126      *     is placed in the first place if it exists.
127      */
buildOrderedConnectedLeadDevices( @ullable LocalBluetoothManager localBtManager, Map<Integer, List<BluetoothDevice>> groupedConnectedDevices, boolean filterByInSharing)128     public static List<CachedBluetoothDevice> buildOrderedConnectedLeadDevices(
129             @Nullable LocalBluetoothManager localBtManager,
130             Map<Integer, List<BluetoothDevice>> groupedConnectedDevices,
131             boolean filterByInSharing) {
132         List<CachedBluetoothDevice> orderedDevices = new ArrayList<>();
133         if (localBtManager == null) {
134             Log.d(TAG, "Skip buildOrderedConnectedLeadDevices due to bt manager is null");
135             return orderedDevices;
136         }
137         CachedBluetoothDeviceManager deviceManager = localBtManager.getCachedDeviceManager();
138         for (List<BluetoothDevice> devices : groupedConnectedDevices.values()) {
139             CachedBluetoothDevice leadDevice = getLeadDevice(deviceManager, devices);
140             if (leadDevice == null) {
141                 Log.d(TAG, "Skip due to no lead device");
142                 continue;
143             }
144             if (filterByInSharing
145                     && !BluetoothUtils.hasConnectedBroadcastSource(leadDevice, localBtManager)) {
146                 Log.d(
147                         TAG,
148                         "Filtered the device due to not in sharing session: "
149                                 + leadDevice.getDevice().getAnonymizedAddress());
150                 continue;
151             }
152             orderedDevices.add(leadDevice);
153         }
154         orderedDevices.sort(sCachedDeviceComparator);
155         return orderedDevices;
156     }
157 
158     /**
159      * Get the lead device from a list of devices with same group id.
160      *
161      * @param deviceManager CachedBluetoothDeviceManager
162      * @param devices A list of devices with same group id.
163      * @return The lead device
164      */
165     @Nullable
getLeadDevice( @ullable CachedBluetoothDeviceManager deviceManager, @NonNull List<BluetoothDevice> devices)166     public static CachedBluetoothDevice getLeadDevice(
167             @Nullable CachedBluetoothDeviceManager deviceManager,
168             @NonNull List<BluetoothDevice> devices) {
169         if (deviceManager == null || devices.isEmpty()) return null;
170         List<CachedBluetoothDevice> cachedDevices =
171                 devices.stream()
172                         .map(device -> deviceManager.findDevice(device))
173                         .filter(Objects::nonNull)
174                         .collect(toList());
175         for (CachedBluetoothDevice cachedDevice : cachedDevices) {
176             if (!cachedDevice.getMemberDevice().isEmpty()) {
177                 return cachedDevice;
178             }
179         }
180         CachedBluetoothDevice leadDevice = cachedDevices.isEmpty() ? null : cachedDevices.get(0);
181         Log.d(
182                 TAG,
183                 "No lead device in the group, pick arbitrary device as the lead: "
184                         + (leadDevice == null
185                                 ? "null"
186                                 : leadDevice.getDevice().getAnonymizedAddress()));
187         return leadDevice;
188     }
189 
190     /**
191      * Fetch a list of ordered connected lead {@link AudioSharingDeviceItem}s eligible for audio
192      * sharing. The active device is placed in the first place if it exists. The devices can be
193      * filtered by whether it is already in the audio sharing session.
194      *
195      * @param localBtManager The BT manager to provide BT functions.
196      * @param groupedConnectedDevices devices connected to broadcast assistant grouped by CSIP group
197      *     id.
198      * @param filterByInSharing Whether to filter the device by if is already in the sharing
199      *     session.
200      * @return A list of ordered connected devices eligible for the audio sharing. The active device
201      *     is placed in the first place if it exists.
202      */
203     @NonNull
buildOrderedConnectedLeadAudioSharingDeviceItem( @ullable LocalBluetoothManager localBtManager, Map<Integer, List<BluetoothDevice>> groupedConnectedDevices, boolean filterByInSharing)204     public static List<AudioSharingDeviceItem> buildOrderedConnectedLeadAudioSharingDeviceItem(
205             @Nullable LocalBluetoothManager localBtManager,
206             Map<Integer, List<BluetoothDevice>> groupedConnectedDevices,
207             boolean filterByInSharing) {
208         return buildOrderedConnectedLeadDevices(
209                         localBtManager, groupedConnectedDevices, filterByInSharing)
210                 .stream()
211                 .map(AudioSharingUtils::buildAudioSharingDeviceItem)
212                 .collect(toList());
213     }
214 
215     /** Return if there exists active connected lead device. */
hasActiveConnectedLeadDevice( @ullable LocalBluetoothManager localBtManager)216     public static boolean hasActiveConnectedLeadDevice(
217             @Nullable LocalBluetoothManager localBtManager) {
218         CachedBluetoothDeviceManager deviceManager =
219                 localBtManager == null ? null : localBtManager.getCachedDeviceManager();
220         if (deviceManager == null) {
221             Log.d(TAG, "hasActiveConnectedLeadDevice return false due to null device manager.");
222             return false;
223         }
224         return deviceManager.getCachedDevicesCopy().stream()
225                 .anyMatch(BluetoothUtils::isActiveMediaDevice);
226     }
227 
228     /** Build {@link AudioSharingDeviceItem} from {@link CachedBluetoothDevice}. */
buildAudioSharingDeviceItem( CachedBluetoothDevice cachedDevice)229     public static AudioSharingDeviceItem buildAudioSharingDeviceItem(
230             CachedBluetoothDevice cachedDevice) {
231         return new AudioSharingDeviceItem(
232                 cachedDevice.getName(),
233                 BluetoothUtils.getGroupId(cachedDevice),
234                 isActiveLeAudioDevice(cachedDevice));
235     }
236 
237     /**
238      * Check if {@link CachedBluetoothDevice} is an active le audio device.
239      *
240      * @param cachedDevice The cached bluetooth device to check.
241      * @return Whether the device is an active le audio device.
242      */
isActiveLeAudioDevice(@ullable CachedBluetoothDevice cachedDevice)243     public static boolean isActiveLeAudioDevice(@Nullable CachedBluetoothDevice cachedDevice) {
244         return cachedDevice != null && BluetoothUtils.isActiveLeAudioDevice(cachedDevice);
245     }
246 
247     /** Toast message on main thread. */
toastMessage(Context context, String message)248     public static void toastMessage(Context context, String message) {
249         context.getMainExecutor()
250                 .execute(() -> Toast.makeText(context, message, Toast.LENGTH_LONG).show());
251     }
252 
253     /** Add source to target sinks. */
addSourceToTargetSinks( List<BluetoothDevice> sinks, @Nullable LocalBluetoothManager localBtManager)254     public static void addSourceToTargetSinks(
255             List<BluetoothDevice> sinks, @Nullable LocalBluetoothManager localBtManager) {
256         if (localBtManager == null) {
257             Log.d(TAG, "skip addSourceToTargetDevices: LocalBluetoothManager is null!");
258             return;
259         }
260         if (sinks.isEmpty()) {
261             Log.d(TAG, "Skip addSourceToTargetDevices. No sinks.");
262             return;
263         }
264         LocalBluetoothLeBroadcast broadcast =
265                 localBtManager.getProfileManager().getLeAudioBroadcastProfile();
266         if (broadcast == null) {
267             Log.d(TAG, "skip addSourceToTargetDevices. Broadcast profile is null.");
268             return;
269         }
270         LocalBluetoothLeBroadcastAssistant assistant =
271                 localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
272         if (assistant == null) {
273             Log.d(TAG, "skip addSourceToTargetDevices. Assistant profile is null.");
274             return;
275         }
276         BluetoothLeBroadcastMetadata broadcastMetadata =
277                 broadcast.getLatestBluetoothLeBroadcastMetadata();
278         if (broadcastMetadata == null) {
279             Log.d(TAG, "skip addSourceToTargetDevices: There is no broadcastMetadata.");
280             return;
281         }
282         List<BluetoothDevice> connectedDevices = assistant.getAllConnectedDevices();
283         for (BluetoothDevice sink : sinks) {
284             if (connectedDevices.contains(sink)) {
285                 Log.d(
286                         TAG,
287                         "Add broadcast with broadcastId: "
288                                 + broadcastMetadata.getBroadcastId()
289                                 + " to the device: "
290                                 + sink.getAnonymizedAddress());
291                 assistant.addSource(sink, broadcastMetadata, /* isGroupOp= */ false);
292             } else {
293                 Log.d(
294                         TAG,
295                         "Skip add broadcast with broadcastId: "
296                                 + broadcastMetadata.getBroadcastId()
297                                 + " to the not connected device: "
298                                 + sink.getAnonymizedAddress());
299             }
300         }
301     }
302 
303     /** Stops the latest broadcast. */
stopBroadcasting(@ullable LocalBluetoothManager manager)304     public static void stopBroadcasting(@Nullable LocalBluetoothManager manager) {
305         if (manager == null) {
306             Log.d(TAG, "Skip stop broadcasting due to bt manager is null");
307             return;
308         }
309         LocalBluetoothLeBroadcast broadcast =
310                 manager.getProfileManager().getLeAudioBroadcastProfile();
311         if (broadcast == null) {
312             Log.d(TAG, "Skip stop broadcasting due to broadcast profile is null");
313         } else {
314             broadcast.stopBroadcast(broadcast.getLatestBroadcastId());
315         }
316     }
317 
318     /** Post the runnable to main thread. */
postOnMainThread(@onNull Context context, @NonNull Runnable runnable)319     public static void postOnMainThread(@NonNull Context context, @NonNull Runnable runnable) {
320         context.getMainExecutor().execute(runnable);
321     }
322 
323     /** Check if the {@link CachedBluetoothDevice} supports LE Audio profile */
isLeAudioSupported(CachedBluetoothDevice cachedDevice)324     public static boolean isLeAudioSupported(CachedBluetoothDevice cachedDevice) {
325         return cachedDevice.getProfiles().stream()
326                 .anyMatch(
327                         profile ->
328                                 profile instanceof LeAudioProfile
329                                         && profile.isEnabled(cachedDevice.getDevice()));
330     }
331 
332     /** Check if the LE Audio related profiles ready */
isAudioSharingProfileReady( @ullable LocalBluetoothProfileManager profileManager)333     public static boolean isAudioSharingProfileReady(
334             @Nullable LocalBluetoothProfileManager profileManager) {
335         if (profileManager == null) return false;
336         LocalBluetoothLeBroadcast broadcast = profileManager.getLeAudioBroadcastProfile();
337         if (broadcast == null || !broadcast.isProfileReady()) {
338             return false;
339         }
340         LocalBluetoothLeBroadcastAssistant assistant =
341                 profileManager.getLeAudioBroadcastAssistantProfile();
342         if (assistant == null || !assistant.isProfileReady()) {
343             return false;
344         }
345         VolumeControlProfile vc = profileManager.getVolumeControlProfile();
346         return vc != null && vc.isProfileReady();
347     }
348 
349     /** Set {@link CachedBluetoothDevice} as user preferred primary device for call audio */
setUserPreferredPrimary( @onNull Context context, @Nullable CachedBluetoothDevice cachedDevice)350     public static void setUserPreferredPrimary(
351             @NonNull Context context, @Nullable CachedBluetoothDevice cachedDevice) {
352         if (cachedDevice == null) return;
353         if (BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(context)) {
354             int groupId = BluetoothUtils.getGroupId(cachedDevice);
355             // TODO: use real key name in SettingsProvider
356             int userPreferredId =
357                     Settings.Secure.getInt(
358                             context.getContentResolver(),
359                             BLUETOOTH_LE_BROADCAST_PRIMARY_DEVICE_GROUP_ID,
360                             BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
361             if (groupId != userPreferredId) {
362                 Settings.Secure.putInt(
363                         context.getContentResolver(),
364                         BLUETOOTH_LE_BROADCAST_PRIMARY_DEVICE_GROUP_ID,
365                         groupId);
366             }
367         }
368     }
369 
370     /**
371      * Build audio sharing dialog log event data
372      *
373      * @param sourcePageId The source page id on which the dialog is shown. *
374      * @param pageId The page id of the dialog.
375      * @param userTriggered Indicates whether the dialog is triggered by user click.
376      * @param deviceCountInSharing The count of the devices joining the audio sharing.
377      * @param candidateDeviceCount The count of the eligible devices to join the audio sharing.
378      * @return The event data to be attached to the audio sharing action logs.
379      */
380     @NonNull
buildAudioSharingDialogEventData( int sourcePageId, int pageId, boolean userTriggered, int deviceCountInSharing, int candidateDeviceCount)381     public static Pair<Integer, Object>[] buildAudioSharingDialogEventData(
382             int sourcePageId,
383             int pageId,
384             boolean userTriggered,
385             int deviceCountInSharing,
386             int candidateDeviceCount) {
387         return new Pair[] {
388             Pair.create(METRIC_KEY_SOURCE_PAGE_ID.ordinal(), sourcePageId),
389             Pair.create(METRIC_KEY_PAGE_ID.ordinal(), pageId),
390             Pair.create(METRIC_KEY_USER_TRIGGERED.ordinal(), userTriggered ? 1 : 0),
391             Pair.create(METRIC_KEY_DEVICE_COUNT_IN_SHARING.ordinal(), deviceCountInSharing),
392             Pair.create(METRIC_KEY_CANDIDATE_DEVICE_COUNT.ordinal(), candidateDeviceCount)
393         };
394     }
395 
396     private static final Comparator<CachedBluetoothDevice> sCachedDeviceComparator =
397             (CachedBluetoothDevice d1, CachedBluetoothDevice d2) -> {
398                 // Active above not inactive
399                 int comparison =
400                         (isActiveLeAudioDevice(d2) ? 1 : 0) - (isActiveLeAudioDevice(d1) ? 1 : 0);
401                 if (comparison != 0) return comparison;
402                 // Bonded above not bonded
403                 comparison =
404                         (d2.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0)
405                                 - (d1.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
406                 if (comparison != 0) return comparison;
407                 // Bond timestamp available above unavailable
408                 comparison =
409                         (d2.getBondTimestamp() != null ? 1 : 0)
410                                 - (d1.getBondTimestamp() != null ? 1 : 0);
411                 if (comparison != 0) return comparison;
412                 // Order by bond timestamp if it is available
413                 // Otherwise order by device name
414                 return d1.getBondTimestamp() != null
415                         ? d1.getBondTimestamp().compareTo(d2.getBondTimestamp())
416                         : d1.getName().compareTo(d2.getName());
417             };
418 }
419