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.audiostreams;
18 
19 import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_ID;
20 import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_TITLE;
21 import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.DEVICES;
22 
23 import static java.util.Collections.emptyList;
24 
25 import android.bluetooth.BluetoothDevice;
26 import android.bluetooth.BluetoothLeAudioContentMetadata;
27 import android.bluetooth.BluetoothLeBroadcastMetadata;
28 import android.bluetooth.BluetoothLeBroadcastReceiveState;
29 import android.bluetooth.BluetoothProfile;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.util.Log;
33 
34 import androidx.annotation.VisibleForTesting;
35 
36 import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
37 import com.android.settingslib.bluetooth.BluetoothUtils;
38 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
39 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
40 import com.android.settingslib.bluetooth.LocalBluetoothManager;
41 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
42 import com.android.settingslib.utils.ThreadUtils;
43 
44 import com.google.common.base.Strings;
45 
46 import java.util.ArrayList;
47 import java.util.List;
48 import java.util.Optional;
49 import java.util.stream.Stream;
50 
51 import javax.annotation.Nullable;
52 
53 /**
54  * A helper class that adds, removes and retrieves LE broadcast sources for all active sink devices.
55  */
56 public class AudioStreamsHelper {
57 
58     private static final String TAG = "AudioStreamsHelper";
59     private static final boolean DEBUG = BluetoothUtils.D;
60 
61     private final @Nullable LocalBluetoothManager mBluetoothManager;
62     private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
63 
AudioStreamsHelper(@ullable LocalBluetoothManager bluetoothManager)64     AudioStreamsHelper(@Nullable LocalBluetoothManager bluetoothManager) {
65         mBluetoothManager = bluetoothManager;
66         mLeBroadcastAssistant = getLeBroadcastAssistant(mBluetoothManager);
67     }
68 
69     /**
70      * Adds the specified LE broadcast source to all active sinks.
71      *
72      * @param source The LE broadcast metadata representing the audio source.
73      */
addSource(BluetoothLeBroadcastMetadata source)74     void addSource(BluetoothLeBroadcastMetadata source) {
75         if (mLeBroadcastAssistant == null) {
76             Log.w(TAG, "addSource(): LeBroadcastAssistant is null!");
77             return;
78         }
79         var unused =
80                 ThreadUtils.postOnBackgroundThread(
81                         () -> {
82                             for (var sink :
83                                     getConnectedBluetoothDevices(
84                                             mBluetoothManager, /* inSharingOnly= */ false)) {
85                                 if (DEBUG) {
86                                     Log.d(
87                                             TAG,
88                                             "addSource(): join broadcast broadcastId"
89                                                     + " : "
90                                                     + source.getBroadcastId()
91                                                     + " sink : "
92                                                     + sink.getAddress());
93                                 }
94                                 mLeBroadcastAssistant.addSource(sink, source, false);
95                             }
96                         });
97     }
98 
99     /** Removes sources from LE broadcasts associated for all active sinks based on broadcast Id. */
removeSource(int broadcastId)100     void removeSource(int broadcastId) {
101         if (mLeBroadcastAssistant == null) {
102             Log.w(TAG, "removeSource(): LeBroadcastAssistant is null!");
103             return;
104         }
105         var unused =
106                 ThreadUtils.postOnBackgroundThread(
107                         () -> {
108                             for (var sink :
109                                     getConnectedBluetoothDevices(
110                                             mBluetoothManager, /* inSharingOnly= */ true)) {
111                                 if (DEBUG) {
112                                     Log.d(
113                                             TAG,
114                                             "removeSource(): remove all sources with broadcast id :"
115                                                     + broadcastId
116                                                     + " from sink : "
117                                                     + sink.getAddress());
118                                 }
119                                 mLeBroadcastAssistant.getAllSources(sink).stream()
120                                         .filter(state -> state.getBroadcastId() == broadcastId)
121                                         .forEach(
122                                                 state ->
123                                                         mLeBroadcastAssistant.removeSource(
124                                                                 sink, state.getSourceId()));
125                             }
126                         });
127     }
128 
129     /** Retrieves a list of all LE broadcast receive states from active sinks. */
130     @VisibleForTesting
getAllConnectedSources()131     public List<BluetoothLeBroadcastReceiveState> getAllConnectedSources() {
132         if (mLeBroadcastAssistant == null) {
133             Log.w(TAG, "getAllSources(): LeBroadcastAssistant is null!");
134             return emptyList();
135         }
136         return getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true).stream()
137                 .flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream())
138                 .filter(AudioStreamsHelper::isConnected)
139                 .toList();
140     }
141 
142     /** Retrieves LocalBluetoothLeBroadcastAssistant. */
143     @VisibleForTesting
144     @Nullable
getLeBroadcastAssistant()145     public LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant() {
146         return mLeBroadcastAssistant;
147     }
148 
149     /** Checks the connectivity status based on the provided broadcast receive state. */
isConnected(BluetoothLeBroadcastReceiveState state)150     public static boolean isConnected(BluetoothLeBroadcastReceiveState state) {
151         return state.getBisSyncState().stream().anyMatch(bitmap -> bitmap != 0);
152     }
153 
isBadCode(BluetoothLeBroadcastReceiveState state)154     static boolean isBadCode(BluetoothLeBroadcastReceiveState state) {
155         return state.getPaSyncState() == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED
156                 && state.getBigEncryptionState()
157                         == BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_BAD_CODE;
158     }
159 
160     /**
161      * Returns a {@code CachedBluetoothDevice} that is either connected to a broadcast source or is
162      * a connected LE device.
163      */
getCachedBluetoothDeviceInSharingOrLeConnected( @ndroidx.annotation.Nullable LocalBluetoothManager manager)164     public static Optional<CachedBluetoothDevice> getCachedBluetoothDeviceInSharingOrLeConnected(
165             @androidx.annotation.Nullable LocalBluetoothManager manager) {
166         if (manager == null) {
167             Log.w(
168                     TAG,
169                     "getCachedBluetoothDeviceInSharingOrLeConnected(): LocalBluetoothManager is"
170                             + " null!");
171             return Optional.empty();
172         }
173         var groupedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(manager);
174         var leadDevices =
175                 AudioSharingUtils.buildOrderedConnectedLeadDevices(manager, groupedDevices, false);
176         if (leadDevices.isEmpty()) {
177             Log.w(TAG, "getCachedBluetoothDeviceInSharingOrLeConnected(): No lead device!");
178             return Optional.empty();
179         }
180         var deviceHasSource =
181                 leadDevices.stream()
182                         .filter(device -> hasConnectedBroadcastSource(device, manager))
183                         .findFirst();
184         if (deviceHasSource.isPresent()) {
185             Log.d(
186                     TAG,
187                     "getCachedBluetoothDeviceInSharingOrLeConnected(): Device has connected source"
188                             + " found: "
189                             + deviceHasSource.get().getAddress());
190             return deviceHasSource;
191         }
192         Log.d(
193                 TAG,
194                 "getCachedBluetoothDeviceInSharingOrLeConnected(): Device connected found: "
195                         + leadDevices.get(0).getAddress());
196         return Optional.of(leadDevices.get(0));
197     }
198 
199     /** Returns a {@code CachedBluetoothDevice} that has a connected broadcast source. */
getCachedBluetoothDeviceInSharing( @ndroidx.annotation.Nullable LocalBluetoothManager manager)200     static Optional<CachedBluetoothDevice> getCachedBluetoothDeviceInSharing(
201             @androidx.annotation.Nullable LocalBluetoothManager manager) {
202         if (manager == null) {
203             Log.w(TAG, "getCachedBluetoothDeviceInSharing(): LocalBluetoothManager is null!");
204             return Optional.empty();
205         }
206         var groupedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(manager);
207         var leadDevices =
208                 AudioSharingUtils.buildOrderedConnectedLeadDevices(manager, groupedDevices, false);
209         if (leadDevices.isEmpty()) {
210             Log.w(TAG, "getCachedBluetoothDeviceInSharing(): No lead device!");
211             return Optional.empty();
212         }
213         return leadDevices.stream()
214                 .filter(device -> hasConnectedBroadcastSource(device, manager))
215                 .findFirst();
216     }
217 
218     /**
219      * Check if {@link CachedBluetoothDevice} has connected to a broadcast source.
220      *
221      * @param cachedDevice The cached bluetooth device to check.
222      * @param localBtManager The BT manager to provide BT functions.
223      * @return Whether the device has connected to a broadcast source.
224      */
hasConnectedBroadcastSource( CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager)225     private static boolean hasConnectedBroadcastSource(
226             CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager) {
227         if (localBtManager == null) {
228             Log.d(TAG, "Skip check hasConnectedBroadcastSource due to bt manager is null");
229             return false;
230         }
231         LocalBluetoothLeBroadcastAssistant assistant =
232                 localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
233         if (assistant == null) {
234             Log.d(TAG, "Skip check hasConnectedBroadcastSource due to assistant profile is null");
235             return false;
236         }
237         List<BluetoothLeBroadcastReceiveState> sourceList =
238                 assistant.getAllSources(cachedDevice.getDevice());
239         if (!sourceList.isEmpty()
240                 && sourceList.stream().anyMatch(AudioStreamsHelper::isConnected)) {
241             Log.d(
242                     TAG,
243                     "Lead device has connected broadcast source, device = "
244                             + cachedDevice.getDevice().getAnonymizedAddress());
245             return true;
246         }
247         // Return true if member device is in broadcast.
248         for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) {
249             List<BluetoothLeBroadcastReceiveState> list =
250                     assistant.getAllSources(device.getDevice());
251             if (!list.isEmpty() && list.stream().anyMatch(AudioStreamsHelper::isConnected)) {
252                 Log.d(
253                         TAG,
254                         "Member device has connected broadcast source, device = "
255                                 + device.getDevice().getAnonymizedAddress());
256                 return true;
257             }
258         }
259         return false;
260     }
261 
262     /**
263      * Retrieves a list of connected Bluetooth devices that belongs to one {@link
264      * CachedBluetoothDevice} that's either connected to a broadcast source or is a connected LE
265      * audio device.
266      */
getConnectedBluetoothDevices( @ullable LocalBluetoothManager manager, boolean inSharingOnly)267     static List<BluetoothDevice> getConnectedBluetoothDevices(
268             @Nullable LocalBluetoothManager manager, boolean inSharingOnly) {
269         if (manager == null) {
270             Log.w(TAG, "getConnectedBluetoothDevices(): LocalBluetoothManager is null!");
271             return emptyList();
272         }
273         var leBroadcastAssistant = getLeBroadcastAssistant(manager);
274         if (leBroadcastAssistant == null) {
275             Log.w(TAG, "getConnectedBluetoothDevices(): LeBroadcastAssistant is null!");
276             return emptyList();
277         }
278         List<BluetoothDevice> connectedDevices =
279                 leBroadcastAssistant.getDevicesMatchingConnectionStates(
280                         new int[] {BluetoothProfile.STATE_CONNECTED});
281         Optional<CachedBluetoothDevice> cachedBluetoothDevice =
282                 inSharingOnly
283                         ? getCachedBluetoothDeviceInSharing(manager)
284                         : getCachedBluetoothDeviceInSharingOrLeConnected(manager);
285         List<BluetoothDevice> bluetoothDevices =
286                 cachedBluetoothDevice
287                         .map(
288                                 c ->
289                                         Stream.concat(
290                                                         Stream.of(c.getDevice()),
291                                                         c.getMemberDevice().stream()
292                                                                 .map(
293                                                                         CachedBluetoothDevice
294                                                                                 ::getDevice))
295                                                 .filter(connectedDevices::contains)
296                                                 .toList())
297                         .orElse(emptyList());
298         Log.d(TAG, "getConnectedBluetoothDevices() devices: " + bluetoothDevices);
299         return bluetoothDevices;
300     }
301 
getLeBroadcastAssistant( @ullable LocalBluetoothManager manager)302     private static @Nullable LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant(
303             @Nullable LocalBluetoothManager manager) {
304         if (manager == null) {
305             Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothManager is null!");
306             return null;
307         }
308 
309         LocalBluetoothProfileManager profileManager = manager.getProfileManager();
310         if (profileManager == null) {
311             Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothProfileManager is null!");
312             return null;
313         }
314 
315         return profileManager.getLeAudioBroadcastAssistantProfile();
316     }
317 
getBroadcastName(BluetoothLeBroadcastMetadata source)318     static String getBroadcastName(BluetoothLeBroadcastMetadata source) {
319         String broadcastName = source.getBroadcastName();
320         if (broadcastName != null && !broadcastName.isEmpty()) {
321             return broadcastName;
322         }
323         return source.getSubgroups().stream()
324                 .map(subgroup -> subgroup.getContentMetadata().getProgramInfo())
325                 .filter(programInfo -> !Strings.isNullOrEmpty(programInfo))
326                 .findFirst()
327                 .orElse("Broadcast Id: " + source.getBroadcastId());
328     }
329 
getBroadcastName(BluetoothLeBroadcastReceiveState state)330     static String getBroadcastName(BluetoothLeBroadcastReceiveState state) {
331         return state.getSubgroupMetadata().stream()
332                 .map(BluetoothLeAudioContentMetadata::getProgramInfo)
333                 .filter(i -> !Strings.isNullOrEmpty(i))
334                 .findFirst()
335                 .orElse("Broadcast Id: " + state.getBroadcastId());
336     }
337 
startMediaService(Context context, int audioStreamBroadcastId, String title)338     void startMediaService(Context context, int audioStreamBroadcastId, String title) {
339         List<BluetoothDevice> devices =
340                 getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true);
341         if (devices.isEmpty()) {
342             return;
343         }
344         var intent = new Intent(context, AudioStreamMediaService.class);
345         intent.putExtra(BROADCAST_ID, audioStreamBroadcastId);
346         intent.putExtra(BROADCAST_TITLE, title);
347         intent.putParcelableArrayListExtra(DEVICES, new ArrayList<>(devices));
348         context.startService(intent);
349     }
350 }
351