• 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.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 import static com.android.settingslib.accessibility.AccessibilityUtils.setAccessibilityServiceState;
23 import static com.android.settingslib.bluetooth.BluetoothUtils.isAudioSharingHysteresisModeFixAvailable;
24 import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState;
25 import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.DECRYPTION_FAILED;
26 import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.PAUSED;
27 import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.STREAMING;
28 import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.getLocalSourceState;
29 
30 import static java.util.Collections.emptyList;
31 import static java.util.Collections.emptyMap;
32 import static java.util.stream.Collectors.toMap;
33 
34 import android.accessibilityservice.AccessibilityServiceInfo;
35 import android.bluetooth.BluetoothDevice;
36 import android.bluetooth.BluetoothLeAudioContentMetadata;
37 import android.bluetooth.BluetoothLeBroadcastMetadata;
38 import android.bluetooth.BluetoothLeBroadcastReceiveState;
39 import android.content.ComponentName;
40 import android.content.Context;
41 import android.content.Intent;
42 import android.content.res.Configuration;
43 import android.util.Log;
44 import android.util.Pair;
45 import android.view.accessibility.AccessibilityManager;
46 
47 import androidx.annotation.VisibleForTesting;
48 import androidx.fragment.app.FragmentActivity;
49 
50 import com.android.settings.R;
51 import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
52 import com.android.settingslib.bluetooth.BluetoothUtils;
53 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
54 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
55 import com.android.settingslib.bluetooth.LocalBluetoothManager;
56 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
57 import com.android.settingslib.utils.ThreadUtils;
58 
59 import com.google.android.material.appbar.AppBarLayout;
60 import com.google.common.base.Strings;
61 
62 import java.util.ArrayList;
63 import java.util.Collections;
64 import java.util.HashSet;
65 import java.util.List;
66 import java.util.Map;
67 import java.util.Optional;
68 import java.util.Set;
69 import java.util.function.Function;
70 import java.util.stream.Stream;
71 
72 import javax.annotation.Nullable;
73 
74 /**
75  * A helper class that adds, removes and retrieves LE broadcast sources for all active sink devices.
76  */
77 public class AudioStreamsHelper {
78 
79     private static final String TAG = "AudioStreamsHelper";
80     private static final boolean DEBUG = BluetoothUtils.D;
81 
82     private final @Nullable LocalBluetoothManager mBluetoothManager;
83     private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
84 
AudioStreamsHelper(@ullable LocalBluetoothManager bluetoothManager)85     AudioStreamsHelper(@Nullable LocalBluetoothManager bluetoothManager) {
86         mBluetoothManager = bluetoothManager;
87         mLeBroadcastAssistant = getLeBroadcastAssistant(mBluetoothManager);
88     }
89 
90     /**
91      * Adds the specified LE broadcast source to all active sinks.
92      *
93      * @param source The LE broadcast metadata representing the audio source.
94      */
95     @VisibleForTesting
addSource(BluetoothLeBroadcastMetadata source)96     public void addSource(BluetoothLeBroadcastMetadata source) {
97         if (mLeBroadcastAssistant == null) {
98             Log.w(TAG, "addSource(): LeBroadcastAssistant is null!");
99             return;
100         }
101         var unused =
102                 ThreadUtils.postOnBackgroundThread(
103                         () -> {
104                             for (var sink :
105                                     getConnectedBluetoothDevices(
106                                             mBluetoothManager, /* inSharingOnly= */ false)) {
107                                 if (DEBUG) {
108                                     Log.d(
109                                             TAG,
110                                             "addSource(): join broadcast broadcastId"
111                                                     + " : "
112                                                     + source.getBroadcastId()
113                                                     + " sink : "
114                                                     + sink.getAddress());
115                                 }
116                                 mLeBroadcastAssistant.addSource(sink, source, false);
117                             }
118                         });
119     }
120 
121     /** Removes sources from LE broadcasts associated for all active sinks based on broadcast Id. */
122     @VisibleForTesting
removeSource(int broadcastId)123     public void removeSource(int broadcastId) {
124         if (mLeBroadcastAssistant == null) {
125             Log.w(TAG, "removeSource(): LeBroadcastAssistant is null!");
126             return;
127         }
128         var unused =
129                 ThreadUtils.postOnBackgroundThread(
130                         () -> {
131                             for (var sink :
132                                     getConnectedBluetoothDevices(
133                                             mBluetoothManager, /* inSharingOnly= */ true)) {
134                                 if (DEBUG) {
135                                     Log.d(
136                                             TAG,
137                                             "removeSource(): remove all sources with broadcast id :"
138                                                     + broadcastId
139                                                     + " from sink : "
140                                                     + sink.getAddress());
141                                 }
142                                 mLeBroadcastAssistant.getAllSources(sink).stream()
143                                         .filter(state -> state.getBroadcastId() == broadcastId)
144                                         .forEach(
145                                                 state ->
146                                                         mLeBroadcastAssistant.removeSource(
147                                                                 sink, state.getSourceId()));
148                             }
149                         });
150     }
151 
152     /**
153      * Gets a map of connected broadcast IDs to their corresponding local broadcast source states.
154      *
155      * <p>If multiple sources have the same broadcast ID, the state of the source that is
156      * {@code STREAMING} is preferred.
157      */
getConnectedBroadcastIdAndState( boolean hysteresisModeFixAvailable)158     public Map<Integer, LocalBluetoothLeBroadcastSourceState> getConnectedBroadcastIdAndState(
159             boolean hysteresisModeFixAvailable) {
160         if (mBluetoothManager == null || mLeBroadcastAssistant == null) {
161             Log.w(TAG,
162                     "getConnectedBroadcastIdAndState(): BluetoothManager or LeBroadcastAssistant "
163                             + "is null!");
164             return emptyMap();
165         }
166         return getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true).stream()
167                 .flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream())
168                 .map(state -> new Pair<>(state.getBroadcastId(), getLocalSourceState(state)))
169                 .filter(pair -> pair.second == STREAMING
170                         || (hysteresisModeFixAvailable && pair.second == PAUSED))
171                 .collect(toMap(
172                         p -> p.first,
173                         p -> p.second,
174                         (existingState, newState) -> existingState == STREAMING ? existingState
175                                 : newState
176                 ));
177     }
178 
179     /** Retrieves a list of all LE broadcast receive states keyed by each active device. */
getAllSourcesByDevice()180     public Map<BluetoothDevice, List<BluetoothLeBroadcastReceiveState>> getAllSourcesByDevice() {
181         if (mLeBroadcastAssistant == null) {
182             Log.w(TAG, "getAllSourcesByDevice(): LeBroadcastAssistant is null!");
183             return emptyMap();
184         }
185         return getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true).stream()
186                 .collect(toMap(Function.identity(), mLeBroadcastAssistant::getAllSources));
187     }
188 
189     /** Retrieves LocalBluetoothLeBroadcastAssistant. */
190     @Nullable
getLeBroadcastAssistant()191     public LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant() {
192         return mLeBroadcastAssistant;
193     }
194 
195     /**
196      * Returns a {@code CachedBluetoothDevice} that is either connected to a broadcast source or is
197      * a connected LE device.
198      */
getCachedBluetoothDeviceInSharingOrLeConnected( @ndroidx.annotation.Nullable LocalBluetoothManager manager)199     public static Optional<CachedBluetoothDevice> getCachedBluetoothDeviceInSharingOrLeConnected(
200             @androidx.annotation.Nullable LocalBluetoothManager manager) {
201         if (manager == null) {
202             Log.w(
203                     TAG,
204                     "getCachedBluetoothDeviceInSharingOrLeConnected(): LocalBluetoothManager is"
205                             + " null!");
206             return Optional.empty();
207         }
208         var groupedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(manager);
209         var leadDevices =
210                 AudioSharingUtils.buildOrderedConnectedLeadDevices(manager, groupedDevices, false);
211         if (leadDevices.isEmpty()) {
212             Log.w(TAG, "getCachedBluetoothDeviceInSharingOrLeConnected(): No lead device!");
213             return Optional.empty();
214         }
215         var deviceHasSource =
216                 leadDevices.stream()
217                         .filter(device -> hasBroadcastSource(device, manager))
218                         .findFirst();
219         if (deviceHasSource.isPresent()) {
220             Log.d(
221                     TAG,
222                     "getCachedBluetoothDeviceInSharingOrLeConnected(): Device has connected source"
223                             + " found: "
224                             + deviceHasSource.get().getAddress());
225             return deviceHasSource;
226         }
227         Log.d(
228                 TAG,
229                 "getCachedBluetoothDeviceInSharingOrLeConnected(): Device connected found: "
230                         + leadDevices.get(0).getAddress());
231         return Optional.of(leadDevices.get(0));
232     }
233 
234     /** Returns a {@code CachedBluetoothDevice} that has a connected broadcast source. */
getCachedBluetoothDeviceInSharing( @ndroidx.annotation.Nullable LocalBluetoothManager manager)235     static Optional<CachedBluetoothDevice> getCachedBluetoothDeviceInSharing(
236             @androidx.annotation.Nullable LocalBluetoothManager manager) {
237         if (manager == null) {
238             Log.w(TAG, "getCachedBluetoothDeviceInSharing(): LocalBluetoothManager is null!");
239             return Optional.empty();
240         }
241         var groupedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(manager);
242         var leadDevices =
243                 AudioSharingUtils.buildOrderedConnectedLeadDevices(manager, groupedDevices, false);
244         if (leadDevices.isEmpty()) {
245             Log.w(TAG, "getCachedBluetoothDeviceInSharing(): No lead device!");
246             return Optional.empty();
247         }
248         return leadDevices.stream()
249                 .filter(device -> hasBroadcastSource(device, manager))
250                 .findFirst();
251     }
252 
253     /**
254      * Check if {@link CachedBluetoothDevice} has a broadcast source that is in STREAMING, PAUSED
255      * or DECRYPTION_FAILED state.
256      *
257      * @param cachedDevice   The cached bluetooth device to check.
258      * @param localBtManager The BT manager to provide BT functions.
259      * @return Whether the device has a broadcast source.
260      */
hasBroadcastSource( CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager)261     public static boolean hasBroadcastSource(
262             CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager) {
263         if (localBtManager == null) {
264             Log.d(TAG, "Skip check hasBroadcastSource due to bt manager is null");
265             return false;
266         }
267         LocalBluetoothLeBroadcastAssistant assistant =
268                 localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
269         if (assistant == null) {
270             Log.d(TAG, "Skip check hasBroadcastSource due to assistant profile is null");
271             return false;
272         }
273         List<BluetoothLeBroadcastReceiveState> sourceList =
274                 assistant.getAllSources(cachedDevice.getDevice());
275         boolean hysteresisModeFixAvailable = isAudioSharingHysteresisModeFixAvailable(
276                 localBtManager.getContext());
277         if (hasReceiveState(sourceList, hysteresisModeFixAvailable)) {
278             Log.d(
279                     TAG,
280                     "Lead device has broadcast source, device = "
281                             + cachedDevice.getDevice().getAnonymizedAddress());
282             return true;
283         }
284         // Return true if member device is in broadcast.
285         for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) {
286             List<BluetoothLeBroadcastReceiveState> list =
287                     assistant.getAllSources(device.getDevice());
288             if (hasReceiveState(list, hysteresisModeFixAvailable)) {
289                 Log.d(
290                         TAG,
291                         "Member device has broadcast source, device = "
292                                 + device.getDevice().getAnonymizedAddress());
293                 return true;
294             }
295         }
296         return false;
297     }
298 
hasReceiveState(List<BluetoothLeBroadcastReceiveState> states, boolean hysteresisModeFixAvailable)299     private static boolean hasReceiveState(List<BluetoothLeBroadcastReceiveState> states,
300             boolean hysteresisModeFixAvailable) {
301         return states.stream().anyMatch(state -> {
302             var localSourceState = getLocalSourceState(state);
303             if (hysteresisModeFixAvailable) {
304                 return localSourceState == STREAMING || localSourceState == DECRYPTION_FAILED
305                         || localSourceState == PAUSED;
306             }
307             return localSourceState == STREAMING || localSourceState == DECRYPTION_FAILED;
308         });
309     }
310 
311     /**
312      * Retrieves a list of connected Bluetooth devices that belongs to one {@link
313      * CachedBluetoothDevice} that's either connected to a broadcast source or is a connected LE
314      * audio device.
315      */
getConnectedBluetoothDevices( @ullable LocalBluetoothManager manager, boolean inSharingOnly)316     static List<BluetoothDevice> getConnectedBluetoothDevices(
317             @Nullable LocalBluetoothManager manager, boolean inSharingOnly) {
318         if (manager == null) {
319             Log.w(TAG, "getConnectedBluetoothDevices(): LocalBluetoothManager is null!");
320             return emptyList();
321         }
322         var leBroadcastAssistant = getLeBroadcastAssistant(manager);
323         if (leBroadcastAssistant == null) {
324             Log.w(TAG, "getConnectedBluetoothDevices(): LeBroadcastAssistant is null!");
325             return emptyList();
326         }
327         List<BluetoothDevice> connectedDevices = leBroadcastAssistant.getAllConnectedDevices();
328         Optional<CachedBluetoothDevice> cachedBluetoothDevice =
329                 inSharingOnly
330                         ? getCachedBluetoothDeviceInSharing(manager)
331                         : getCachedBluetoothDeviceInSharingOrLeConnected(manager);
332         List<BluetoothDevice> bluetoothDevices =
333                 cachedBluetoothDevice
334                         .map(
335                                 c ->
336                                         Stream.concat(
337                                                         Stream.of(c.getDevice()),
338                                                         c.getMemberDevice().stream()
339                                                                 .map(
340                                                                         CachedBluetoothDevice
341                                                                                 ::getDevice))
342                                                 .filter(connectedDevices::contains)
343                                                 .toList())
344                         .orElse(emptyList());
345         Log.d(TAG, "getConnectedBluetoothDevices() devices: " + bluetoothDevices);
346         return bluetoothDevices;
347     }
348 
getLeBroadcastAssistant( @ullable LocalBluetoothManager manager)349     private static @Nullable LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant(
350             @Nullable LocalBluetoothManager manager) {
351         if (manager == null) {
352             Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothManager is null!");
353             return null;
354         }
355 
356         LocalBluetoothProfileManager profileManager = manager.getProfileManager();
357         if (profileManager == null) {
358             Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothProfileManager is null!");
359             return null;
360         }
361 
362         return profileManager.getLeAudioBroadcastAssistantProfile();
363     }
364 
getBroadcastName(BluetoothLeBroadcastMetadata source)365     static String getBroadcastName(BluetoothLeBroadcastMetadata source) {
366         String broadcastName = source.getBroadcastName();
367         if (broadcastName != null && !broadcastName.isEmpty()) {
368             return broadcastName;
369         }
370         return source.getSubgroups().stream()
371                 .map(subgroup -> subgroup.getContentMetadata().getProgramInfo())
372                 .filter(programInfo -> !Strings.isNullOrEmpty(programInfo))
373                 .findFirst()
374                 .orElse("Broadcast Id: " + source.getBroadcastId());
375     }
376 
getBroadcastName(BluetoothLeBroadcastReceiveState state)377     static String getBroadcastName(BluetoothLeBroadcastReceiveState state) {
378         return state.getSubgroupMetadata().stream()
379                 .map(BluetoothLeAudioContentMetadata::getProgramInfo)
380                 .filter(i -> !Strings.isNullOrEmpty(i))
381                 .findFirst()
382                 .orElse("Broadcast Id: " + state.getBroadcastId());
383     }
384 
startMediaService(Context context, int audioStreamBroadcastId, String title)385     void startMediaService(Context context, int audioStreamBroadcastId, String title) {
386         List<BluetoothDevice> devices =
387                 getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true);
388         if (devices.isEmpty()) {
389             return;
390         }
391         var intent = new Intent(context, AudioStreamMediaService.class);
392         intent.putExtra(BROADCAST_ID, audioStreamBroadcastId);
393         intent.putExtra(BROADCAST_TITLE, title);
394         intent.putParcelableArrayListExtra(DEVICES, new ArrayList<>(devices));
395         context.startService(intent);
396     }
397 
configureAppBarByOrientation(@ullable FragmentActivity activity)398     static void configureAppBarByOrientation(@Nullable FragmentActivity activity) {
399         if (activity != null) {
400             AppBarLayout appBarLayout = activity.findViewById(R.id.app_bar);
401             if (appBarLayout != null) {
402                 boolean canAppBarExpand =
403                         activity.getResources().getConfiguration().orientation
404                                 == Configuration.ORIENTATION_PORTRAIT;
405                 appBarLayout.setExpanded(canAppBarExpand);
406             }
407         }
408     }
409 
410     /**
411      * Retrieves a set of enabled screen reader services that are pre-installed.
412      *
413      * <p>This method checks the accessibility manager for enabled accessibility services
414      * and filters them based on a list of pre-installed screen reader service component names
415      * defined in the {@code config_preinstalled_screen_reader_services} resource array.</p>
416      *
417      * @param context The context.
418      * @return A set of {@link ComponentName} objects representing the enabled pre-installed
419      * screen reader services, or an empty set if no services are found, or if an error occurs.
420      */
getEnabledScreenReaderServices(Context context)421     public static Set<ComponentName> getEnabledScreenReaderServices(Context context) {
422         AccessibilityManager manager = context.getSystemService(AccessibilityManager.class);
423         if (manager == null) {
424             return Collections.emptySet();
425         }
426         Set<String> screenReaderServices = new HashSet<>();
427         Collections.addAll(screenReaderServices, context.getResources()
428                 .getStringArray(R.array.config_preinstalled_screen_reader_services));
429         if (screenReaderServices.isEmpty()) {
430             return Collections.emptySet();
431         }
432         Set<ComponentName> enabledScreenReaderServices = new HashSet<>();
433         List<AccessibilityServiceInfo> enabledServices = manager.getEnabledAccessibilityServiceList(
434                 AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
435         for (AccessibilityServiceInfo service : enabledServices) {
436             ComponentName componentName = service.getComponentName();
437             if (screenReaderServices.contains(componentName.flattenToString())) {
438                 enabledScreenReaderServices.add(componentName);
439             }
440         }
441         Log.d(TAG, "getEnabledScreenReaderServices(): " + enabledScreenReaderServices);
442         return enabledScreenReaderServices;
443     }
444 
445     /**
446      * Turns off the specified accessibility services.
447      *
448      * This method iterates through a set of ComponentName objects, each representing an
449      * accessibility service, and disables them.
450      *
451      * @param context The application context.
452      * @param services A set of ComponentName objects representing the services to disable.
453      */
setAccessibilityServiceOff(Context context, Set<ComponentName> services)454     public static void setAccessibilityServiceOff(Context context, Set<ComponentName> services) {
455         for (ComponentName service : services) {
456             Log.d(TAG, "setScreenReaderOff(): " + service);
457             setAccessibilityServiceState(context, service, false);
458         }
459     }
460 }
461