/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.app; import android.Manifest; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; import android.annotation.UserIdInt; import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.compat.annotation.LoggingOnly; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.drawable.Icon; import android.media.INearbyMediaDevicesProvider; import android.media.INearbyMediaDevicesUpdateCallback; import android.media.MediaRoute2Info; import android.media.NearbyDevice; import android.media.NearbyMediaDevicesProvider; import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.util.Pair; import android.util.Slog; import android.view.KeyEvent; import android.view.View; import com.android.internal.compat.IPlatformCompat; import com.android.internal.statusbar.AppClipsServiceConnector; import com.android.internal.statusbar.IAddTileResultCallback; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.IUndoMediaTransferCallback; import com.android.internal.statusbar.NotificationVisibility; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Consumer; /** * Allows an app to control the status bar. */ @SystemService(Context.STATUS_BAR_SERVICE) public class StatusBarManager { // LINT.IfChange /** @hide */ public static final int DISABLE_EXPAND = View.STATUS_BAR_DISABLE_EXPAND; /** @hide */ public static final int DISABLE_NOTIFICATION_ICONS = View.STATUS_BAR_DISABLE_NOTIFICATION_ICONS; /** @hide */ public static final int DISABLE_NOTIFICATION_ALERTS = View.STATUS_BAR_DISABLE_NOTIFICATION_ALERTS; /** @hide */ @Deprecated @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public static final int DISABLE_NOTIFICATION_TICKER = View.STATUS_BAR_DISABLE_NOTIFICATION_TICKER; /** @hide */ public static final int DISABLE_SYSTEM_INFO = View.STATUS_BAR_DISABLE_SYSTEM_INFO; /** @hide */ public static final int DISABLE_HOME = View.STATUS_BAR_DISABLE_HOME; /** @hide */ public static final int DISABLE_RECENT = View.STATUS_BAR_DISABLE_RECENT; /** @hide */ public static final int DISABLE_BACK = View.STATUS_BAR_DISABLE_BACK; /** @hide */ public static final int DISABLE_CLOCK = View.STATUS_BAR_DISABLE_CLOCK; /** @hide */ public static final int DISABLE_SEARCH = View.STATUS_BAR_DISABLE_SEARCH; /** @hide */ public static final int DISABLE_ONGOING_CALL_CHIP = View.STATUS_BAR_DISABLE_ONGOING_CALL_CHIP; /** @hide */ @Deprecated public static final int DISABLE_NAVIGATION = View.STATUS_BAR_DISABLE_HOME | View.STATUS_BAR_DISABLE_RECENT; /** @hide */ public static final int DISABLE_NONE = 0x00000000; /** @hide */ public static final int DISABLE_MASK = DISABLE_EXPAND | DISABLE_NOTIFICATION_ICONS | DISABLE_NOTIFICATION_ALERTS | DISABLE_NOTIFICATION_TICKER | DISABLE_SYSTEM_INFO | DISABLE_RECENT | DISABLE_HOME | DISABLE_BACK | DISABLE_CLOCK | DISABLE_SEARCH | DISABLE_ONGOING_CALL_CHIP; /** @hide */ @IntDef(flag = true, prefix = {"DISABLE_"}, value = { DISABLE_NONE, DISABLE_EXPAND, DISABLE_NOTIFICATION_ICONS, DISABLE_NOTIFICATION_ALERTS, DISABLE_NOTIFICATION_TICKER, DISABLE_SYSTEM_INFO, DISABLE_HOME, DISABLE_RECENT, DISABLE_BACK, DISABLE_CLOCK, DISABLE_SEARCH, DISABLE_ONGOING_CALL_CHIP }) @Retention(RetentionPolicy.SOURCE) public @interface DisableFlags {} /** * Flag to disable quick settings. * * Setting this flag disables quick settings completely, but does not disable expanding the * notification shade. */ /** @hide */ public static final int DISABLE2_QUICK_SETTINGS = 1; /** @hide */ public static final int DISABLE2_SYSTEM_ICONS = 1 << 1; /** @hide */ public static final int DISABLE2_NOTIFICATION_SHADE = 1 << 2; /** @hide */ public static final int DISABLE2_GLOBAL_ACTIONS = 1 << 3; /** @hide */ public static final int DISABLE2_ROTATE_SUGGESTIONS = 1 << 4; /** @hide */ public static final int DISABLE2_NONE = 0x00000000; /** @hide */ public static final int DISABLE2_MASK = DISABLE2_QUICK_SETTINGS | DISABLE2_SYSTEM_ICONS | DISABLE2_NOTIFICATION_SHADE | DISABLE2_GLOBAL_ACTIONS | DISABLE2_ROTATE_SUGGESTIONS; /** @hide */ @IntDef(flag = true, prefix = { "DISABLE2_" }, value = { DISABLE2_NONE, DISABLE2_MASK, DISABLE2_QUICK_SETTINGS, DISABLE2_SYSTEM_ICONS, DISABLE2_NOTIFICATION_SHADE, DISABLE2_GLOBAL_ACTIONS, DISABLE2_ROTATE_SUGGESTIONS }) @Retention(RetentionPolicy.SOURCE) public @interface Disable2Flags {} // LINT.ThenChange(frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/DisableFlagsLogger.kt) private static final String TAG = "StatusBarManager"; /** * Default disable flags for setup * * @hide */ public static final int DEFAULT_SETUP_DISABLE_FLAGS = DISABLE_NOTIFICATION_ALERTS | DISABLE_HOME | DISABLE_EXPAND | DISABLE_RECENT | DISABLE_CLOCK | DISABLE_SEARCH; /** * Default disable2 flags for setup * * @hide */ public static final int DEFAULT_SETUP_DISABLE2_FLAGS = DISABLE2_NONE; /** * disable flags to be applied when the device is sim-locked. */ private static final int DEFAULT_SIM_LOCKED_DISABLED_FLAGS = DISABLE_EXPAND; /** @hide */ public static final int NAVIGATION_HINT_BACK_ALT = 1 << 0; /** @hide */ public static final int NAVIGATION_HINT_IME_SHOWN = 1 << 1; /** @hide */ public static final int NAVIGATION_HINT_IME_SWITCHER_SHOWN = 1 << 2; /** @hide */ public static final int WINDOW_STATUS_BAR = 1; /** @hide */ public static final int WINDOW_NAVIGATION_BAR = 2; /** @hide */ @IntDef(flag = true, prefix = { "WINDOW_" }, value = { WINDOW_STATUS_BAR, WINDOW_NAVIGATION_BAR }) @Retention(RetentionPolicy.SOURCE) public @interface WindowType {} /** @hide */ public static final int WINDOW_STATE_SHOWING = 0; /** @hide */ public static final int WINDOW_STATE_HIDING = 1; /** @hide */ public static final int WINDOW_STATE_HIDDEN = 2; /** @hide */ @IntDef(flag = true, prefix = { "WINDOW_STATE_" }, value = { WINDOW_STATE_SHOWING, WINDOW_STATE_HIDING, WINDOW_STATE_HIDDEN }) @Retention(RetentionPolicy.SOURCE) public @interface WindowVisibleState {} /** @hide */ public static final int CAMERA_LAUNCH_SOURCE_WIGGLE = 0; /** @hide */ public static final int CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP = 1; /** @hide */ public static final int CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER = 2; /** @hide */ public static final int CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE = 3; /** * Broadcast action: sent to apps that hold the status bar permission when * KeyguardManager#setPrivateNotificationsAllowed() is changed. * * Extras: #EXTRA_KM_PRIVATE_NOTIFS_ALLOWED * @hide */ public static final String ACTION_KEYGUARD_PRIVATE_NOTIFICATIONS_CHANGED = "android.app.action.KEYGUARD_PRIVATE_NOTIFICATIONS_CHANGED"; /** * Boolean, the latest value of KeyguardManager#getPrivateNotificationsAllowed() * @hide */ public static final String EXTRA_KM_PRIVATE_NOTIFS_ALLOWED = "android.app.extra.KM_PRIVATE_NOTIFS_ALLOWED"; /** * Session flag for {@link #registerSessionListener} indicating the listener * is interested in sessions on the keygaurd. * Keyguard Session Boundaries: * START_SESSION: device starts going to sleep OR the keyguard is newly shown * END_SESSION: device starts going to sleep OR keyguard is no longer showing * @hide */ public static final int SESSION_KEYGUARD = 1 << 0; /** * Session flag for {@link #registerSessionListener} indicating the current session * is interested in session on the biometric prompt. * @hide */ public static final int SESSION_BIOMETRIC_PROMPT = 1 << 1; /** @hide */ public static final Set ALL_SESSIONS = Set.of( SESSION_KEYGUARD, SESSION_BIOMETRIC_PROMPT ); /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, prefix = { "SESSION_KEYGUARD" }, value = { SESSION_KEYGUARD, SESSION_BIOMETRIC_PROMPT, }) public @interface SessionFlags {} /** * Response indicating that the tile was not added. */ public static final int TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED = 0; /** * Response indicating that the tile was already added and the user was not prompted. */ public static final int TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED = 1; /** * Response indicating that the tile was added. */ public static final int TILE_ADD_REQUEST_RESULT_TILE_ADDED = 2; /** @hide */ public static final int TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED = 3; /** * Values greater or equal to this value indicate an error in the request. */ private static final int TILE_ADD_REQUEST_FIRST_ERROR_CODE = 1000; /** * Indicates that this package does not match that of the * {@link android.service.quicksettings.TileService}. */ public static final int TILE_ADD_REQUEST_ERROR_MISMATCHED_PACKAGE = TILE_ADD_REQUEST_FIRST_ERROR_CODE; /** * Indicates that there's a request in progress for this package. */ public static final int TILE_ADD_REQUEST_ERROR_REQUEST_IN_PROGRESS = TILE_ADD_REQUEST_FIRST_ERROR_CODE + 1; /** * Indicates that the component does not match an enabled exported * {@link android.service.quicksettings.TileService} for the current user. */ public static final int TILE_ADD_REQUEST_ERROR_BAD_COMPONENT = TILE_ADD_REQUEST_FIRST_ERROR_CODE + 2; /** * Indicates that the user is not the current user. */ public static final int TILE_ADD_REQUEST_ERROR_NOT_CURRENT_USER = TILE_ADD_REQUEST_FIRST_ERROR_CODE + 3; /** * Indicates that the requesting application is not in the foreground. */ public static final int TILE_ADD_REQUEST_ERROR_APP_NOT_IN_FOREGROUND = TILE_ADD_REQUEST_FIRST_ERROR_CODE + 4; /** * The request could not be processed because no fulfilling service was found. This could be * a temporary issue (for example, SystemUI has crashed). */ public static final int TILE_ADD_REQUEST_ERROR_NO_STATUS_BAR_SERVICE = TILE_ADD_REQUEST_FIRST_ERROR_CODE + 5; /** @hide */ @IntDef(prefix = {"TILE_ADD_REQUEST"}, value = { TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED, TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED, TILE_ADD_REQUEST_RESULT_TILE_ADDED, TILE_ADD_REQUEST_ERROR_MISMATCHED_PACKAGE, TILE_ADD_REQUEST_ERROR_REQUEST_IN_PROGRESS, TILE_ADD_REQUEST_ERROR_BAD_COMPONENT, TILE_ADD_REQUEST_ERROR_NOT_CURRENT_USER, TILE_ADD_REQUEST_ERROR_APP_NOT_IN_FOREGROUND, TILE_ADD_REQUEST_ERROR_NO_STATUS_BAR_SERVICE }) @Retention(RetentionPolicy.SOURCE) public @interface RequestResult {} /** * Constant for {@link #setNavBarMode(int)} indicating the default navbar mode. * * @hide */ @SystemApi public static final int NAV_BAR_MODE_DEFAULT = 0; /** * Constant for {@link #setNavBarMode(int)} indicating kids navbar mode. * *

When used, back and home icons will change drawables and layout, recents will be hidden, * and enables the setting to force navbar visible, even when apps are in immersive mode. * * @hide */ @SystemApi public static final int NAV_BAR_MODE_KIDS = 1; /** @hide */ @IntDef(prefix = {"NAV_BAR_MODE_"}, value = { NAV_BAR_MODE_DEFAULT, NAV_BAR_MODE_KIDS }) @Retention(RetentionPolicy.SOURCE) public @interface NavBarMode {} /** * State indicating that this sender device is close to a receiver device, so the user can * potentially *start* a cast to the receiver device if the user moves their device a bit * closer. *

* Important notes: *

* * @hide */ @SystemApi public static final int MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST = 0; /** * State indicating that this sender device is close to a receiver device, so the user can * potentially *end* a cast on the receiver device if the user moves this device a bit closer. *

* Important notes: *

* * @hide */ @SystemApi public static final int MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST = 1; /** * State indicating that a media transfer from this sender device to a receiver device has been * started. *

* Important note: This state is for *starting* a cast. It should be used when this device is * currently playing media locally and the media has started being transferred to the receiver * device instead. * * @hide */ @SystemApi public static final int MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED = 2; /** * State indicating that a media transfer from the receiver and back to this sender device * has been started. *

* Important note: This state is for *ending* a cast. It should be used when media is currently * being played on the receiver device and the media has started being transferred to play * locally instead. * * @hide */ @SystemApi public static final int MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED = 3; /** * State indicating that a media transfer from this sender device to a receiver device has * finished successfully. *

* Important note: This state is for *starting* a cast. It should be used when this device had * previously been playing media locally and the media has successfully been transferred to the * receiver device instead. * * @hide */ @SystemApi public static final int MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED = 4; /** * State indicating that a media transfer from the receiver and back to this sender device has * finished successfully. *

* Important note: This state is for *ending* a cast. It should be used when media was * previously being played on the receiver device and has been successfully transferred to play * locally on this device instead. * * @hide */ @SystemApi public static final int MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED = 5; /** * State indicating that the attempted transfer to the receiver device has failed. * * @hide */ @SystemApi public static final int MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_FAILED = 6; /** * State indicating that the attempted transfer back to this device has failed. * * @hide */ @SystemApi public static final int MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_FAILED = 7; /** * State indicating that this sender device is no longer close to the receiver device. * * @hide */ @SystemApi public static final int MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER = 8; /** @hide */ @IntDef(prefix = {"MEDIA_TRANSFER_SENDER_STATE_"}, value = { MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST, MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED, MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED, MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_FAILED, MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_FAILED, MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, }) @Retention(RetentionPolicy.SOURCE) public @interface MediaTransferSenderState {} /** * State indicating that this receiver device is close to a sender device, so the user can * potentially start or end a cast to the receiver device if the user moves the sender device a * bit closer. *

* Important note: This state represents that the device is close enough to inform the user that * transferring is an option, but the device is *not* close enough to actually initiate a * transfer yet. * * @hide */ @SystemApi public static final int MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER = 0; /** * State indicating that this receiver device is no longer close to the sender device. * * @hide */ @SystemApi public static final int MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER = 1; /** * State indicating that media transfer to this receiver device is succeeded. * * @hide */ @SystemApi public static final int MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED = 2; /** * State indicating that media transfer to this receiver device is failed. * * @hide */ @SystemApi public static final int MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_FAILED = 3; /** @hide */ @IntDef(prefix = {"MEDIA_TRANSFER_RECEIVER_STATE_"}, value = { MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER, MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER, MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_FAILED, }) @Retention(RetentionPolicy.SOURCE) public @interface MediaTransferReceiverState {} /** * A map from a provider registered in * {@link #registerNearbyMediaDevicesProvider(NearbyMediaDevicesProvider)} to the wrapper * around the provider that was created internally. We need the wrapper to make the provider * binder-compatible, and we need to store a reference to the wrapper so that when the provider * is un-registered, we un-register the saved wrapper instance. */ private final Map nearbyMediaDevicesProviderMap = new HashMap<>(); /** * Media controls based on {@link android.app.Notification.MediaStyle} notifications will have * actions based on the media session's {@link android.media.session.PlaybackState}, rather than * the notification's actions. * * These actions will be: * - Play/Pause (depending on whether the current state is a playing state) * - Previous (if declared), or a custom action if the slot is not reserved with * {@code SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV} * - Next (if declared), or a custom action if the slot is not reserved with * {@code SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT} * - Custom action * - Custom action * * @see androidx.media.utils.MediaConstants#SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV * @see androidx.media.utils.MediaConstants#SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT */ @ChangeId @EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) private static final long MEDIA_CONTROL_SESSION_ACTIONS = 203800354L; /** * Media controls based on {@link android.app.Notification.MediaStyle} notifications should * include a non-empty title, either in the {@link android.media.MediaMetadata} or * notification title. */ @ChangeId @LoggingOnly private static final long MEDIA_CONTROL_BLANK_TITLE = 274775190L; @UnsupportedAppUsage private Context mContext; private IStatusBarService mService; @UnsupportedAppUsage private IBinder mToken = new Binder(); private final IPlatformCompat mPlatformCompat = IPlatformCompat.Stub.asInterface( ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)); @UnsupportedAppUsage StatusBarManager(Context context) { mContext = context; } @UnsupportedAppUsage private synchronized IStatusBarService getService() { if (mService == null) { mService = IStatusBarService.Stub.asInterface( ServiceManager.getService(Context.STATUS_BAR_SERVICE)); if (mService == null) { Slog.w(TAG, "warning: no STATUS_BAR_SERVICE"); } } return mService; } /** * Disable some features in the status bar. Pass the bitwise-or of the DISABLE_* flags. * To re-enable everything, pass {@link #DISABLE_NONE}. * * @hide */ @UnsupportedAppUsage public void disable(int what) { try { final int userId = Binder.getCallingUserHandle().getIdentifier(); final IStatusBarService svc = getService(); if (svc != null) { svc.disableForUser(what, mToken, mContext.getPackageName(), userId); } } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } /** * Disable additional status bar features. Pass the bitwise-or of the DISABLE2_* flags. * To re-enable everything, pass {@link #DISABLE_NONE}. * * Warning: Only pass DISABLE2_* flags into this function, do not use DISABLE_* flags. * * @hide */ public void disable2(@Disable2Flags int what) { try { final int userId = Binder.getCallingUserHandle().getIdentifier(); final IStatusBarService svc = getService(); if (svc != null) { svc.disable2ForUser(what, mToken, mContext.getPackageName(), userId); } } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } /** * Simulate notification click for testing * * @hide */ @TestApi public void clickNotification(@Nullable String key, int rank, int count, boolean visible) { clickNotificationInternal(key, rank, count, visible); } private void clickNotificationInternal(String key, int rank, int count, boolean visible) { try { final IStatusBarService svc = getService(); if (svc != null) { svc.onNotificationClick(key, NotificationVisibility.obtain(key, rank, count, visible)); } } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } /** * Simulate notification feedback for testing * * @hide */ @TestApi public void sendNotificationFeedback(@Nullable String key, @Nullable Bundle feedback) { try { final IStatusBarService svc = getService(); if (svc != null) { svc.onNotificationFeedbackReceived(key, feedback); } } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } /** * Expand the notifications panel. * * @hide */ @UnsupportedAppUsage @TestApi public void expandNotificationsPanel() { try { final IStatusBarService svc = getService(); if (svc != null) { svc.expandNotificationsPanel(); } } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } /** * Collapse the notifications and settings panels. * * Starting in Android {@link Build.VERSION_CODES.S}, apps targeting SDK level {@link * Build.VERSION_CODES.S} or higher will need {@link android.Manifest.permission.STATUS_BAR} * permission to call this API. * * @hide */ @RequiresPermission(android.Manifest.permission.STATUS_BAR) @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, publicAlternatives = "This operation" + " is not allowed anymore, please see {@link android.content" + ".Intent#ACTION_CLOSE_SYSTEM_DIALOGS} for more details.") @TestApi public void collapsePanels() { try { final IStatusBarService svc = getService(); if (svc != null) { svc.collapsePanels(); } } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } /** * Toggles the notification panel. * * @hide */ @RequiresPermission(android.Manifest.permission.STATUS_BAR) @TestApi public void togglePanel() { try { final IStatusBarService svc = getService(); if (svc != null) { svc.togglePanel(); } } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } /** * Sends system keys to the status bar. * * @hide */ @RequiresPermission(android.Manifest.permission.STATUS_BAR) @TestApi public void handleSystemKey(@NonNull KeyEvent key) { try { final IStatusBarService svc = getService(); if (svc != null) { svc.handleSystemKey(key); } } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } /** * Gets the last handled system key. A system key is a KeyEvent that the * {@link com.android.server.policy.PhoneWindowManager} sends directly to the * status bar, rather than forwarding to apps. If a key has never been sent to the * status bar, will return -1. * * @return the keycode of the last KeyEvent that has been sent to the system. * @hide */ @RequiresPermission(android.Manifest.permission.STATUS_BAR) @TestApi public int getLastSystemKey() { try { final IStatusBarService svc = getService(); if (svc != null) { return svc.getLastSystemKey(); } } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } return -1; } /** * Expand the settings panel. * * @hide */ @UnsupportedAppUsage public void expandSettingsPanel() { expandSettingsPanel(null); } /** * Expand the settings panel and open a subPanel. If the subpanel is null or does not have a * corresponding tile, the QS panel is simply expanded * * @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public void expandSettingsPanel(@Nullable String subPanel) { try { final IStatusBarService svc = getService(); if (svc != null) { svc.expandSettingsPanel(subPanel); } } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public void setIcon(String slot, int iconId, int iconLevel, String contentDescription) { try { final IStatusBarService svc = getService(); if (svc != null) { svc.setIcon(slot, mContext.getPackageName(), iconId, iconLevel, contentDescription); } } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public void removeIcon(String slot) { try { final IStatusBarService svc = getService(); if (svc != null) { svc.removeIcon(slot); } } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public void setIconVisibility(String slot, boolean visible) { try { final IStatusBarService svc = getService(); if (svc != null) { svc.setIconVisibility(slot, visible); } } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } /** * Enable or disable status bar elements (notifications, clock) which are inappropriate during * device setup. * * @param disabled whether to apply or remove the disabled flags * * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.STATUS_BAR) public void setDisabledForSetup(boolean disabled) { try { final int userId = Binder.getCallingUserHandle().getIdentifier(); final IStatusBarService svc = getService(); if (svc != null) { svc.disableForUser(disabled ? DEFAULT_SETUP_DISABLE_FLAGS : DISABLE_NONE, mToken, mContext.getPackageName(), userId); svc.disable2ForUser(disabled ? DEFAULT_SETUP_DISABLE2_FLAGS : DISABLE2_NONE, mToken, mContext.getPackageName(), userId); } } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } /** * Enable or disable expansion of the status bar. When the device is SIM-locked, the status * bar should not be expandable. * * @param disabled If {@code true}, the status bar will be set to non-expandable. If * {@code false}, re-enables expansion of the status bar. * @hide */ @TestApi @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @RequiresPermission(android.Manifest.permission.STATUS_BAR) public void setExpansionDisabledForSimNetworkLock(boolean disabled) { try { final int userId = Binder.getCallingUserHandle().getIdentifier(); final IStatusBarService svc = getService(); if (svc != null) { svc.disableForUser(disabled ? DEFAULT_SIM_LOCKED_DISABLED_FLAGS : DISABLE_NONE, mToken, mContext.getPackageName(), userId); } } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } /** * Get this app's currently requested disabled components * * @return a new DisableInfo * * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.STATUS_BAR) @NonNull public DisableInfo getDisableInfo() { try { final int userId = Binder.getCallingUserHandle().getIdentifier(); final IStatusBarService svc = getService(); int[] flags = new int[] {0, 0}; if (svc != null) { flags = svc.getDisableFlags(mToken, userId); } return new DisableInfo(flags[0], flags[1]); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } /** * Sets an active {@link android.service.quicksettings.TileService} to listening state * * The {@code componentName}'s package must match the calling package. * * @param componentName the tile to set into listening state * @see android.service.quicksettings.TileService#requestListeningState * @hide */ public void requestTileServiceListeningState(@NonNull ComponentName componentName) { Objects.requireNonNull(componentName); try { getService().requestTileServiceListeningState(componentName, mContext.getUserId()); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } /** * Request to the user to add a {@link android.service.quicksettings.TileService} * to the set of current QS tiles. *

* Calling this will prompt the user to decide whether they want to add the shown * {@link android.service.quicksettings.TileService} to their current tiles. The user can * deny the request and the system can stop processing requests for a given * {@link ComponentName} after a number of requests. *

* The request will show to the user information about the tile: *

*

* The user for which this will be added is determined from the {@link Context} used to retrieve * this service, and must match the current user. The requesting application must be in the * foreground ({@link ActivityManager.RunningAppProcessInfo#IMPORTANCE_FOREGROUND} * and the {@link android.service.quicksettings.TileService} must be exported. * * Note: the system can choose to auto-deny a request if the user has denied that specific * request (user, ComponentName) enough times before. * * @param tileServiceComponentName {@link ComponentName} of the * {@link android.service.quicksettings.TileService} for the request. * @param tileLabel label of the tile to show to the user. * @param icon icon to use in the tile shown to the user. * @param resultExecutor an executor to run the callback on * @param resultCallback callback to indicate the result of the request. * * @see android.service.quicksettings.TileService */ public void requestAddTileService( @NonNull ComponentName tileServiceComponentName, @NonNull CharSequence tileLabel, @NonNull Icon icon, @NonNull Executor resultExecutor, @NonNull Consumer resultCallback ) { Objects.requireNonNull(tileServiceComponentName); Objects.requireNonNull(tileLabel); Objects.requireNonNull(icon); Objects.requireNonNull(resultExecutor); Objects.requireNonNull(resultCallback); if (!tileServiceComponentName.getPackageName().equals(mContext.getPackageName())) { resultExecutor.execute( () -> resultCallback.accept(TILE_ADD_REQUEST_ERROR_MISMATCHED_PACKAGE)); return; } int userId = mContext.getUserId(); RequestResultCallback callbackProxy = new RequestResultCallback(resultExecutor, resultCallback); IStatusBarService svc = getService(); try { svc.requestAddTile( tileServiceComponentName, tileLabel, icon, userId, callbackProxy ); } catch (RemoteException ex) { ex.rethrowFromSystemServer(); } } /** * @hide * @param packageName */ @TestApi public void cancelRequestAddTile(@NonNull String packageName) { Objects.requireNonNull(packageName); IStatusBarService svc = getService(); try { svc.cancelRequestAddTile(packageName); } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** * Sets or removes the navigation bar mode. * * @param navBarMode the mode of the navigation bar to be set. * * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.STATUS_BAR) public void setNavBarMode(@NavBarMode int navBarMode) { if (navBarMode != NAV_BAR_MODE_DEFAULT && navBarMode != NAV_BAR_MODE_KIDS) { throw new IllegalArgumentException("Supplied navBarMode not supported: " + navBarMode); } try { final IStatusBarService svc = getService(); if (svc != null) { svc.setNavBarMode(navBarMode); } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Gets the navigation bar mode. Returns default value if no mode is set. * * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.STATUS_BAR) public @NavBarMode int getNavBarMode() { int navBarMode = NAV_BAR_MODE_DEFAULT; try { final IStatusBarService svc = getService(); if (svc != null) { navBarMode = svc.getNavBarMode(); } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } return navBarMode; } /** * Notifies the system of a new media tap-to-transfer state for the sender device. * *

The callback should only be provided for the {@link * MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED} or {@link * MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED} states, since those are the * only states where an action can be un-done. * * @param displayState the new state for media tap-to-transfer. * @param routeInfo the media route information for the media being transferred. * @param undoExecutor an executor to run the callback on and must be provided if the * callback is non-null. * @param undoCallback a callback that will be triggered if the user elects to undo a media * transfer. * * @throws IllegalArgumentException if an undo callback is provided for states that are not a * succeeded state. * @throws IllegalArgumentException if an executor is not provided when a callback is. * * @hide */ @SystemApi @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) public void updateMediaTapToTransferSenderDisplay( @MediaTransferSenderState int displayState, @NonNull MediaRoute2Info routeInfo, @Nullable Executor undoExecutor, @Nullable Runnable undoCallback ) { Objects.requireNonNull(routeInfo); if (displayState != MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED && displayState != MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED && undoCallback != null) { throw new IllegalArgumentException( "The undoCallback should only be provided when the state is a " + "transfer succeeded state"); } if (undoCallback != null && undoExecutor == null) { throw new IllegalArgumentException( "You must pass an executor when you pass an undo callback"); } IStatusBarService svc = getService(); try { UndoCallback callbackProxy = null; if (undoExecutor != null) { callbackProxy = new UndoCallback(undoExecutor, undoCallback); } svc.updateMediaTapToTransferSenderDisplay(displayState, routeInfo, callbackProxy); } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** * Notifies the system of a new media tap-to-transfer state for the receiver device. * * @param displayState the new state for media tap-to-transfer. * @param routeInfo the media route information for the media being transferred. * @param appIcon the icon of the app playing the media. * @param appName the name of the app playing the media. * * @hide */ @SystemApi @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) public void updateMediaTapToTransferReceiverDisplay( @MediaTransferReceiverState int displayState, @NonNull MediaRoute2Info routeInfo, @Nullable Icon appIcon, @Nullable CharSequence appName) { Objects.requireNonNull(routeInfo); IStatusBarService svc = getService(); try { svc.updateMediaTapToTransferReceiverDisplay(displayState, routeInfo, appIcon, appName); } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** * Registers a provider that notifies callbacks about the status of nearby devices that are able * to play media. *

* If multiple providers are registered, all the providers will be used for nearby device * information. *

* @param provider the nearby device information provider to register *

* @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void registerNearbyMediaDevicesProvider( @NonNull NearbyMediaDevicesProvider provider ) { Objects.requireNonNull(provider); if (nearbyMediaDevicesProviderMap.containsKey(provider)) { return; } try { final IStatusBarService svc = getService(); NearbyMediaDevicesProviderWrapper providerWrapper = new NearbyMediaDevicesProviderWrapper(provider); nearbyMediaDevicesProviderMap.put(provider, providerWrapper); svc.registerNearbyMediaDevicesProvider(providerWrapper); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Unregisters a provider that gives information about nearby devices that are able to play * media. *

* See {@link registerNearbyMediaDevicesProvider}. *

* @param provider the nearby device information provider to unregister *

* @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void unregisterNearbyMediaDevicesProvider( @NonNull NearbyMediaDevicesProvider provider ) { Objects.requireNonNull(provider); if (!nearbyMediaDevicesProviderMap.containsKey(provider)) { return; } try { final IStatusBarService svc = getService(); NearbyMediaDevicesProviderWrapper providerWrapper = nearbyMediaDevicesProviderMap.get(provider); nearbyMediaDevicesProviderMap.remove(provider); svc.unregisterNearbyMediaDevicesProvider(providerWrapper); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Checks whether the given package should use session-based actions for its media controls. * * @param packageName App posting media controls * @param user Current user handle * @return true if the app supports session actions * * @hide */ @RequiresPermission(allOf = {android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG, android.Manifest.permission.LOG_COMPAT_CHANGE}) public static boolean useMediaSessionActionsForApp(String packageName, UserHandle user) { return CompatChanges.isChangeEnabled(MEDIA_CONTROL_SESSION_ACTIONS, packageName, user); } /** * Log that the given package has posted media controls with a blank title * * @param packageName App posting media controls * @param userId Current user ID * @throws RuntimeException if there is an error reporting the change * * @hide */ public void logBlankMediaTitle(String packageName, @UserIdInt int userId) throws RuntimeException { try { mPlatformCompat.reportChangeByPackageName(MEDIA_CONTROL_BLANK_TITLE, packageName, userId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Checks whether the supplied activity can {@link Activity#startActivityForResult(Intent, int)} * a system activity that captures content on the screen to take a screenshot. * *

Note: The result should not be cached. * *

The system activity displays an editing tool that allows user to edit the screenshot, save * it on device, and return the edited screenshot as {@link android.net.Uri} to the calling * activity. User interaction is required to return the edited screenshot to the calling * activity. * *

When {@code true}, callers can use {@link Activity#startActivityForResult(Intent, int)} * to start start the content capture activity using * {@link Intent#ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE}. * * @param activity Calling activity * @return true if the activity supports launching the capture content activity for note. * * @see Intent#ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE * @see Manifest.permission#LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE * @see android.app.role.RoleManager#ROLE_NOTES */ @RequiresPermission(Manifest.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE) public boolean canLaunchCaptureContentActivityForNote(@NonNull Activity activity) { Objects.requireNonNull(activity); IBinder activityToken = activity.getActivityToken(); int taskId = ActivityClient.getInstance().getTaskForActivity(activityToken, false); return new AppClipsServiceConnector(mContext) .canLaunchCaptureContentActivityForNote(taskId); } /** @hide */ public static String windowStateToString(int state) { if (state == WINDOW_STATE_HIDING) return "WINDOW_STATE_HIDING"; if (state == WINDOW_STATE_HIDDEN) return "WINDOW_STATE_HIDDEN"; if (state == WINDOW_STATE_SHOWING) return "WINDOW_STATE_SHOWING"; return "WINDOW_STATE_UNKNOWN"; } /** * DisableInfo describes this app's requested state of the StatusBar with regards to which * components are enabled/disabled * * @hide */ @SystemApi public static final class DisableInfo { private boolean mStatusBarExpansion; private boolean mNavigateHome; private boolean mNotificationPeeking; private boolean mRecents; private boolean mSearch; private boolean mSystemIcons; private boolean mClock; private boolean mNotificationIcons; private boolean mRotationSuggestion; /** @hide */ public DisableInfo(int flags1, int flags2) { mStatusBarExpansion = (flags1 & DISABLE_EXPAND) != 0; mNavigateHome = (flags1 & DISABLE_HOME) != 0; mNotificationPeeking = (flags1 & DISABLE_NOTIFICATION_ALERTS) != 0; mRecents = (flags1 & DISABLE_RECENT) != 0; mSearch = (flags1 & DISABLE_SEARCH) != 0; mSystemIcons = (flags1 & DISABLE_SYSTEM_INFO) != 0; mClock = (flags1 & DISABLE_CLOCK) != 0; mNotificationIcons = (flags1 & DISABLE_NOTIFICATION_ICONS) != 0; mRotationSuggestion = (flags2 & DISABLE2_ROTATE_SUGGESTIONS) != 0; } /** @hide */ public DisableInfo() {} /** * @return {@code true} if expanding the notification shade is disabled * * @hide */ @SystemApi public boolean isStatusBarExpansionDisabled() { return mStatusBarExpansion; } /** * @hide */ public void setStatusBarExpansionDisabled(boolean disabled) { mStatusBarExpansion = disabled; } /** * @return {@code true} if navigation home is disabled * * @hide */ @SystemApi public boolean isNavigateToHomeDisabled() { return mNavigateHome; } /** * @hide */ public void setNagivationHomeDisabled(boolean disabled) { mNavigateHome = disabled; } /** * @return {@code true} if notification peeking (heads-up notification) is disabled * * @hide */ @SystemApi public boolean isNotificationPeekingDisabled() { return mNotificationPeeking; } /** @hide */ public void setNotificationPeekingDisabled(boolean disabled) { mNotificationPeeking = disabled; } /** * @return {@code true} if mRecents/overview is disabled * * @hide */ @SystemApi public boolean isRecentsDisabled() { return mRecents; } /** @hide */ public void setRecentsDisabled(boolean disabled) { mRecents = disabled; } /** * @return {@code true} if mSearch is disabled * * @hide */ @SystemApi public boolean isSearchDisabled() { return mSearch; } /** @hide */ public void setSearchDisabled(boolean disabled) { mSearch = disabled; } /** * @return {@code true} if system icons are disabled * * @hide */ public boolean areSystemIconsDisabled() { return mSystemIcons; } /** * @hide */ public void setSystemIconsDisabled(boolean disabled) { mSystemIcons = disabled; } /** * @return {@code true} if the clock icon is disabled * * @hide */ public boolean isClockDisabled() { return mClock; } /** * @hide */ public void setClockDisabled(boolean disabled) { mClock = disabled; } /** * @return {@code true} if notification icons are disabled * * @hide */ public boolean areNotificationIconsDisabled() { return mNotificationIcons; } /** * @hide */ public void setNotificationIconsDisabled(boolean disabled) { mNotificationIcons = disabled; } /** * Returns whether the rotation suggestion is disabled. * * @hide */ @TestApi public boolean isRotationSuggestionDisabled() { return mRotationSuggestion; } /** * @return {@code true} if no components are disabled (default state) * @hide */ @SystemApi public boolean areAllComponentsEnabled() { return !mStatusBarExpansion && !mNavigateHome && !mNotificationPeeking && !mRecents && !mSearch && !mSystemIcons && !mClock && !mNotificationIcons && !mRotationSuggestion; } /** @hide */ public void setEnableAll() { mStatusBarExpansion = false; mNavigateHome = false; mNotificationPeeking = false; mRecents = false; mSearch = false; mSystemIcons = false; mClock = false; mNotificationIcons = false; mRotationSuggestion = false; } /** * @return {@code true} if all status bar components are disabled * * @hide */ public boolean areAllComponentsDisabled() { return mStatusBarExpansion && mNavigateHome && mNotificationPeeking && mRecents && mSearch && mSystemIcons && mClock && mNotificationIcons && mRotationSuggestion; } /** @hide */ public void setDisableAll() { mStatusBarExpansion = true; mNavigateHome = true; mNotificationPeeking = true; mRecents = true; mSearch = true; mSystemIcons = true; mClock = true; mNotificationIcons = true; mRotationSuggestion = true; } @NonNull @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("DisableInfo: "); sb.append(" mStatusBarExpansion=").append(mStatusBarExpansion ? "disabled" : "enabled"); sb.append(" mNavigateHome=").append(mNavigateHome ? "disabled" : "enabled"); sb.append(" mNotificationPeeking=") .append(mNotificationPeeking ? "disabled" : "enabled"); sb.append(" mRecents=").append(mRecents ? "disabled" : "enabled"); sb.append(" mSearch=").append(mSearch ? "disabled" : "enabled"); sb.append(" mSystemIcons=").append(mSystemIcons ? "disabled" : "enabled"); sb.append(" mClock=").append(mClock ? "disabled" : "enabled"); sb.append(" mNotificationIcons=").append(mNotificationIcons ? "disabled" : "enabled"); sb.append(" mRotationSuggestion=").append(mRotationSuggestion ? "disabled" : "enabled"); return sb.toString(); } /** * Convert a DisableInfo to equivalent flags * @return a pair of equivalent disable flags * * @hide */ public Pair toFlags() { int disable1 = DISABLE_NONE; int disable2 = DISABLE2_NONE; if (mStatusBarExpansion) disable1 |= DISABLE_EXPAND; if (mNavigateHome) disable1 |= DISABLE_HOME; if (mNotificationPeeking) disable1 |= DISABLE_NOTIFICATION_ALERTS; if (mRecents) disable1 |= DISABLE_RECENT; if (mSearch) disable1 |= DISABLE_SEARCH; if (mSystemIcons) disable1 |= DISABLE_SYSTEM_INFO; if (mClock) disable1 |= DISABLE_CLOCK; if (mNotificationIcons) disable1 |= DISABLE_NOTIFICATION_ICONS; if (mRotationSuggestion) disable2 |= DISABLE2_ROTATE_SUGGESTIONS; return new Pair(disable1, disable2); } } /** * @hide */ static final class RequestResultCallback extends IAddTileResultCallback.Stub { @NonNull private final Executor mExecutor; @NonNull private final Consumer mCallback; RequestResultCallback(@NonNull Executor executor, @NonNull Consumer callback) { mExecutor = executor; mCallback = callback; } @Override public void onTileRequest(int userResponse) { mExecutor.execute(() -> mCallback.accept(userResponse)); } } /** * @hide */ static final class UndoCallback extends IUndoMediaTransferCallback.Stub { @NonNull private final Executor mExecutor; @NonNull private final Runnable mCallback; UndoCallback(@NonNull Executor executor, @NonNull Runnable callback) { mExecutor = executor; mCallback = callback; } @Override public void onUndoTriggered() { final long callingIdentity = Binder.clearCallingIdentity(); try { mExecutor.execute(mCallback); } finally { restoreCallingIdentity(callingIdentity); } } } /** * @hide */ static final class NearbyMediaDevicesProviderWrapper extends INearbyMediaDevicesProvider.Stub { @NonNull private final NearbyMediaDevicesProvider mProvider; // Because we're wrapping a {@link NearbyMediaDevicesProvider} in a binder-compatible // interface, we also need to wrap the callbacks that the provider receives. We use // this map to keep track of the original callback and the wrapper callback so that // unregistering the callback works correctly. @NonNull private final Map>> mRegisteredCallbacks = new HashMap<>(); NearbyMediaDevicesProviderWrapper(@NonNull NearbyMediaDevicesProvider provider) { mProvider = provider; } @Override public void registerNearbyDevicesCallback( @NonNull INearbyMediaDevicesUpdateCallback callback) { Consumer> callbackAsConsumer = nearbyDevices -> { try { callback.onDevicesUpdated(nearbyDevices); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } }; mRegisteredCallbacks.put(callback, callbackAsConsumer); mProvider.registerNearbyDevicesCallback(callbackAsConsumer); } @Override public void unregisterNearbyDevicesCallback( @NonNull INearbyMediaDevicesUpdateCallback callback) { if (!mRegisteredCallbacks.containsKey(callback)) { return; } mProvider.unregisterNearbyDevicesCallback(mRegisteredCallbacks.get(callback)); mRegisteredCallbacks.remove(callback); } } }