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