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