1 /*
2  * Copyright (C) 2020 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.systemui.media.dialog;
18 
19 import static android.media.RouteListingPreference.ACTION_TRANSFER_MEDIA;
20 import static android.media.RouteListingPreference.EXTRA_ROUTE_ID;
21 import static android.provider.Settings.ACTION_BLUETOOTH_SETTINGS;
22 
23 import android.annotation.CallbackExecutor;
24 import android.app.AlertDialog;
25 import android.app.KeyguardManager;
26 import android.app.Notification;
27 import android.app.WallpaperColors;
28 import android.bluetooth.BluetoothDevice;
29 import android.bluetooth.BluetoothLeBroadcast;
30 import android.bluetooth.BluetoothLeBroadcastAssistant;
31 import android.bluetooth.BluetoothLeBroadcastMetadata;
32 import android.bluetooth.BluetoothLeBroadcastReceiveState;
33 import android.content.ComponentName;
34 import android.content.Context;
35 import android.content.DialogInterface;
36 import android.content.Intent;
37 import android.content.pm.ApplicationInfo;
38 import android.content.pm.PackageManager;
39 import android.graphics.Bitmap;
40 import android.graphics.PorterDuff;
41 import android.graphics.PorterDuffColorFilter;
42 import android.graphics.drawable.BitmapDrawable;
43 import android.graphics.drawable.Drawable;
44 import android.graphics.drawable.Icon;
45 import android.media.AudioManager;
46 import android.media.INearbyMediaDevicesUpdateCallback;
47 import android.media.MediaMetadata;
48 import android.media.MediaRoute2Info;
49 import android.media.NearbyDevice;
50 import android.media.RoutingSessionInfo;
51 import android.media.session.MediaController;
52 import android.media.session.MediaSession;
53 import android.media.session.MediaSessionManager;
54 import android.media.session.PlaybackState;
55 import android.os.IBinder;
56 import android.os.PowerExemptionManager;
57 import android.os.RemoteException;
58 import android.os.UserHandle;
59 import android.os.UserManager;
60 import android.provider.Settings;
61 import android.text.TextUtils;
62 import android.util.Log;
63 import android.view.View;
64 import android.view.WindowManager;
65 
66 import androidx.annotation.NonNull;
67 import androidx.annotation.VisibleForTesting;
68 import androidx.core.graphics.drawable.IconCompat;
69 
70 import com.android.settingslib.RestrictedLockUtilsInternal;
71 import com.android.settingslib.Utils;
72 import com.android.settingslib.bluetooth.BluetoothUtils;
73 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
74 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
75 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastMetadata;
76 import com.android.settingslib.bluetooth.LocalBluetoothManager;
77 import com.android.settingslib.media.InfoMediaManager;
78 import com.android.settingslib.media.LocalMediaManager;
79 import com.android.settingslib.media.MediaDevice;
80 import com.android.settingslib.utils.ThreadUtils;
81 import com.android.systemui.R;
82 import com.android.systemui.animation.ActivityLaunchAnimator;
83 import com.android.systemui.animation.DialogLaunchAnimator;
84 import com.android.systemui.broadcast.BroadcastSender;
85 import com.android.systemui.flags.FeatureFlags;
86 import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
87 import com.android.systemui.monet.ColorScheme;
88 import com.android.systemui.plugins.ActivityStarter;
89 import com.android.systemui.settings.UserTracker;
90 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
91 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
92 import com.android.systemui.statusbar.phone.SystemUIDialog;
93 
94 import java.nio.charset.StandardCharsets;
95 import java.util.ArrayList;
96 import java.util.Collection;
97 import java.util.Collections;
98 import java.util.Comparator;
99 import java.util.HashMap;
100 import java.util.List;
101 import java.util.Map;
102 import java.util.Set;
103 import java.util.concurrent.ConcurrentHashMap;
104 import java.util.concurrent.CopyOnWriteArrayList;
105 import java.util.concurrent.Executor;
106 import java.util.stream.Collectors;
107 
108 import javax.inject.Inject;
109 
110 /**
111  * Controller for media output dialog
112  */
113 public class MediaOutputController implements LocalMediaManager.DeviceCallback,
114         INearbyMediaDevicesUpdateCallback {
115 
116     private static final String TAG = "MediaOutputController";
117     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
118     private static final String PAGE_CONNECTED_DEVICES_KEY =
119             "top_level_connected_devices";
120     private static final long ALLOWLIST_DURATION_MS = 20000;
121     private static final String ALLOWLIST_REASON = "mediaoutput:remote_transfer";
122 
123     private final String mPackageName;
124     private final Context mContext;
125     private final MediaSessionManager mMediaSessionManager;
126     private final LocalBluetoothManager mLocalBluetoothManager;
127     private final ActivityStarter mActivityStarter;
128     private final DialogLaunchAnimator mDialogLaunchAnimator;
129     private final CommonNotifCollection mNotifCollection;
130     private final Object mMediaDevicesLock = new Object();
131     @VisibleForTesting
132     final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>();
133     final List<MediaDevice> mCachedMediaDevices = new CopyOnWriteArrayList<>();
134     private final List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>();
135     private final AudioManager mAudioManager;
136     private final PowerExemptionManager mPowerExemptionManager;
137     private final KeyguardManager mKeyGuardManager;
138     private final NearbyMediaDevicesManager mNearbyMediaDevicesManager;
139     private final Map<String, Integer> mNearbyDeviceInfoMap = new ConcurrentHashMap<>();
140 
141     @VisibleForTesting
142     boolean mIsRefreshing = false;
143     @VisibleForTesting
144     boolean mNeedRefresh = false;
145     private MediaController mMediaController;
146     @VisibleForTesting
147     Callback mCallback;
148     @VisibleForTesting
149     LocalMediaManager mLocalMediaManager;
150     @VisibleForTesting
151     MediaOutputMetricLogger mMetricLogger;
152     private int mCurrentState;
153 
154     private int mColorItemContent;
155     private int mColorSeekbarProgress;
156     private int mColorButtonBackground;
157     private int mColorItemBackground;
158     private int mColorConnectedItemBackground;
159     private int mColorPositiveButtonText;
160     private int mColorDialogBackground;
161     private int mItemMarginEndDefault;
162     private int mItemMarginEndSelectable;
163     private float mInactiveRadius;
164     private float mActiveRadius;
165     private FeatureFlags mFeatureFlags;
166     private UserTracker mUserTracker;
167 
168     public enum BroadcastNotifyDialog {
169         ACTION_FIRST_LAUNCH,
170         ACTION_BROADCAST_INFO_ICON
171     }
172 
173     @Inject
MediaOutputController(@onNull Context context, String packageName, MediaSessionManager mediaSessionManager, LocalBluetoothManager lbm, ActivityStarter starter, CommonNotifCollection notifCollection, DialogLaunchAnimator dialogLaunchAnimator, NearbyMediaDevicesManager nearbyMediaDevicesManager, AudioManager audioManager, PowerExemptionManager powerExemptionManager, KeyguardManager keyGuardManager, FeatureFlags featureFlags, UserTracker userTracker)174     public MediaOutputController(@NonNull Context context, String packageName,
175             MediaSessionManager mediaSessionManager, LocalBluetoothManager
176             lbm, ActivityStarter starter,
177             CommonNotifCollection notifCollection,
178             DialogLaunchAnimator dialogLaunchAnimator,
179             NearbyMediaDevicesManager nearbyMediaDevicesManager,
180             AudioManager audioManager,
181             PowerExemptionManager powerExemptionManager,
182             KeyguardManager keyGuardManager,
183             FeatureFlags featureFlags,
184             UserTracker userTracker) {
185         mContext = context;
186         mPackageName = packageName;
187         mMediaSessionManager = mediaSessionManager;
188         mLocalBluetoothManager = lbm;
189         mActivityStarter = starter;
190         mNotifCollection = notifCollection;
191         mAudioManager = audioManager;
192         mPowerExemptionManager = powerExemptionManager;
193         mKeyGuardManager = keyGuardManager;
194         mFeatureFlags = featureFlags;
195         mUserTracker = userTracker;
196         InfoMediaManager imm = new InfoMediaManager(mContext, packageName, null, lbm);
197         mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
198         mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName);
199         mDialogLaunchAnimator = dialogLaunchAnimator;
200         mNearbyMediaDevicesManager = nearbyMediaDevicesManager;
201         mColorItemContent = Utils.getColorStateListDefaultColor(mContext,
202                 R.color.media_dialog_item_main_content);
203         mColorSeekbarProgress = Utils.getColorStateListDefaultColor(mContext,
204                 R.color.media_dialog_seekbar_progress);
205         mColorButtonBackground = Utils.getColorStateListDefaultColor(mContext,
206                 R.color.media_dialog_button_background);
207         mColorItemBackground = Utils.getColorStateListDefaultColor(mContext,
208                 R.color.media_dialog_item_background);
209         mColorConnectedItemBackground = Utils.getColorStateListDefaultColor(mContext,
210                 R.color.media_dialog_connected_item_background);
211         mColorPositiveButtonText = Utils.getColorStateListDefaultColor(mContext,
212                 R.color.media_dialog_solid_button_text);
213         mInactiveRadius = mContext.getResources().getDimension(
214                 R.dimen.media_output_dialog_background_radius);
215         mActiveRadius = mContext.getResources().getDimension(
216                 R.dimen.media_output_dialog_active_background_radius);
217         mColorDialogBackground = Utils.getColorStateListDefaultColor(mContext,
218                 R.color.media_dialog_background);
219         mItemMarginEndDefault = (int) mContext.getResources().getDimension(
220                 R.dimen.media_output_dialog_default_margin_end);
221         mItemMarginEndSelectable = (int) mContext.getResources().getDimension(
222                 R.dimen.media_output_dialog_selectable_margin_end);
223     }
224 
start(@onNull Callback cb)225     void start(@NonNull Callback cb) {
226         synchronized (mMediaDevicesLock) {
227             mCachedMediaDevices.clear();
228             mMediaItemList.clear();
229         }
230         mNearbyDeviceInfoMap.clear();
231         if (mNearbyMediaDevicesManager != null) {
232             mNearbyMediaDevicesManager.registerNearbyDevicesCallback(this);
233         }
234         if (!TextUtils.isEmpty(mPackageName)) {
235             mMediaController = getMediaController();
236             if (mMediaController != null) {
237                 mMediaController.unregisterCallback(mCb);
238                 if (mMediaController.getPlaybackState() != null) {
239                     mCurrentState = mMediaController.getPlaybackState().getState();
240                 }
241                 mMediaController.registerCallback(mCb);
242             }
243         }
244         if (mMediaController == null) {
245             if (DEBUG) {
246                 Log.d(TAG, "No media controller for " + mPackageName);
247             }
248         }
249         mCallback = cb;
250         mLocalMediaManager.registerCallback(this);
251         mLocalMediaManager.startScan();
252     }
253 
shouldShowLaunchSection()254     boolean shouldShowLaunchSection() {
255         // TODO(b/231398073): Implements this when available.
256         return false;
257     }
258 
isRefreshing()259     boolean isRefreshing() {
260         return mIsRefreshing;
261     }
262 
setRefreshing(boolean refreshing)263     void setRefreshing(boolean refreshing) {
264         mIsRefreshing = refreshing;
265     }
266 
stop()267     void stop() {
268         if (mMediaController != null) {
269             mMediaController.unregisterCallback(mCb);
270         }
271         mLocalMediaManager.unregisterCallback(this);
272         mLocalMediaManager.stopScan();
273         synchronized (mMediaDevicesLock) {
274             mCachedMediaDevices.clear();
275             mMediaItemList.clear();
276         }
277         if (mNearbyMediaDevicesManager != null) {
278             mNearbyMediaDevicesManager.unregisterNearbyDevicesCallback(this);
279         }
280         mNearbyDeviceInfoMap.clear();
281     }
282 
getMediaController()283     private MediaController getMediaController() {
284         for (NotificationEntry entry : mNotifCollection.getAllNotifs()) {
285             final Notification notification = entry.getSbn().getNotification();
286             if (notification.isMediaNotification()
287                     && TextUtils.equals(entry.getSbn().getPackageName(), mPackageName)) {
288                 MediaSession.Token token = notification.extras.getParcelable(
289                         Notification.EXTRA_MEDIA_SESSION,
290                         MediaSession.Token.class);
291                 return new MediaController(mContext, token);
292             }
293         }
294         for (MediaController controller : mMediaSessionManager.getActiveSessionsForUser(null,
295                 mUserTracker.getUserHandle())) {
296             if (TextUtils.equals(controller.getPackageName(), mPackageName)) {
297                 return controller;
298             }
299         }
300         return null;
301     }
302 
303     @Override
onDeviceListUpdate(List<MediaDevice> devices)304     public void onDeviceListUpdate(List<MediaDevice> devices) {
305         boolean isListEmpty = mMediaItemList.isEmpty();
306         if (isListEmpty || !mIsRefreshing) {
307             buildMediaItems(devices);
308             mCallback.onDeviceListChanged();
309         } else {
310             synchronized (mMediaDevicesLock) {
311                 mNeedRefresh = true;
312                 mCachedMediaDevices.clear();
313                 mCachedMediaDevices.addAll(devices);
314             }
315         }
316     }
317 
318     @Override
onSelectedDeviceStateChanged(MediaDevice device, @LocalMediaManager.MediaDeviceState int state)319     public void onSelectedDeviceStateChanged(MediaDevice device,
320             @LocalMediaManager.MediaDeviceState int state) {
321         mCallback.onRouteChanged();
322         mMetricLogger.logOutputItemSuccess(device.toString(), new ArrayList<>(mMediaItemList));
323     }
324 
325     @Override
onDeviceAttributesChanged()326     public void onDeviceAttributesChanged() {
327         mCallback.onRouteChanged();
328     }
329 
330     @Override
onRequestFailed(int reason)331     public void onRequestFailed(int reason) {
332         mCallback.onRouteChanged();
333         mMetricLogger.logOutputItemFailure(new ArrayList<>(mMediaItemList), reason);
334     }
335 
336     /**
337      * Checks if there's any muting expected device exist
338      */
hasMutingExpectedDevice()339     public boolean hasMutingExpectedDevice() {
340         return mAudioManager.getMutingExpectedDevice() != null;
341     }
342 
343     /**
344      * Cancels mute await connection action in follow up request
345      */
cancelMuteAwaitConnection()346     public void cancelMuteAwaitConnection() {
347         if (mAudioManager.getMutingExpectedDevice() == null) {
348             return;
349         }
350         try {
351             synchronized (mMediaDevicesLock) {
352                 mMediaItemList.removeIf((MediaItem::isMutingExpectedDevice));
353             }
354             mAudioManager.cancelMuteAwaitConnection(mAudioManager.getMutingExpectedDevice());
355         } catch (Exception e) {
356             Log.d(TAG, "Unable to cancel mute await connection");
357         }
358     }
359 
getAppSourceIconFromPackage()360     Drawable getAppSourceIconFromPackage() {
361         if (mPackageName.isEmpty()) {
362             return null;
363         }
364         try {
365             Log.d(TAG, "try to get app icon");
366             return mContext.getPackageManager()
367                     .getApplicationIcon(mPackageName);
368         } catch (PackageManager.NameNotFoundException e) {
369             Log.d(TAG, "icon not found");
370             return null;
371         }
372     }
373 
getAppSourceName()374     String getAppSourceName() {
375         if (mPackageName.isEmpty()) {
376             return null;
377         }
378         final PackageManager packageManager = mContext.getPackageManager();
379         ApplicationInfo applicationInfo;
380         try {
381             applicationInfo = packageManager.getApplicationInfo(mPackageName,
382                     PackageManager.ApplicationInfoFlags.of(0));
383         } catch (PackageManager.NameNotFoundException e) {
384             applicationInfo = null;
385         }
386         final String applicationName =
387                 (String) (applicationInfo != null ? packageManager.getApplicationLabel(
388                         applicationInfo)
389                         : mContext.getString(R.string.media_output_dialog_unknown_launch_app_name));
390         return applicationName;
391     }
392 
getAppLaunchIntent()393     Intent getAppLaunchIntent() {
394         if (mPackageName.isEmpty()) {
395             return null;
396         }
397         return mContext.getPackageManager().getLaunchIntentForPackage(mPackageName);
398     }
399 
tryToLaunchInAppRoutingIntent(String routeId, View view)400     void tryToLaunchInAppRoutingIntent(String routeId, View view) {
401         ComponentName componentName = mLocalMediaManager.getLinkedItemComponentName();
402         if (componentName != null) {
403             ActivityLaunchAnimator.Controller controller =
404                     mDialogLaunchAnimator.createActivityLaunchController(view);
405             Intent launchIntent = new Intent(ACTION_TRANSFER_MEDIA);
406             launchIntent.setComponent(componentName);
407             launchIntent.putExtra(EXTRA_ROUTE_ID, routeId);
408             launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
409             mCallback.dismissDialog();
410             mActivityStarter.startActivity(launchIntent, true, controller);
411         }
412     }
413 
tryToLaunchMediaApplication(View view)414     void tryToLaunchMediaApplication(View view) {
415         ActivityLaunchAnimator.Controller controller =
416                 mDialogLaunchAnimator.createActivityLaunchController(view);
417         Intent launchIntent = getAppLaunchIntent();
418         if (launchIntent != null) {
419             launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
420             mCallback.dismissDialog();
421             mActivityStarter.startActivity(launchIntent, true, controller);
422         }
423     }
424 
getHeaderTitle()425     CharSequence getHeaderTitle() {
426         if (mMediaController != null) {
427             final MediaMetadata metadata = mMediaController.getMetadata();
428             if (metadata != null) {
429                 return metadata.getDescription().getTitle();
430             }
431         }
432         return mContext.getText(R.string.controls_media_title);
433     }
434 
getHeaderSubTitle()435     CharSequence getHeaderSubTitle() {
436         if (mMediaController == null) {
437             return null;
438         }
439         final MediaMetadata metadata = mMediaController.getMetadata();
440         if (metadata == null) {
441             return null;
442         }
443         return metadata.getDescription().getSubtitle();
444     }
445 
getHeaderIcon()446     IconCompat getHeaderIcon() {
447         if (mMediaController == null) {
448             return null;
449         }
450         final MediaMetadata metadata = mMediaController.getMetadata();
451         if (metadata != null) {
452             final Bitmap bitmap = metadata.getDescription().getIconBitmap();
453             if (bitmap != null) {
454                 final Bitmap roundBitmap = Utils.convertCornerRadiusBitmap(mContext, bitmap,
455                         (float) mContext.getResources().getDimensionPixelSize(
456                                 R.dimen.media_output_dialog_icon_corner_radius));
457                 return IconCompat.createWithBitmap(roundBitmap);
458             }
459         }
460         if (DEBUG) {
461             Log.d(TAG, "Media meta data does not contain icon information");
462         }
463         return getNotificationIcon();
464     }
465 
getDeviceIconCompat(MediaDevice device)466     IconCompat getDeviceIconCompat(MediaDevice device) {
467         Drawable drawable = device.getIcon();
468         if (drawable == null) {
469             if (DEBUG) {
470                 Log.d(TAG, "getDeviceIconCompat() device : " + device.getName()
471                         + ", drawable is null");
472             }
473             // Use default Bluetooth device icon to handle getIcon() is null case.
474             drawable = mContext.getDrawable(com.android.internal.R.drawable.ic_bt_headphones_a2dp);
475         }
476         if (!(drawable instanceof BitmapDrawable)) {
477             setColorFilter(drawable, isActiveItem(device));
478         }
479         return BluetoothUtils.createIconWithDrawable(drawable);
480     }
481 
setColorFilter(Drawable drawable, boolean isActive)482     void setColorFilter(Drawable drawable, boolean isActive) {
483         drawable.setColorFilter(new PorterDuffColorFilter(mColorItemContent,
484                 PorterDuff.Mode.SRC_IN));
485     }
486 
isActiveItem(MediaDevice device)487     boolean isActiveItem(MediaDevice device) {
488         boolean isConnected = mLocalMediaManager.getCurrentConnectedDevice().getId().equals(
489                 device.getId());
490         boolean isSelectedDeviceInGroup = getSelectedMediaDevice().size() > 1
491                 && getSelectedMediaDevice().contains(device);
492         return (!hasAdjustVolumeUserRestriction() && isConnected && !isAnyDeviceTransferring())
493                 || isSelectedDeviceInGroup;
494     }
495 
getNotificationSmallIcon()496     IconCompat getNotificationSmallIcon() {
497         if (TextUtils.isEmpty(mPackageName)) {
498             return null;
499         }
500         for (NotificationEntry entry : mNotifCollection.getAllNotifs()) {
501             final Notification notification = entry.getSbn().getNotification();
502             if (notification.isMediaNotification()
503                     && TextUtils.equals(entry.getSbn().getPackageName(), mPackageName)) {
504                 final Icon icon = notification.getSmallIcon();
505                 if (icon == null) {
506                     break;
507                 }
508                 return IconCompat.createFromIcon(icon);
509             }
510         }
511         return null;
512     }
513 
getNotificationIcon()514     IconCompat getNotificationIcon() {
515         if (TextUtils.isEmpty(mPackageName)) {
516             return null;
517         }
518         for (NotificationEntry entry : mNotifCollection.getAllNotifs()) {
519             final Notification notification = entry.getSbn().getNotification();
520             if (notification.isMediaNotification()
521                     && TextUtils.equals(entry.getSbn().getPackageName(), mPackageName)) {
522                 final Icon icon = notification.getLargeIcon();
523                 if (icon == null) {
524                     break;
525                 }
526                 return IconCompat.createFromIcon(icon);
527             }
528         }
529         return null;
530     }
531 
setCurrentColorScheme(WallpaperColors wallpaperColors, boolean isDarkTheme)532     void setCurrentColorScheme(WallpaperColors wallpaperColors, boolean isDarkTheme) {
533         ColorScheme mCurrentColorScheme = new ColorScheme(wallpaperColors,
534                 isDarkTheme);
535         if (isDarkTheme) {
536             mColorItemContent = mCurrentColorScheme.getAccent1().getS100(); // A1-100
537             mColorSeekbarProgress = mCurrentColorScheme.getAccent2().getS600(); // A2-600
538             mColorButtonBackground = mCurrentColorScheme.getAccent1().getS300(); // A1-300
539             mColorItemBackground = mCurrentColorScheme.getNeutral2().getS800(); // N2-800
540             mColorConnectedItemBackground = mCurrentColorScheme.getAccent2().getS800(); // A2-800
541             mColorPositiveButtonText = mCurrentColorScheme.getAccent2().getS800(); // A2-800
542             mColorDialogBackground = mCurrentColorScheme.getNeutral1().getS900(); // N1-900
543         } else {
544             mColorItemContent = mCurrentColorScheme.getAccent1().getS800(); // A1-800
545             mColorSeekbarProgress = mCurrentColorScheme.getAccent1().getS300(); // A1-300
546             mColorButtonBackground = mCurrentColorScheme.getAccent1().getS600(); // A1-600
547             mColorItemBackground = mCurrentColorScheme.getAccent2().getS50(); // A2-50
548             mColorConnectedItemBackground = mCurrentColorScheme.getAccent1().getS100(); // A1-100
549             mColorPositiveButtonText = mCurrentColorScheme.getNeutral1().getS50(); // N1-50
550             mColorDialogBackground = mCurrentColorScheme.getBackgroundColor();
551         }
552     }
553 
refreshDataSetIfNeeded()554     void refreshDataSetIfNeeded() {
555         if (mNeedRefresh) {
556             buildMediaItems(mCachedMediaDevices);
557             mCallback.onDeviceListChanged();
558             mNeedRefresh = false;
559         }
560     }
561 
getColorConnectedItemBackground()562     public int getColorConnectedItemBackground() {
563         return mColorConnectedItemBackground;
564     }
565 
getColorPositiveButtonText()566     public int getColorPositiveButtonText() {
567         return mColorPositiveButtonText;
568     }
569 
getColorDialogBackground()570     public int getColorDialogBackground() {
571         return mColorDialogBackground;
572     }
573 
getColorItemContent()574     public int getColorItemContent() {
575         return mColorItemContent;
576     }
577 
getColorSeekbarProgress()578     public int getColorSeekbarProgress() {
579         return mColorSeekbarProgress;
580     }
581 
getColorButtonBackground()582     public int getColorButtonBackground() {
583         return mColorButtonBackground;
584     }
585 
getColorItemBackground()586     public int getColorItemBackground() {
587         return mColorItemBackground;
588     }
589 
getInactiveRadius()590     public float getInactiveRadius() {
591         return mInactiveRadius;
592     }
593 
getActiveRadius()594     public float getActiveRadius() {
595         return mActiveRadius;
596     }
597 
getItemMarginEndDefault()598     public int getItemMarginEndDefault() {
599         return mItemMarginEndDefault;
600     }
601 
getItemMarginEndSelectable()602     public int getItemMarginEndSelectable() {
603         return mItemMarginEndSelectable;
604     }
605 
buildMediaItems(List<MediaDevice> devices)606     private void buildMediaItems(List<MediaDevice> devices) {
607         synchronized (mMediaDevicesLock) {
608             if (!mLocalMediaManager.isPreferenceRouteListingExist()) {
609                 attachRangeInfo(devices);
610                 Collections.sort(devices, Comparator.naturalOrder());
611             }
612             // For the first time building list, to make sure the top device is the connected
613             // device.
614             boolean needToHandleMutingExpectedDevice =
615                     hasMutingExpectedDevice() && !isCurrentConnectedDeviceRemote();
616             final MediaDevice connectedMediaDevice =
617                     needToHandleMutingExpectedDevice ? null
618                             : getCurrentConnectedMediaDevice();
619             if (mMediaItemList.isEmpty()) {
620                 if (connectedMediaDevice == null) {
621                     if (DEBUG) {
622                         Log.d(TAG, "No connected media device or muting expected device exist.");
623                     }
624                     categorizeMediaItems(null, devices, needToHandleMutingExpectedDevice);
625                     return;
626                 }
627                 // selected device exist
628                 categorizeMediaItems(connectedMediaDevice, devices, false);
629                 return;
630             }
631             // To keep the same list order
632             final List<MediaDevice> targetMediaDevices = new ArrayList<>();
633             final Map<Integer, MediaItem> dividerItems = new HashMap<>();
634             for (MediaItem originalMediaItem : mMediaItemList) {
635                 for (MediaDevice newDevice : devices) {
636                     if (originalMediaItem.getMediaDevice().isPresent()
637                             && TextUtils.equals(originalMediaItem.getMediaDevice().get().getId(),
638                             newDevice.getId())) {
639                         targetMediaDevices.add(newDevice);
640                         break;
641                     }
642                 }
643                 if (originalMediaItem.getMediaItemType()
644                         == MediaItem.MediaItemType.TYPE_GROUP_DIVIDER) {
645                     dividerItems.put(mMediaItemList.indexOf(originalMediaItem), originalMediaItem);
646                 }
647             }
648             if (targetMediaDevices.size() != devices.size()) {
649                 devices.removeAll(targetMediaDevices);
650                 targetMediaDevices.addAll(devices);
651             }
652             List<MediaItem> finalMediaItems = targetMediaDevices.stream().map(
653                     MediaItem::new).collect(Collectors.toList());
654             dividerItems.forEach((key, item) -> {
655                 finalMediaItems.add(key, item);
656             });
657             attachConnectNewDeviceItemIfNeeded(finalMediaItems);
658             mMediaItemList.clear();
659             mMediaItemList.addAll(finalMediaItems);
660         }
661     }
662 
categorizeMediaItems(MediaDevice connectedMediaDevice, List<MediaDevice> devices, boolean needToHandleMutingExpectedDevice)663     private void categorizeMediaItems(MediaDevice connectedMediaDevice, List<MediaDevice> devices,
664             boolean needToHandleMutingExpectedDevice) {
665         synchronized (mMediaDevicesLock) {
666             List<MediaItem> finalMediaItems = new ArrayList<>();
667             Set<String> selectedDevicesIds = getSelectedMediaDevice().stream().map(
668                     MediaDevice::getId).collect(Collectors.toSet());
669             if (connectedMediaDevice != null) {
670                 selectedDevicesIds.add(connectedMediaDevice.getId());
671             }
672             boolean suggestedDeviceAdded = false;
673             boolean displayGroupAdded = false;
674             for (MediaDevice device : devices) {
675                 if (needToHandleMutingExpectedDevice && device.isMutingExpectedDevice()) {
676                     finalMediaItems.add(0, new MediaItem(device));
677                 } else if (!needToHandleMutingExpectedDevice && selectedDevicesIds.contains(
678                         device.getId())) {
679                     finalMediaItems.add(0, new MediaItem(device));
680                 } else {
681                     if (device.isSuggestedDevice() && !suggestedDeviceAdded) {
682                         attachGroupDivider(finalMediaItems, mContext.getString(
683                                 R.string.media_output_group_title_suggested_device));
684                         suggestedDeviceAdded = true;
685                     } else if (!device.isSuggestedDevice() && !displayGroupAdded) {
686                         attachGroupDivider(finalMediaItems, mContext.getString(
687                                 R.string.media_output_group_title_speakers_and_displays));
688                         displayGroupAdded = true;
689                     }
690                     finalMediaItems.add(new MediaItem(device));
691                 }
692             }
693             attachConnectNewDeviceItemIfNeeded(finalMediaItems);
694             mMediaItemList.clear();
695             mMediaItemList.addAll(finalMediaItems);
696         }
697     }
698 
attachGroupDivider(List<MediaItem> mediaItems, String title)699     private void attachGroupDivider(List<MediaItem> mediaItems, String title) {
700         mediaItems.add(
701                 new MediaItem(title, MediaItem.MediaItemType.TYPE_GROUP_DIVIDER));
702     }
703 
attachConnectNewDeviceItemIfNeeded(List<MediaItem> mediaItems)704     private void attachConnectNewDeviceItemIfNeeded(List<MediaItem> mediaItems) {
705         // Attach "Connect a device" item only when current output is not remote and not a group
706         if (!isCurrentConnectedDeviceRemote() && getSelectedMediaDevice().size() == 1) {
707             mediaItems.add(new MediaItem());
708         }
709     }
710 
attachRangeInfo(List<MediaDevice> devices)711     private void attachRangeInfo(List<MediaDevice> devices) {
712         for (MediaDevice mediaDevice : devices) {
713             if (mNearbyDeviceInfoMap.containsKey(mediaDevice.getId())) {
714                 mediaDevice.setRangeZone(mNearbyDeviceInfoMap.get(mediaDevice.getId()));
715             }
716         }
717 
718     }
719 
isCurrentConnectedDeviceRemote()720     boolean isCurrentConnectedDeviceRemote() {
721         MediaDevice currentConnectedMediaDevice = getCurrentConnectedMediaDevice();
722         return currentConnectedMediaDevice != null && isActiveRemoteDevice(
723                 currentConnectedMediaDevice);
724     }
725 
isCurrentOutputDeviceHasSessionOngoing()726     boolean isCurrentOutputDeviceHasSessionOngoing() {
727         MediaDevice currentConnectedMediaDevice = getCurrentConnectedMediaDevice();
728         return currentConnectedMediaDevice != null
729                 && (currentConnectedMediaDevice.isHostForOngoingSession());
730     }
731 
getGroupMediaDevices()732     List<MediaDevice> getGroupMediaDevices() {
733         final List<MediaDevice> selectedDevices = getSelectedMediaDevice();
734         final List<MediaDevice> selectableDevices = getSelectableMediaDevice();
735         if (mGroupMediaDevices.isEmpty()) {
736             mGroupMediaDevices.addAll(selectedDevices);
737             mGroupMediaDevices.addAll(selectableDevices);
738             return mGroupMediaDevices;
739         }
740         // To keep the same list order
741         final Collection<MediaDevice> sourceDevices = new ArrayList<>();
742         final Collection<MediaDevice> targetMediaDevices = new ArrayList<>();
743         sourceDevices.addAll(selectedDevices);
744         sourceDevices.addAll(selectableDevices);
745         for (MediaDevice originalDevice : mGroupMediaDevices) {
746             for (MediaDevice newDevice : sourceDevices) {
747                 if (TextUtils.equals(originalDevice.getId(), newDevice.getId())) {
748                     targetMediaDevices.add(newDevice);
749                     sourceDevices.remove(newDevice);
750                     break;
751                 }
752             }
753         }
754         // Add new devices at the end of list if necessary
755         if (!sourceDevices.isEmpty()) {
756             targetMediaDevices.addAll(sourceDevices);
757         }
758         mGroupMediaDevices.clear();
759         mGroupMediaDevices.addAll(targetMediaDevices);
760 
761         return mGroupMediaDevices;
762     }
763 
resetGroupMediaDevices()764     void resetGroupMediaDevices() {
765         mGroupMediaDevices.clear();
766     }
767 
connectDevice(MediaDevice device)768     void connectDevice(MediaDevice device) {
769         mMetricLogger.updateOutputEndPoints(getCurrentConnectedMediaDevice(), device);
770 
771         ThreadUtils.postOnBackgroundThread(() -> {
772             mLocalMediaManager.connectDevice(device);
773         });
774     }
775 
getMediaItemList()776     public List<MediaItem> getMediaItemList() {
777         return mMediaItemList;
778     }
779 
getCurrentConnectedMediaDevice()780     MediaDevice getCurrentConnectedMediaDevice() {
781         return mLocalMediaManager.getCurrentConnectedDevice();
782     }
783 
addDeviceToPlayMedia(MediaDevice device)784     boolean addDeviceToPlayMedia(MediaDevice device) {
785         mMetricLogger.logInteractionExpansion(device);
786         return mLocalMediaManager.addDeviceToPlayMedia(device);
787     }
788 
removeDeviceFromPlayMedia(MediaDevice device)789     boolean removeDeviceFromPlayMedia(MediaDevice device) {
790         return mLocalMediaManager.removeDeviceFromPlayMedia(device);
791     }
792 
getSelectableMediaDevice()793     List<MediaDevice> getSelectableMediaDevice() {
794         return mLocalMediaManager.getSelectableMediaDevice();
795     }
796 
getSelectedMediaDevice()797     List<MediaDevice> getSelectedMediaDevice() {
798         return mLocalMediaManager.getSelectedMediaDevice();
799     }
800 
getDeselectableMediaDevice()801     List<MediaDevice> getDeselectableMediaDevice() {
802         return mLocalMediaManager.getDeselectableMediaDevice();
803     }
804 
adjustSessionVolume(int volume)805     void adjustSessionVolume(int volume) {
806         mLocalMediaManager.adjustSessionVolume(volume);
807     }
808 
getSessionVolumeMax()809     int getSessionVolumeMax() {
810         return mLocalMediaManager.getSessionVolumeMax();
811     }
812 
getSessionVolume()813     int getSessionVolume() {
814         return mLocalMediaManager.getSessionVolume();
815     }
816 
getSessionName()817     CharSequence getSessionName() {
818         return mLocalMediaManager.getSessionName();
819     }
820 
releaseSession()821     void releaseSession() {
822         mMetricLogger.logInteractionStopCasting();
823         mLocalMediaManager.releaseSession();
824     }
825 
getActiveRemoteMediaDevices()826     List<RoutingSessionInfo> getActiveRemoteMediaDevices() {
827         final List<RoutingSessionInfo> sessionInfos = new ArrayList<>();
828         for (RoutingSessionInfo info : mLocalMediaManager.getActiveMediaSession()) {
829             if (!info.isSystemSession()) {
830                 sessionInfos.add(info);
831             }
832         }
833         return sessionInfos;
834     }
835 
adjustVolume(MediaDevice device, int volume)836     void adjustVolume(MediaDevice device, int volume) {
837         ThreadUtils.postOnBackgroundThread(() -> {
838             device.requestSetVolume(volume);
839         });
840     }
841 
logInteractionAdjustVolume(MediaDevice device)842     void logInteractionAdjustVolume(MediaDevice device) {
843         mMetricLogger.logInteractionAdjustVolume(device);
844     }
845 
logInteractionMuteDevice(MediaDevice device)846     void logInteractionMuteDevice(MediaDevice device) {
847         mMetricLogger.logInteractionMute(device);
848     }
849 
logInteractionUnmuteDevice(MediaDevice device)850     void logInteractionUnmuteDevice(MediaDevice device) {
851         mMetricLogger.logInteractionUnmute(device);
852     }
853 
getPackageName()854     String getPackageName() {
855         return mPackageName;
856     }
857 
hasAdjustVolumeUserRestriction()858     boolean hasAdjustVolumeUserRestriction() {
859         if (RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
860                 mContext, UserManager.DISALLOW_ADJUST_VOLUME, UserHandle.myUserId()) != null) {
861             return true;
862         }
863         final UserManager um = mContext.getSystemService(UserManager.class);
864         return um.hasBaseUserRestriction(UserManager.DISALLOW_ADJUST_VOLUME,
865                 UserHandle.of(UserHandle.myUserId()));
866     }
867 
isAnyDeviceTransferring()868     boolean isAnyDeviceTransferring() {
869         synchronized (mMediaDevicesLock) {
870             for (MediaItem mediaItem : mMediaItemList) {
871                 if (mediaItem.getMediaDevice().isPresent()
872                         && mediaItem.getMediaDevice().get().getState()
873                         == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) {
874                     return true;
875                 }
876             }
877         }
878         return false;
879     }
880 
launchBluetoothPairing(View view)881     void launchBluetoothPairing(View view) {
882         ActivityLaunchAnimator.Controller controller =
883                 mDialogLaunchAnimator.createActivityLaunchController(view);
884 
885         if (controller == null || (mKeyGuardManager != null
886                 && mKeyGuardManager.isKeyguardLocked())) {
887             mCallback.dismissDialog();
888         }
889 
890         Intent launchIntent =
891                 new Intent(ACTION_BLUETOOTH_SETTINGS)
892                         .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
893         final Intent deepLinkIntent =
894                 new Intent(Settings.ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY);
895         if (deepLinkIntent.resolveActivity(mContext.getPackageManager()) != null) {
896             Log.d(TAG, "Device support split mode, launch page with deep link");
897             deepLinkIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
898             deepLinkIntent.putExtra(
899                     Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI,
900                     launchIntent.toUri(Intent.URI_INTENT_SCHEME));
901             deepLinkIntent.putExtra(
902                     Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY,
903                     PAGE_CONNECTED_DEVICES_KEY);
904             mActivityStarter.startActivity(deepLinkIntent, true, controller);
905             return;
906         }
907         mActivityStarter.startActivity(launchIntent, true, controller);
908     }
909 
launchLeBroadcastNotifyDialog(View mediaOutputDialog, BroadcastSender broadcastSender, BroadcastNotifyDialog action, final DialogInterface.OnClickListener listener)910     void launchLeBroadcastNotifyDialog(View mediaOutputDialog, BroadcastSender broadcastSender,
911             BroadcastNotifyDialog action, final DialogInterface.OnClickListener listener) {
912         final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
913         switch (action) {
914             case ACTION_FIRST_LAUNCH:
915                 builder.setTitle(R.string.media_output_first_broadcast_title);
916                 builder.setMessage(R.string.media_output_first_notify_broadcast_message);
917                 builder.setNegativeButton(android.R.string.cancel, null);
918                 builder.setPositiveButton(R.string.media_output_broadcast, listener);
919                 break;
920             case ACTION_BROADCAST_INFO_ICON:
921                 builder.setTitle(R.string.media_output_broadcast);
922                 builder.setMessage(R.string.media_output_broadcasting_message);
923                 builder.setPositiveButton(android.R.string.ok, null);
924                 break;
925         }
926 
927         final AlertDialog dialog = builder.create();
928         dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
929         SystemUIDialog.setShowForAllUsers(dialog, true);
930         SystemUIDialog.registerDismissListener(dialog);
931         dialog.show();
932     }
933 
launchMediaOutputBroadcastDialog(View mediaOutputDialog, BroadcastSender broadcastSender)934     void launchMediaOutputBroadcastDialog(View mediaOutputDialog, BroadcastSender broadcastSender) {
935         MediaOutputController controller = new MediaOutputController(mContext, mPackageName,
936                 mMediaSessionManager, mLocalBluetoothManager, mActivityStarter,
937                 mNotifCollection, mDialogLaunchAnimator, mNearbyMediaDevicesManager,
938                 mAudioManager, mPowerExemptionManager, mKeyGuardManager, mFeatureFlags,
939                 mUserTracker);
940         MediaOutputBroadcastDialog dialog = new MediaOutputBroadcastDialog(mContext, true,
941                 broadcastSender, controller);
942         dialog.show();
943     }
944 
getBroadcastName()945     String getBroadcastName() {
946         LocalBluetoothLeBroadcast broadcast =
947                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
948         if (broadcast == null) {
949             Log.d(TAG, "getBroadcastName: LE Audio Broadcast is null");
950             return "";
951         }
952         return broadcast.getProgramInfo();
953     }
954 
setBroadcastName(String broadcastName)955     void setBroadcastName(String broadcastName) {
956         LocalBluetoothLeBroadcast broadcast =
957                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
958         if (broadcast == null) {
959             Log.d(TAG, "setBroadcastName: LE Audio Broadcast is null");
960             return;
961         }
962         broadcast.setProgramInfo(broadcastName);
963     }
964 
getBroadcastCode()965     String getBroadcastCode() {
966         LocalBluetoothLeBroadcast broadcast =
967                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
968         if (broadcast == null) {
969             Log.d(TAG, "getBroadcastCode: LE Audio Broadcast is null");
970             return "";
971         }
972         return new String(broadcast.getBroadcastCode(), StandardCharsets.UTF_8);
973     }
974 
setBroadcastCode(String broadcastCode)975     void setBroadcastCode(String broadcastCode) {
976         LocalBluetoothLeBroadcast broadcast =
977                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
978         if (broadcast == null) {
979             Log.d(TAG, "setBroadcastCode: LE Audio Broadcast is null");
980             return;
981         }
982         broadcast.setBroadcastCode(broadcastCode.getBytes(StandardCharsets.UTF_8));
983     }
984 
setTemporaryAllowListExceptionIfNeeded(MediaDevice targetDevice)985     void setTemporaryAllowListExceptionIfNeeded(MediaDevice targetDevice) {
986         if (mPowerExemptionManager == null || mPackageName == null) {
987             Log.w(TAG, "powerExemptionManager or package name is null");
988             return;
989         }
990         mPowerExemptionManager.addToTemporaryAllowList(mPackageName,
991                 PowerExemptionManager.REASON_MEDIA_NOTIFICATION_TRANSFER,
992                 ALLOWLIST_REASON,
993                 ALLOWLIST_DURATION_MS);
994     }
995 
getLocalBroadcastMetadataQrCodeString()996     String getLocalBroadcastMetadataQrCodeString() {
997         LocalBluetoothLeBroadcast broadcast =
998                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
999         if (broadcast == null) {
1000             Log.d(TAG, "getLocalBroadcastMetadataQrCodeString: LE Audio Broadcast is null");
1001             return "";
1002         }
1003         final LocalBluetoothLeBroadcastMetadata metadata =
1004                 broadcast.getLocalBluetoothLeBroadcastMetaData();
1005         return metadata != null ? metadata.convertToQrCodeString() : "";
1006     }
1007 
getBroadcastMetadata()1008     BluetoothLeBroadcastMetadata getBroadcastMetadata() {
1009         LocalBluetoothLeBroadcast broadcast =
1010                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
1011         if (broadcast == null) {
1012             Log.d(TAG, "getBroadcastMetadata: LE Audio Broadcast is null");
1013             return null;
1014         }
1015 
1016         return broadcast.getLatestBluetoothLeBroadcastMetadata();
1017     }
1018 
isActiveRemoteDevice(@onNull MediaDevice device)1019     boolean isActiveRemoteDevice(@NonNull MediaDevice device) {
1020         final List<String> features = device.getFeatures();
1021         return (features.contains(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK)
1022                 || features.contains(MediaRoute2Info.FEATURE_REMOTE_AUDIO_PLAYBACK)
1023                 || features.contains(MediaRoute2Info.FEATURE_REMOTE_VIDEO_PLAYBACK)
1024                 || features.contains(MediaRoute2Info.FEATURE_REMOTE_GROUP_PLAYBACK));
1025     }
1026 
isBluetoothLeDevice(@onNull MediaDevice device)1027     boolean isBluetoothLeDevice(@NonNull MediaDevice device) {
1028         return device.isBLEDevice();
1029     }
1030 
isBroadcastSupported()1031     boolean isBroadcastSupported() {
1032         LocalBluetoothLeBroadcast broadcast =
1033                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
1034         return broadcast != null ? true : false;
1035     }
1036 
isBluetoothLeBroadcastEnabled()1037     boolean isBluetoothLeBroadcastEnabled() {
1038         LocalBluetoothLeBroadcast broadcast =
1039                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
1040         if (broadcast == null) {
1041             return false;
1042         }
1043         return broadcast.isEnabled(null);
1044     }
1045 
startBluetoothLeBroadcast()1046     boolean startBluetoothLeBroadcast() {
1047         LocalBluetoothLeBroadcast broadcast =
1048                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
1049         if (broadcast == null) {
1050             Log.d(TAG, "The broadcast profile is null");
1051             return false;
1052         }
1053         broadcast.startBroadcast(getAppSourceName(), /*language*/ null);
1054         return true;
1055     }
1056 
stopBluetoothLeBroadcast()1057     boolean stopBluetoothLeBroadcast() {
1058         LocalBluetoothLeBroadcast broadcast =
1059                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
1060         if (broadcast == null) {
1061             Log.d(TAG, "The broadcast profile is null");
1062             return false;
1063         }
1064         broadcast.stopLatestBroadcast();
1065         return true;
1066     }
1067 
updateBluetoothLeBroadcast()1068     boolean updateBluetoothLeBroadcast() {
1069         LocalBluetoothLeBroadcast broadcast =
1070                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
1071         if (broadcast == null) {
1072             Log.d(TAG, "The broadcast profile is null");
1073             return false;
1074         }
1075         broadcast.updateBroadcast(getAppSourceName(), /*language*/ null);
1076         return true;
1077     }
1078 
registerLeBroadcastServiceCallback( @onNull @allbackExecutor Executor executor, @NonNull BluetoothLeBroadcast.Callback callback)1079     void registerLeBroadcastServiceCallback(
1080             @NonNull @CallbackExecutor Executor executor,
1081             @NonNull BluetoothLeBroadcast.Callback callback) {
1082         LocalBluetoothLeBroadcast broadcast =
1083                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
1084         if (broadcast == null) {
1085             Log.d(TAG, "The broadcast profile is null");
1086             return;
1087         }
1088         Log.d(TAG, "Register LE broadcast callback");
1089         broadcast.registerServiceCallBack(executor, callback);
1090     }
1091 
unregisterLeBroadcastServiceCallback( @onNull BluetoothLeBroadcast.Callback callback)1092     void unregisterLeBroadcastServiceCallback(
1093             @NonNull BluetoothLeBroadcast.Callback callback) {
1094         LocalBluetoothLeBroadcast broadcast =
1095                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
1096         if (broadcast == null) {
1097             Log.d(TAG, "The broadcast profile is null");
1098             return;
1099         }
1100         Log.d(TAG, "Unregister LE broadcast callback");
1101         broadcast.unregisterServiceCallBack(callback);
1102     }
1103 
getConnectedBroadcastSinkDevices()1104     List<BluetoothDevice> getConnectedBroadcastSinkDevices() {
1105         LocalBluetoothLeBroadcastAssistant assistant =
1106                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
1107         if (assistant == null) {
1108             Log.d(TAG, "getConnectedBroadcastSinkDevices: The broadcast assistant profile is null");
1109             return null;
1110         }
1111 
1112         return assistant.getConnectedDevices();
1113     }
1114 
isThereAnyBroadcastSourceIntoSinkDevice(BluetoothDevice sink)1115     boolean isThereAnyBroadcastSourceIntoSinkDevice(BluetoothDevice sink) {
1116         LocalBluetoothLeBroadcastAssistant assistant =
1117                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
1118         if (assistant == null) {
1119             Log.d(TAG, "isThereAnyBroadcastSourceIntoSinkDevice: The broadcast assistant profile "
1120                     + "is null");
1121             return false;
1122         }
1123         List<BluetoothLeBroadcastReceiveState> sourceList = assistant.getAllSources(sink);
1124         Log.d(TAG, "isThereAnyBroadcastSourceIntoSinkDevice: List size: " + sourceList.size());
1125         return !sourceList.isEmpty();
1126     }
1127 
addSourceIntoSinkDeviceWithBluetoothLeAssistant(BluetoothDevice sink, BluetoothLeBroadcastMetadata metadata, boolean isGroupOp)1128     boolean addSourceIntoSinkDeviceWithBluetoothLeAssistant(BluetoothDevice sink,
1129             BluetoothLeBroadcastMetadata metadata, boolean isGroupOp) {
1130         LocalBluetoothLeBroadcastAssistant assistant =
1131                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
1132         if (assistant == null) {
1133             Log.d(TAG, "addSourceIntoSinkDeviceWithBluetoothLeAssistant: The broadcast assistant "
1134                     + "profile is null");
1135             return false;
1136         }
1137         assistant.addSource(sink, metadata, isGroupOp);
1138         return true;
1139     }
1140 
registerLeBroadcastAssistantServiceCallback( @onNull @allbackExecutor Executor executor, @NonNull BluetoothLeBroadcastAssistant.Callback callback)1141     void registerLeBroadcastAssistantServiceCallback(
1142             @NonNull @CallbackExecutor Executor executor,
1143             @NonNull BluetoothLeBroadcastAssistant.Callback callback) {
1144         LocalBluetoothLeBroadcastAssistant assistant =
1145                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
1146         if (assistant == null) {
1147             Log.d(TAG, "registerLeBroadcastAssistantServiceCallback: The broadcast assistant "
1148                     + "profile is null");
1149             return;
1150         }
1151         Log.d(TAG, "Register LE broadcast assistant callback");
1152         assistant.registerServiceCallBack(executor, callback);
1153     }
1154 
unregisterLeBroadcastAssistantServiceCallback( @onNull BluetoothLeBroadcastAssistant.Callback callback)1155     void unregisterLeBroadcastAssistantServiceCallback(
1156             @NonNull BluetoothLeBroadcastAssistant.Callback callback) {
1157         LocalBluetoothLeBroadcastAssistant assistant =
1158                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
1159         if (assistant == null) {
1160             Log.d(TAG, "unregisterLeBroadcastAssistantServiceCallback: The broadcast assistant "
1161                     + "profile is null");
1162             return;
1163         }
1164         Log.d(TAG, "Unregister LE broadcast assistant callback");
1165         assistant.unregisterServiceCallBack(callback);
1166     }
1167 
isPlayBackInfoLocal()1168     private boolean isPlayBackInfoLocal() {
1169         return mMediaController != null
1170                 && mMediaController.getPlaybackInfo() != null
1171                 && mMediaController.getPlaybackInfo().getPlaybackType()
1172                 == MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL;
1173     }
1174 
isPlaying()1175     boolean isPlaying() {
1176         if (mMediaController == null) {
1177             return false;
1178         }
1179 
1180         PlaybackState state = mMediaController.getPlaybackState();
1181         if (state == null) {
1182             return false;
1183         }
1184 
1185         return (state.getState() == PlaybackState.STATE_PLAYING);
1186     }
1187 
isVolumeControlEnabled(@onNull MediaDevice device)1188     boolean isVolumeControlEnabled(@NonNull MediaDevice device) {
1189         return (isPlayBackInfoLocal()
1190                 || device.getDeviceType() != MediaDevice.MediaDeviceType.TYPE_CAST_GROUP_DEVICE)
1191                 && !device.isVolumeFixed();
1192     }
1193 
1194     @Override
onDevicesUpdated(List<NearbyDevice> nearbyDevices)1195     public void onDevicesUpdated(List<NearbyDevice> nearbyDevices) throws RemoteException {
1196         mNearbyDeviceInfoMap.clear();
1197         for (NearbyDevice nearbyDevice : nearbyDevices) {
1198             mNearbyDeviceInfoMap.put(nearbyDevice.getMediaRoute2Id(), nearbyDevice.getRangeZone());
1199         }
1200         mNearbyMediaDevicesManager.unregisterNearbyDevicesCallback(this);
1201     }
1202 
1203     @Override
asBinder()1204     public IBinder asBinder() {
1205         return null;
1206     }
1207 
1208     @VisibleForTesting
1209     final MediaController.Callback mCb = new MediaController.Callback() {
1210         @Override
1211         public void onMetadataChanged(MediaMetadata metadata) {
1212             mCallback.onMediaChanged();
1213         }
1214 
1215         @Override
1216         public void onPlaybackStateChanged(PlaybackState playbackState) {
1217             final int newState =
1218                     playbackState == null ? PlaybackState.STATE_STOPPED : playbackState.getState();
1219             if (mCurrentState == newState) {
1220                 return;
1221             }
1222 
1223             if (newState == PlaybackState.STATE_STOPPED) {
1224                 mCallback.onMediaStoppedOrPaused();
1225             }
1226             mCurrentState = newState;
1227         }
1228     };
1229 
1230     interface Callback {
1231         /**
1232          * Override to handle the media content updating.
1233          */
onMediaChanged()1234         void onMediaChanged();
1235 
1236         /**
1237          * Override to handle the media state updating.
1238          */
onMediaStoppedOrPaused()1239         void onMediaStoppedOrPaused();
1240 
1241         /**
1242          * Override to handle the device status or attributes updating.
1243          */
onRouteChanged()1244         void onRouteChanged();
1245 
1246         /**
1247          * Override to handle the devices set updating.
1248          */
onDeviceListChanged()1249         void onDeviceListChanged();
1250 
1251         /**
1252          * Override to dismiss dialog.
1253          */
dismissDialog()1254         void dismissDialog();
1255     }
1256 }
1257